/* This file is part of Ext JS 6.2.0.981 Copyright (c) 2011-2016 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: 6.2.0.981 Build date: 2016-08-31 14:49:44 (08dbbd0ec0b8bc0e014d725fdb7d9650d510b343) */ // @tag core // @define Ext.Boot var Ext = Ext || {}; // /** * @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; Boot.isIE10 = (new Function('/*@cc_on return @_jscript_version @*/')()) === 10; 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 element creation / insertion, // but * * * * 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) { if (name === 'paths') { Loader.setPath(value); } else { _config[name] = value; var delegated = delegatedConfigs[name]; 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) { for (var 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'); * * alert(Ext.Loader.getPath('My.awesome.Class')); // alerts '/path/to/My/awesome/Class.js' * * 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' * }); * * alert(Ext.Loader.getPath('My.awesome.Class')); // alerts '/other/path/for/awesome/stuff/Class.js' * * alert(Ext.Loader.getPath('My.awesome.more.Class')); // alerts '/more/awesome/path/Class.js' * * alert(Ext.Loader.getPath('My.cool.Class')); // alerts '/path/to/lib/cool/Class.js' * * alert(Ext.Loader.getPath('Unknown.strange.Stuff')); // alerts 'Unknown/strange/Stuff.js' * * @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) { if (excludes) { return Loader.exclude(excludes).require(expressions, fn, scope); } var classNames = Manager.getNamesByExpression(expressions); return Loader.load(classNames, fn, scope); }, syncRequire: function() { var wasEnabled = Loader.syncModeEnabled; Loader.syncModeEnabled = true; var 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; Loader.syncModeEnabled = true; var 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); } var state = Manager.classState, missingClassNames = [], urls = [], urlByClass = {}, numClasses = classNames.length, url, 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() { var options = this, onError = options.onError; Loader.hasFileLoadError = true; --Loader.scriptsLoading; if (onError) { //TODO: need an adapter to convert to v4 onError signatures onError.call(options.userScope, options); } 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() { if (!Loader.syncModeEnabled && !Loader.scriptsLoading && Loader.isLoading && !Loader.hasFileLoadError) { var missingQueue = Loader.missingQueue, missingClasses = [], missingPaths = []; for (var 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) { if (withDomReady) { Ready.on(fn, scope, options); } else { var 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 = {}, name, missing = 0; 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() { if (!Loader.scriptsLoading && Loader.missingCount) { Ext.log.error('[Loader] The following classes failed to load:'); for (var 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) { Ext.classSystemMonitor && Ext.classSystemMonitor(cls, 'Ext.Loader#loaderPreprocessor', arguments); // jshint ignore:line 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; } 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) { deadlockPath.push(cls); var requires = _requiresMap[cls], dep, i, ln; 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() { 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 (var 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) { Ext.classSystemMonitor && Ext.classSystemMonitor(cls, 'Ext.Loader#usesPostprocessor', arguments); // jshint ignore:line var uses = data.uses, classNames; 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 * * ### 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 { 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, parentMixinConfig, befores, afters, extended; 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'); } befores = mixinConfig.before; afters = mixinConfig.after; hooks = mixinConfig.on; extended = mixinConfig.extended; } if (befores || afters || 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.addMember(key, function() { if (mixin[value].apply(this, arguments) !== false) { return this.callParent(arguments); } }); }); } if (afters) { Ext.Object.each(afters, function(key, value) { targetClass.addMember(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); } }; } } }; }); // @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 (optional) The default function to call. If not specified here, it must be specified during the {@link #delay} call. * @param {Object} scope (optional) 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 (optional) 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() { var globalEvents = Ext.GlobalEvents; clearInterval(me.id); me.id = null; fn.apply(scope, args || []); if (fireIdleEvent !== false && globalEvents.hasListeners.idle) { globalEvents.fireEvent('idle'); } }; 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 * @param {Function} newFn (optional) Overrides function passed to constructor * @param {Object} newScope (optional) Overrides scope passed to constructor. Remember that if no scope * is specified, this will refer to the browser window. * @param {Array} newArgs (optional) Overrides args passed to constructor */ 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; if (!me.id) { me.id = Ext.interval(call, delay); } }; /** * Cancel the last queued timeout */ me.cancel = function() { if (me.id) { clearInterval(me.id); me.id = null; } }; }; // @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 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).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, namedScope = Ext._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; // 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 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; } 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) { observable._getPublisher(eventName).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 chained = e; e = args[0] = chained.chain({ type: type }); } // 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" delegateEl = e.getTarget('#' + 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; } } 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) { // DON'T raise errors if the destroyed scope is an Ext.container.Monitor! // It is to be deprecated and removed shortly. if (fireScope.$className !== 'Ext.container.Monitor') { Ext.raise({ msg: 'Attempting to fire "' + me.name + '" event on destroyed ' + (fireScope.$className || 'object') + ' instance with id: ' + (fireScope.id || 'unknown'), instance: fireScope }); } me.removeListener(fireFn, fireScope, i); fireFn = null; } // 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; // 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; if (listener.lateBound) { // handler is a function name - need to resolve it to a function reference 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 = (listener.caller || observable).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 tghe 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() { if (o.target === arguments[0]) { 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 handler.apply(scope, arguments); } }; }, createBuffered: function(handler, listener, o, scope, wrapped) { listener.task = new Ext.util.DelayedTask(); return function() { // 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) { 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; } 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); 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) { 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 /** * An Identifiable mixin. * @private */ Ext.define('Ext.mixin.Identifiable', { statics: { uniqueIds: {} }, isIdentifiable: true, mixinId: 'identifiable', idCleanRegex: /\.|[^\w\-]/g, defaultIdPrefix: 'ext-', defaultIdSeparator: '-', getOptimizedId: function() { return this.id; }, getUniqueId: function() { var id = this.id, prototype, separator, xtype, uniqueIds, prefix; // Cannot test falsiness. Zero is a valid ID. if (!(id || id === 0)) { prototype = this.self.prototype; separator = this.defaultIdSeparator; uniqueIds = Ext.mixin.Identifiable.uniqueIds; if (!prototype.hasOwnProperty('identifiablePrefix')) { xtype = this.xtype; if (xtype) { prefix = this.defaultIdPrefix + xtype.replace(this.idCleanRegex, separator) + separator; } else if (!(prefix = prototype.$className)) { prefix = this.defaultIdPrefix + 'anonymous' + separator; } else { prefix = prefix.replace(this.idCleanRegex, separator).toLowerCase() + separator; } prototype.identifiablePrefix = prefix; } prefix = this.identifiablePrefix; if (!uniqueIds.hasOwnProperty(prefix)) { uniqueIds[prefix] = 0; } // 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. id = this.id = this.id = prefix + (++uniqueIds[prefix]); } this.getUniqueId = this.getOptimizedId; return id; }, setId: function(id) { // See getUniqueId() this.id = this.id = id; }, /** * Retrieves the id of this component. Will autogenerate an id if one has not already been set. * @return {String} id */ getId: function() { var id = this.id; if (!id) { id = this.getUniqueId(); } this.getId = this.getOptimizedId; return id; } }); // @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() { this.destroy = Ext.emptyFn; var observable = this.observable; // If that observable is already destroyed, all its listeners were cleared if (!observable.destroyed) { observable[this.managedListeners ? 'mun' : 'un'].apply(observable, this.args); } }; 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} c 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; }, /** * 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, name, 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). 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.SomeGlobalSharedMenu, '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. * @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, /* private */ noDestroy) { var me = this, managedListeners = me.managedListeners = me.managedListeners || [], config, passedOptions; 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) { eventName = Ext.canonicalEventName(eventName); var me = this, // no need to make events since we need an Event with listeners events = me.events, event = events && events[eventName], ret = true; // 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, owner = me.owner, 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 arguments to pass to the handler function. Any additional arguments * passed to {@link Ext.util.Observable#fireEvent fireEvent} will be appended * to these 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. * * **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: '

BODY TITLE

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(ename, fn, scope, options, order, /* private */ caller) { var me = this, namedScopes = Ext._namedScopes, config, namedScope, isClassListener, innerScope, eventOptions; // Object listener hash passed if (typeof ename !== 'string') { options = ename; 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 (ename in options) { config = options[ename]; if (!eventOptions[ename]) { /* 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(ename, config.fn || config, innerScope || scope, config.fn ? config : options, order, caller); } } if (options && options.destroyable) { return new ListenerRemover(me, options); } } else { me.doAddListener(ename, fn, scope, options, order, caller); if (options && options.destroyable) { return new ListenerRemover(me, ename, 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. * * **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(ename, fn, scope, /* private */ eventOptions) { var me = this, config, options; if (typeof ename !== 'string') { options = ename; // give subclasses the opportunity to switch the valid eventOptions // (Ext.Component uses this when the "element" option is used) eventOptions = eventOptions || me.$eventOptions; for (ename in options) { if (options.hasOwnProperty(ename)) { config = options[ename]; if (!me.$eventOptions[ename]) { me.doRemoveListener(ename, config.fn || config, config.scope || options.scope); } } } } else { me.doRemoveListener(ename, 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 ? me.managedListeners.slice() : [], i = 0, len = managedListeners.length; for (; i < len; i++) { me.removeManagedListenerItem(true, managedListeners[i]); } me.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 * 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(ename) { ename = Ext.canonicalEventName(ename); return !!this.hasListeners[ename]; }, /** * 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() { 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) { 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; 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 (me.clearPropertiesOnDestroy) { // 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) { if (!T.HasListeners) { var proto = T.prototype; // 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) { 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 * @inheritdoc Ext.util.Observable#addListener */ on: 'addListener', /** * @method * Shorthand for {@link #removeListener}. * @inheritdoc Ext.util.Observable#removeListener */ un: 'removeListener', /** * @method * Shorthand for {@link #addManagedListener}. * @inheritdoc Ext.util.Observable#addManagedListener */ mon: 'addManagedListener', /** * @method * Shorthand for {@link #removeManagedListener}. * @inheritdoc Ext.util.Observable#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; // this is considered experimental (along with beforeMethod, afterMethod, removeMethodListener?) // allows for easier interceptor and sequences, including cancelling and overwriting the return value of the call // private function getMethodEvent(method) { var e = (this.methodEvents = this.methodEvents || {})[method], returnValue, v, cancel, obj = this, makeCall; if (!e) { this.methodEvents[method] = e = {}; e.originalFn = this[method]; e.methodName = method; e.before = []; e.after = []; makeCall = function(fn, scope, args) { if ((v = fn.apply(scope || obj, 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; } } }; this[method] = function() { var args = Array.prototype.slice.call(arguments, 0), b, i, len; returnValue = v = undefined; cancel = false; for (i = 0 , len = e.before.length; i < len; i++) { b = e.before[i]; makeCall(b.fn, b.scope, args); if (cancel) { return returnValue; } } if ((v = e.originalFn.apply(obj, args)) !== undefined) { returnValue = v; } for (i = 0 , len = e.after.length; i < len; i++) { b = e.after[i]; makeCall(b.fn, b.scope, args); if (cancel) { return returnValue; } } return returnValue; }; } return e; } Ext.apply(proto, { onClassMixedIn: prepareMixin, // these are considered experimental // allows for easier interceptor and sequences, including cancelling and overwriting the return value of the call // adds an 'interceptor' called before the original method beforeMethod: function(method, fn, scope) { getMethodEvent.call(this, method).before.push({ fn: fn, scope: scope }); }, // adds a 'sequence' called after the original method afterMethod: function(method, fn, scope) { getMethodEvent.call(this, method).after.push({ fn: fn, scope: scope }); }, removeMethodListener: function(method, fn, scope) { var e = this.getMethodEvent(method), i, len; for (i = 0 , len = e.before.length; i < len; i++) { 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++) { 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} [o] 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 this. * @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 undefined 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 */ Ext.define('Ext.AbstractManager', { /* Begin Definitions */ requires: [ 'Ext.util.HashMap' ], /* End Definitions */ 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 { /** * @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. */ 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} value 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; } } }; }); /* 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; 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); }, /** * 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} promiseOrValue 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 Ext.promise.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 = Ext.Function.bind(onFulfilled, scope); } if (onRejected) { onRejected = Ext.Function.bind(onRejected, scope); } if (onProgress) { onProgress = Ext.Function.bind(onProgress, 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. */ otherwise: 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 = Ext.Function.bind(onRejected, scope); } return this.owner.then(null, onRejected); }, /** * 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 = Ext.Function.bind(onCompleted, 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() { 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); }, race: function() { //TODO Ext.raise("Not implemented"); }, /** * 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} promiseOrValue 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.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) { var ExtPromise, when; 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, 0); deferred = new Deferred(); setTimeout(function() { deferred.resolve(promiseOrValue); }, milliseconds); return deferred.promise; }, /** * 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) { if (scope == null) { scope = null; } var 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); }, /** * 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) { 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.'); } var 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(value) { var deferred = new Ext.Deferred(); deferred.resolve(value); 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) { if (scope == null) { scope = null; } var 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 = setTimeout(function() { if (timeoutId) { deferred.reject(new Error('Promise timed out.')); } }, milliseconds); Deferred.resolved(promiseOrValue).then(function(value) { clearTimeout(timeoutId); timeoutId = null; deferred.resolve(value); }, function(reason) { clearTimeout(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#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; }; 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 */ /** * 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, 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.type; } if (defaultType && defaultType.constructor === Object) { config = Ext.apply({}, config, defaultType); defaultType = defaultType.type; } 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 = {}; } }; /** * 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 prefix for type (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, 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; } } Factory[factory.name] = fn = factory.create.bind(factory); fn.instance = factory; } return 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() { return (this.deferred = new Ext.Deferred()); }, // deliberate assignment getDeferred: function() { return this.deferred || this.createDeferred(); }, getPromise: function() { return this.getDeferred().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); } } }, onTimeout: function() { var me = this; me.timedout = true; me.timer = null; me.abort(true); }, getTimeout: function() { return this.timeout; }, clearTimer: function() { var timer = this.timer; if (timer) { clearTimeout(timer); this.timer = null; } }, 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, /** * Plcaeholder 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 connection = this.liveConnections[Number(javascriptId)]; // Make sure its a native number 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; // Generate the following HTML set of tags: // + '
' // + '

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.

' //+ 'Get Adobe Flash player' //+ '
' 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('/'); if (Ext.flashPluginPath) { flashObjectPath = Ext.flashPluginPath; } //console.log('LOADING Flash plugin from: ' + flashObjectPath); 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. var swfVersionStr = "11.4.0"; // To use express install, set to playerProductInstall.swf, otherwise the empty string. var xiSwfUrlStr = "playerProductInstall.swf"; var flashvars = {}; var params = {}; params.quality = "high"; params.bgcolor = "#ffffff"; params.allowscriptaccess = "sameDomain"; params.allowfullscreen = "true"; var attributes = {}; attributes.id = "ext-flash-polyfill"; attributes.name = "polyfill"; attributes.align = "middle"; swfobject.embedSWF(flashObjectPath, "ext-flash-polyfill", "0", "0", // no size so it's not visible. swfVersionStr, xiSwfUrlStr, flashvars, params, attributes); }, onError: function() { 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) { // first, make sure flash is loading if needed if (!Ext.data.flash.BinaryXhr.flashPluginInjected) { Ext.data.flash.BinaryXhr.injectFlashPlugin(); } var me = this; 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, async, user, password) { var me = this; me.method = method; me.url = url; me.async = async !== false; 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, status; 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 }; status = 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 len; if (response) { //We have to account for binary response type if (response.responseType === 'arraybuffer') { len = response.byteLength; } else if (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; var success = (status >= 200 && status < 300) || status == 304 || (status == 0 && Ext.isNumber(len)), isException = false; 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, headers; xhr = me.xhr = me.openRequest(options, requestOptions, me.async, me.username, me.password); // XDR doesn't support setting any headers if (!isXdr) { headers = me.setupHeaders(xhr, options, requestOptions.data, requestOptions.params); } if (me.async) { if (!isXdr) { xhr.onreadystatechange = Ext.Function.bind(me.onStateChange, me); } } 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, async, username, password) { var me = this, xhr = me.newRequest(options); if (username) { xhr.open(requestOptions.method, requestOptions.url, async, username, password); } else { if (me.isXdr) { xhr.open(requestOptions.method, requestOptions.url); } else { xhr.open(requestOptions.method, requestOptions.url, async); } } 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.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) { // On browsers that support this, use the native XHR object 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 { Ext.raise({ msg: 'Your browser does not support CORS' }); } return xdr; }, /** * Creates the appropriate XHR transport for this browser. * @private */ getXhrInstance: (function() { var options = [ function() { return new XMLHttpRequest(); }, function() { return new ActiveXObject('MSXML2.XMLHTTP.3.0'); }, // jshint ignore:line function() { return new ActiveXObject('MSXML2.XMLHTTP'); }, // jshint ignore:line function() { return new ActiveXObject('Microsoft.XMLHTTP'); } ], // jshint ignore:line i = 0, len = options.length, xhr; for (; i < len; ++i) { try { xhr = options[i]; xhr(); break; } catch (e) {} } return xhr; }()), 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 = Ext.Function.bind(me.onStateChange, me, [ true ]); xhr.onerror = xhr.ontimeout = Ext.Function.bind(me.onStateChange, me, [ 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; }, onStateChange: function(xdrResult) { var me = this, xhr = me.xhr, globalEvents = Ext.GlobalEvents; // 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(); if (globalEvents.hasListeners.idle) { globalEvents.fireEvent('idle'); } } }, /** * To be called when the request has come back from the server * @param {Object} request * @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} request * @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, byteArray; 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 { // an error is thrown when trying to access responseText or responseXML // on an xhr object with responseType of 'arraybuffer', so only attempt // to set these properties in the response if we're not dealing with // binary data 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. 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 { byteArray = new VBArray(responseBody).toArray(); } // jshint ignore:line 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(); } getIEByteArray(xhr.responseBody, byteArray = []); } else // jshint ignore:line { // 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'; 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'); 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 || {}; 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, request; /* * 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); // 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(); // 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 = '{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
 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 = '{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 <form> element temporarily modified to have its
 * target 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 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 `<`, `&` as
 * `&` 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,
        /**
         * @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 = {};
    },
    /**
     * 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 `
` Element or the id of the `` * 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 `` element temporarily modified to have its [target][] set to refer to a dynamically * generated, hidden `', '{afterIFrameTpl}', { disableFormats: true } ], 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 */ 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 ? ' ' : '​', /** * @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', 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() ]; me.layout = { type: 'vbox', align: 'stretch' }; // 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 && !Ext.isSafari2) { fontSelectItem = Ext.widget('component', { itemId: 'fontSelect', renderTpl: [ '' ], 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 ? '#' + 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 { me.relayCmd(Ext.isOpera ? 'hilitecolor' : 'backcolor', Ext.isWebKit || Ext.isIE || Ext.isOpera ? '#' + color : color); } this.up('menu').hide(); } } ] }) }, me.buttonDefaults)); } if (me.enableAlignments) { items.push('-', btn('justifyleft'), btn('justifycenter'), btn('justifyright')); } if (!Ext.isSafari2) { 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; // - IE9+ require a strict doctype otherwise text outside visible area can't be selected. // - Opera inserts

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('' + '', me.iframePad, h, me.defaultFont); }, /** * @private */ getEditorBody: function() { var doc = this.getDoc(); return doc.body || doc.documentElement; }, /** * @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 for when the iframe document is ready to be manipulated me.monitorTask = Ext.TaskManager.start({ run: me.checkDesignMode, scope: me, interval: 100 }); }, initFrameDoc: function() { var me = this, doc, task; Ext.TaskManager.stop(me.monitorTask); doc = me.getDoc(); me.win = me.getWin(); doc.open(); doc.write(me.getDocMarkup()); doc.close(); task = { // must defer to wait for browser to be ready run: function() { var doc = me.getDoc(); if (doc.body || doc.readyState === 'complete') { Ext.TaskManager.stop(task); me.setDesignMode(true); Ext.defer(me.initEditor, 10, me); } }, interval: 10, duration: 10000, scope: me }; Ext.TaskManager.start(task); }, checkDesignMode: function() { var me = this, doc = me.getDoc(); if (doc && (!doc.editorInitialized || me.getDesignMode() !== 'on')) { me.initFrameDoc(); } }, /** * @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, bodyStyle, match, textElDom; if (me.initialized) { body = me.getEditorBody(); html = body.innerHTML; textElDom = me.textareaEl.dom; if (Ext.isWebKit) { bodyStyle = body.getAttribute('style'); // Safari puts text-align styles on the body element! match = bodyStyle.match(me.textAlignRE); if (match && match[1]) { html = '

' + html + '
'; } } html = me.cleanHtml(html); if (me.fireEvent('beforesync', me, html) !== false) { // Gecko inserts single
tag when input is empty // and user toggles source mode. See https://sencha.jira.com/browse/EXTJSIV-8542 if (Ext.isGecko && textElDom.value === '' && html === '
') { 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; } if (value && value.length) { // Make sure there is content before calling SelectAll, otherwise the caret disappears. me.execCmd('selectall', true); } } focusEl = me.getFocusEl(); if (focusEl && focusEl.focus) { focusEl.focus(); } } return me; }, /** * @private */ initEditor: function() { var me = this, dbody, ss, doc, docEl, fn; //Destroying the component during/before initEditor can cause issues. if (me.destroying || me.destroyed) { return; } dbody = me.getEditorBody(); // IE has a null reference when it first comes online. if (!dbody) { setTimeout(function() { me.initEditor(); }, 10); return; } ss = me.textareaEl.getStyle([ 'font-size', 'font-family', 'background-image', 'background-repeat', 'background-color', 'color' ]); ss['background-attachment'] = 'fixed'; // w3c dbody.bgProperties = 'fixed'; // ie Ext.DomHelper.applyStyles(dbody, ss); doc = me.getDoc(); docEl = Ext.get(doc); if (docEl) { try { docEl.clearListeners(); } catch (e) {} /* * We need to use createDelegate here, because when using buffer, the delayed task is added * as a property to the function. When the listener is removed, the task is deleted from the function. * Since onEditorEvent is shared on the prototype, if we have multiple html editors, the first time one of the editors * is destroyed, it causes the fn to be deleted from the prototype, which causes errors. Essentially, we're just anonymizing the function. */ fn = me.onEditorEvent.bind(me); docEl.on({ mousedown: fn, dblclick: fn, click: fn, keyup: fn, delegated: false, buffer: 100 }); // 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); doc.editorInitialized = true; me.initialized = true; me.pushValue(); me.setReadOnly(me.readOnly); me.fireEvent('initialize', me); } }, /** * @private */ destroyEditor: function() { var me = this, monitorTask = me.monitorTask, doc, prop; if (monitorTask) { Ext.TaskManager.stop(monitorTask); } 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 delete me.iframeEl; delete me.textareaEl; delete me.toolbar; delete me.inputCmp; } }, /** * @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
tag, clicking on the editor after the text where // the
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); }, /** * @private */ onEditorEvent: function() { this.updateToolbar(); }, /** * 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 && !Ext.isSafari2) { // 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'); 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 (!Ext.isSafari2 && 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) { 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, firstNode; 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); } firstNode = frag.firstChild; 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() { var tag; if (Ext.isIE10m) { return function(e) { var me = this, k = e.getKey(), doc = me.getDoc(), readOnly = me.readOnly, range, target; 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('    '); } 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', '    '); 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

 

// 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 '

 

', switch back to visual mode, type something else // and then clear it -- the code below would clear the

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 === '

 

' || innerHTML === '

 

') { 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' * } * // ... * } */ 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 * @private */ /** * @event focus * @private */ /** * @event specialkey * @private */ /** * @cfg {String} fieldCls * @private */ /** * @cfg {String} focusCls * @private */ /** * @cfg {String} autoCreate * @private */ /** * @cfg {String} inputType * @private */ /** * @cfg {String} invalidCls * @private */ /** * @cfg {String} invalidText * @private */ /** * @cfg {Boolean} allowDomMove * @private */ /** * @cfg {String} readOnly * @private */ /** * @cfg {String} tabIndex * @private */ /** * @method validate * @private */ privates: { deferFocus: function() { this.focus(false, true); }, getFocusEl: function() { return this.sourceEditMode ? this.textareaEl : this.iframeEl; } } }); 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', requires: [ 'Ext.selection.Model', 'Ext.data.Store', 'Ext.data.ChainedStore', 'Ext.view.TagKeyNav' ], xtype: 'tagfield', 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 * @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=true] Set to `false` to disable clearing selected * values with Backspace key. This mode is recommended for accessible applications. */ clearOnBackspace: true, /** * @cfg {Boolean} * * `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} * 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} * 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, /** * @private * @cfg */ simulatePlaceholder: true, /** * @cfg * @inheritdoc */ selectOnFocus: true, /** * @cfg growAppend * @hide * Currently unsupported since this is used for horizontal growth and this component * only supports vertical growth. */ /** * @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. */ 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}. */ 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. */ 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. */ 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. */ 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. */ 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. */ 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 */ /** * @private * @cfg */ fieldSubTpl: [ // listWrapper div is tabbable in Firefox, for some unfathomable reason '
{$}="{.}"', ' class="' + Ext.baseCSSPrefix + 'tagfield {fieldCls} {typeCls} {typeCls}-{ui}" style="{wrapperStyle}">', '', '', '
    aria-label="{ariaSelectedListLabel}"', ' aria-multiselectable="true"', ' class="' + Ext.baseCSSPrefix + 'tagfield-arialist">', '
', '
', { disableFormats: true } ], postSubTpl: [ '', '', // end inputWrap '{[values.renderTrigger(parent)]}', '' ], // end triggerWrap extraFieldBodyCls: Ext.baseCSSPrefix + 'tagfield-body', /** * @private */ childEls: [ 'listWrapper', 'itemList', 'inputEl', 'inputElCt', 'selectedText', 'ariaList' ], /** * @private */ clearValueOnEmpty: false, ariaSelectable: true, 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; }, 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(arguments); me.emptyClsElements.push(me.listWrapper, me.placeholderLabel); }, 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) { 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'); } me.multiSelectItemTpl = new Ext.XTemplate([ '', '', '', { 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 } ]); } 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, store, values; if (!me.ariaListItemTpl) { me.ariaListItemTpl = new Ext.XTemplate([ '', '
  • ', '{[this.getItemLabel(values.data)]}', '
  • ', '
    ', { 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 } ]); } 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) { me.ariaSelectedItemTpl = new Ext.XTemplate([ '', '{[this.getItemLabel(values.data)]}', '', { getItemLabel: function(values) { return Ext.String.htmlEncode(me.labelTpl.apply(values)); }, strict: true } ]); } 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(); }, /** * @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(); } }, /** * 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(); }, /** * @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; } } } }); Ext.define('Ext.rtl.form.field.Tag', { override: 'Ext.form.field.Tag', privates: { _getChildElCls: function() { return this.getInherited().rtl ? (' ' + this._rtlCls) : ''; } } }); /** * A time picker which provides a list of times from which to choose. This is used by the 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 = 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. */ format: "g:i A", // /** * @private * 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. */ displayField: 'disp', /** * @private * Year, month, and day that all times will be normalized into internally. */ initDate: [ 2008, 0, 1 ], componentCls: Ext.baseCSSPrefix + 'timepicker', alignOnScroll: false, /** * @cfg * @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', requires: [ 'Ext.form.field.Date', 'Ext.picker.Time', 'Ext.view.BoundListKeyNav', 'Ext.Date' ], alternateClassName: [ 'Ext.form.TimeField', 'Ext.form.Time' ], /** * @cfg {String} [triggerCls='x-form-time-trigger'] * 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}. */ 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}. */ 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. */ invalidText: "{0} is not a valid time", // // /** * @cfg {String} [format=undefined] * 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. */ 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}. */ // // /** * @cfg {String} altFormats * Multiple date formats separated by "|" to try when parsing a user input value and it doesn't match the defined * format. */ 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", // // // The default format for the time field is 'g:i A', which is hardly informative /** * @cfg {String} formatText * The format text to be announced by screen readers when the field is focused. */ /** @ignore */ formatText: 'Expected time format HH:MM space AM or PM', // /** * @cfg {Number} [increment=15] * 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=false] * 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 * @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', queryMode: 'local', displayField: 'disp', valueField: 'date', initComponent: function() { var me = this, min = me.minValue, max = me.maxValue; if (min) { me.setMinValue(min); } if (max) { me.setMaxValue(max); } me.displayTpl = new Ext.XTemplate('' + '{[typeof values === "string" ? values : this.formatDate(values["' + me.displayField + '"])]}' + '' + me.delimiter + '' + '', { formatDate: me.formatDate.bind(me) }); // 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(); 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' ], requires: [ 'Ext.dom.Helper', 'Ext.util.ClickRepeater' ], alternateClassName: [ 'Ext.form.TriggerField', 'Ext.form.TwinTriggerField', 'Ext.form.Trigger' ], /** * @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; 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); me.rowIdx = dataSource.indexOf(me.record); } } 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. 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; }, /** * 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', /** * @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) { this.context = this.editingPlugin.context; this.callParent([ boundEl, value, doFocus ]); }, /** * @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.down(me.context.view.innerSelector); if (innerCell) { if (me.isForTree) { innerCell = innerCell.child(me.treeNodeSelector); } 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. 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)) { 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(), 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); } else { me.editingPlugin.onEditComplete(me, me.getValue(), me.startValue); } }, cacheElement: function() { if (!this.editing && !this.destroyed) { Ext.getDetachedBody().dom.appendChild(this.el.dom); } }, /** * @private * We should do nothing. * Hiding blurs, and blur will terminate the edit. * Must not allow superclass Editor to terminate the edit. */ onHide: function() { 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 don 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.down(me.context.view.innerSelector); if (innerCell) { if (me.isForTree) { innerCell = innerCell.child(me.treeNodeSelector); } 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.down(me.context.view.innerSelector), innerCellTextNode = innerCell.dom.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.dom.innerHTML = 'X'; } me.alignTo(boundEl, me.alignment, offsets); if (isEmpty) { innerCell.dom.firstChild.data = v; } }, getTreeNodeOffset: function(innerCell) { return innerCell.child(this.treeNodeSelector).getOffsetsTo(innerCell)[0]; } }); Ext.define('Ext.rtl.grid.CellEditor', { override: 'Ext.grid.CellEditor', getTreeNodeOffset: function(innerCell) { var offset = this.callParent(arguments); if (this.editingPlugin.grid.isOppositeRootDirection()) { offset = -(innerCell.getWidth() - offset - innerCell.child(this.treeNodeSelector).getWidth()); } return offset; } }); /** * 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(''); } 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', { /* Begin Definitions */ extend: 'Ext.layout.container.Container', alternateClassName: 'Ext.layout.FitLayout', alias: 'layout.fit', /* End Definitions */ /** * @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) | (// jshint ignore:line 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.getScrollbarSize(); if (scrollbars & 1) { // jshint ignore:line // if we need the hscrollbar, remove its height containerSize.height -= scrollbarSize.height; } if (scrollbars & 2) { // jshint ignore:line // 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) { // jshint ignore:line // 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) { // jshint ignore:line // 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' ], extraBaseCls: Ext.baseCSSPrefix + 'grid', extraBodyCls: Ext.baseCSSPrefix + 'grid-body', actionableModeCls: Ext.baseCSSPrefix + 'grid-actionable', noHeaderBordersCls: Ext.baseCSSPrefix + 'no-header-borders', defaultBindProperty: 'store', layout: 'fit', manageLayoutScroll: false, ariaRole: 'presentation', config: { /** * @cfg {Ext.data.Model} selection * The selected model. Typically used with {@link #bind binding}. */ selection: null, /** * @cfg {Ext.grid.CellContext/Ext.data.Model/Number} record * 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=`true`] * 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 }, publishes: [ 'selection' ], twoWayBindable: [ 'selection' ], /** * @cfg {Boolean} [autoLoad=false] * 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=false] * @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 & 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=false] * 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=true] * False to disable column sorting via clicking the header and via the Sorting menu items. */ sortableColumns: true, /** * @cfg {Boolean} [multiColumnSort=false] * 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=false] * 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 is 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=true] * False to disable column dragging within this grid. */ enableColumnMove: true, /** * @cfg {Boolean} [sealedColumns=false] * 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=true] * 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 */ /** * @cfg {Boolean} [rowLines=true] 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=true] * 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 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. 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 */ me.actionables = topGrid ? topGrid.actionables : []; // One shared array when there's a lockable at the top 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."); } var me = this, headerCtCfg = me.columns || me.colModel || [], store, view, i, len, bufferedRenderer, columns, viewScroller, headerCt, headerCtScroller; // 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'); me.enableLocking = me.enableLocking || me.hasLockedColumns(headerCtCfg); // 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) { me.self.mixin('lockable', Ext.grid.locking.Lockable); me.injectLockable(); headerCt = me.headerCt; } else // Not lockable - create the HeaderContainer { // It's a fully instantiated HeaderContainer if (headerCtCfg.isRootHeader) { me.headerCt = headerCt = headerCtCfg; headerCt.grid = me; headerCt.forceFit = !!me.forceFit; headerCt.$initParent = me; // 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 { if (Ext.isArray(headerCtCfg)) { headerCtCfg = { items: headerCtCfg }; } me.headerCt = headerCt = new Ext.grid.header.Container(Ext.apply(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.isDefined(me.enableColumnHide)) { headerCt.enableColumnHide = me.enableColumnHide; } } // Maintain backward compatibiliy by providing the initial leaf column set as a property. me.columns = columns = headerCt.getGridColumns(); 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.dockedItems = [].concat(me.dockedItems || []); me.dockedItems.unshift(headerCt); 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 }); // Decide upon hideHeaders configuration based on columns having content or not. // Scroll syncing will be set up with the view's scroller if headers are visible. me.syncHeaderVisibility(); } // 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.Table#beforeitemkeydown */ 'beforeitemkeydown', /** * @event itemkeydown * @inheritdoc Ext.view.Table#itemkeydown */ 'itemkeydown', /** * @event beforeitemkeyup * @inheritdoc Ext.view.Table#beforeitemkeyup */ 'beforeitemkeyup', /** * @event itemkeyup * @inheritdoc Ext.view.Table#itemkeyup */ 'itemkeyup', /** * @event beforeitemkeypress * @inheritdoc Ext.view.Table#beforeitemkeypress */ 'beforeitemkeypress', /** * @event itemkeypress * @inheritdoc Ext.view.Table#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' ]); } me.callParent(); if (me.enableLocking) { me.afterInjectLockable(); } else { delete headerCt.$initParent; } me.addStateEvents([ 'columnresize', 'columnmove', 'columnhide', 'columnshow', 'sortchange', '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 */ syncHeaderVisibility: function() { var me = this, headerCt = me.headerCt, columns = headerCt.items.items, len = columns.length, currentHideHeaderState = headerCt.height === 0, hideHeaders = !!len, column, colText, i, viewScroller; // If we have not been configured with hideHeaders, then set it if // there ARE columns and none of the columns has header text or child columns. // For example, a simple tree with an automatically inserted TreeColumn. if (me.hideHeaders != null) { hideHeaders = me.hideHeaders; } else { // Loop until we find a column with content. for (i = 0; hideHeaders && i < len; i++) { column = columns[i]; colText = column.text; // If any column was configured with text *that is not  * or child columns, then // we must show headers. if ((colText && colText !== '\xa0') || column.columns || (column.isGroupHeader && column.items.items.length)) { hideHeaders = false; } } } if (!headerCt.rendered || hideHeaders !== currentHideHeaderState) { headerCt.setHeight(hideHeaders ? 0 : null); headerCt.hiddenHeaders = hideHeaders; me.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'); } } } }, 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' }; 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(); }, afterLayout: function(layout) { var lockable = this.mixins.lockable; if (lockable) { lockable.syncLockableLayout.call(this, layout); } this.callParent([ layout ]); }, 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(); }, /** * @inheritdoc Ext.grid.header.Container#getGridColumns */ getColumns: function() { return this.getColumnManager().getColumns(); }, /** * @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. // Regaular 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 this.getView().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(arguments); if (columns) { // Column state restoration needs to examine store state me.headerCt.applyColumnsState(columns, storeState); } // 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 = columns.length, columnState, i, result; // Create a useable 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(); freeRowContexts.push(context); delete liveRowContexts[id]; } } }, createManagedWidget: function(ownerId, widgetConfig, record) { return this.liveRowContexts[record.internalId].getWidget(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. if (!('scrollable' in viewConfig || 'scroll' in viewConfig || 'autoScroll' in viewConfig) && scrollable != null) { viewConfig.scrollable = scrollable; } Ext.create(viewConfig); // 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 = '
    ' + me.view.emptyText + '
    '; } // 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 }); me.headerCt.view = me.view; // 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('
    ' + emptyText + '
    '); 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 */ 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 {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)); }, /** * Returns the grid's selection. See `{@link Ext.selection.Model#getSelection}`. * @inheritdoc Ext.selection.Model#getSelection */ getSelection: function() { return this.getSelectionModel().getSelection(); }, 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; } }, 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 it's 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); } // 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 #reconfigure} with the store param. To set the store AND change columns, * use the {@link #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 */ reconfigure: function(store, columns, /* private */ allowUnbind) { var me = this, oldStore = me.store, headerCt = me.headerCt, lockable = me.lockable, oldColumns = headerCt ? headerCt.items.getRange() : me.columns, view = me.getView(), block, refreshCounter, storeChanged, columnsChanged, 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 (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; } 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); } }, doDestroy: function() { var me = this, task = me.scrollTask; 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.plugins, me.focusEnterLeaveListeners, me.freeRowContexts, Ext.Object.getValues(me.liveRowContexts)); me.callParent(); // Have to unbind the store this late because plugins and other things // may still need it until the very end. me.unbindStore(); }, 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); } var me = this, view = me.getView(), domNode = view.getNode(record), callback, scope, animate, highlight, select, doFocus, scrollable, column, cell; 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) { scrollable = view.getScrollable(); if (column) { cell = Ext.fly(domNode).selectNode(column.getCellSelector()); } if (scrollable) { scrollable.scrollIntoView(cell || domNode, !!column, animate, highlight); } if (!record.isEntity) { record = view.getRecord(domNode); } if (select) { view.getSelectionModel().select(record); } if (doFocus) { view.getNavigationModel().setPosition(record, 0); } Ext.callback(callback, scope || me, [ true, record, domNode ]); } // 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(); }, /** * Toggles ARIA actionable mode on/off * @param {Boolean} enabled * @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() { this.scrollFlags = this._scrollFlags['false']['false']; return { overflowX: 'hidden', overflowY: 'hidden' }; }, getOverflowEl: function() { return null; } } }); /** * @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.getScrollbarSize().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 ]); } }); Ext.define('Ext.rtl.grid.ColumnLayout', { override: 'Ext.grid.ColumnLayout', determineScrollbarWidth: function(ownerContext) { var me = this, view = me.owner.grid.view; me.callParent([ ownerContext ]); if (view.getInherited().rtl) { // Chrome has an RTL bug where overflow only caused by the imposition of the // vertical scrollbar does NOT cause extra left/right scrolling. If that bug is // present, this extra space is not needed in RTL. // // Safari keeps the scrollbar on the right in RTL mode so the extra width comes // from padding added to the header container. // // https://code.google.com/p/chromium/issues/detail?id=179332 // // TODO: Remove the Ext.supports.rtlVertScrollbarOnRight test and the test for // it below when all supported Chrome versions are fixed. // // Chrome also has the xOriginBug: // // http://code.google.com/p/chromium/issues/detail?id=174656 // // This means that the table element has to be positioned right:-15px in RTL // mode. This triggers the right padding to be added in calculateParallel below // which extends the contentWidth. We compensate for this here by reducing the // width by the same amount. // // This extra space is also not needed if the scrollbar is on the right. In // this case, the extra space comes from padding added to the ColumnLayout in // the calculateParallel implementation below. // // So when these conditions are present and the grid is in RTL mode, the // scrollbarAdjustment value for this layout is zero. if (view.bufferedRenderer && Ext.supports.xOriginBug) { me.scrollbarWidth = -Math.abs(me.scrollbarWidth); } else if (Ext.supports.rtlVertScrollbarOverflowBug || Ext.supports.rtlVertScrollbarOnRight) { me.scrollbarWidth = 0; } } }, calculateParallel: function(ownerContext, names, plan) { var me = this, owner = me.owner; if (owner.isRootHeader) { // https://sencha.jira.com/browse/EXTJSIV-11245 // Safari keeps scrollbar on the right even in RTL mode, so any element // which must stay in horizontal sync (like the HeaderContainer) needs the first item to have some "before" margin. // The layout system caches the margin because it is assumed to be static, so we have to clear this cache. if ((Ext.supports.rtlVertScrollbarOnRight && owner.ownerCt.view.getInherited().rtl) || (owner.grid.view.bufferedRenderer && Ext.supports.xOriginBug)) { me.padding.right = me.scrollbarWidth; } } return me.callParent(arguments); } }); /** * @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: 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; } }, onCellMouseDown: function(view, cell, cellIndex, record, row, recordIndex, mousedownEvent) { var targetComponent = Ext.Component.fromElement(mousedownEvent.target, cell), targetEl = mousedownEvent.getTarget(null, null, true), onActionable = targetEl.isFocusable() && !targetEl.is(view.getCellSelector()), 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 actino 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 (!onActionable) { onActionable = (ac = Ext.ComponentManager.getActiveComponent()) && ac !== view && ac.owns(mousedownEvent); } if (onActionable) { // 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 (onActionable) { // 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); } view.setActionableMode(true, mousedownEvent.position); } else { cell.focus(); } if (mousedownEvent.button === 2) { this.fireNavigateEvent(mousedownEvent); } } else { mousedownEvent.preventDefault(true); } // 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(); } } }, onCellClick: function(view, cell, cellIndex, record, row, recordIndex, clickEvent) { var me = this, targetComponent = Ext.Component.fromElement(clickEvent.target, 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 // if (Ext.isIE10m && !me.record) { return; } // 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.fromElement(e.delegatedTarget, null, 'tableview'), cell = e.target, isCell = Ext.fly(cell).is(view.cellSelector), record, column, newPosition; if (view) { // 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; // 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 mousedownEvent.preventDefault(); // Stamp the closest cell into the event as if it were a cellmousedown me.getClosestCell(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. view.getScrollable().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.getClosestCell(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); } this.fireNavigateEvent(clickEvent); } }, getClosestCell: function(event) { var position = event.position, targetCell = position.cellElement, x, columns, len, i, column, b; if (!targetCell) { x = event.getX(); columns = position.view.getVisibleColumnManager().getColumns(); len = columns.length; for (i = 0; i < len; i++) { column = columns[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; } } } }, 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.getFocusTask().cancel(); // 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 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. scrollIntoView scrolls exatly correctly. scroller = view.getScrollable(); if (scroller) { scroller.scrollIntoView(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. 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(); // 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.query('[tabIndex="-1"]').length) { 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; if (position && position.record) { while (result) { // Do not allow SHIFT+(left|right) to wrap. result = position.view.walkCells(result, dir, keyEvent.shiftKey && (dir === 'right' || dir === 'left') ? me.vetoRowChange : null, me); // If the new position is fousable, we're done. if (result && 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 }); } }); Ext.define('Ext.rtl.grid.NavigationModel', { override: 'Ext.grid.NavigationModel', initKeyNav: function(view) { var me = this, proto = me.self.prototype; if (view.getInherited().rtl) { me.onKeyLeft = proto.onKeyRight; me.onKeyRight = proto.onKeyLeft; } me.callParent([ view ]); } }); /** * 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, partnerVisible = partner && partner.grid.isVisible() && !partner.grid.collapsed, context = ownerContext.context; // 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); 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()) { (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, owner = me.owner, columnsChanged = headerContext.getProp('columnsChanged'), state = ownerContext.state, columnFlusher, otherSynchronizer, synchronizer, rowHeightFlusher, bodyDom = owner.body.dom, bodyHeight, ctSize, overflowY; // 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'))) { 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 the grid is shrink wrapping, we can't be overflowing overflowY = false; if (!ownerCtContext.heightModel.shrinkWrap) { // 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; overflowY = bodyHeight > ctSize.height; } ownerContext.setProp('viewOverflowY', overflowY); } // 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. if (me.done && Ext.getScrollbarSize().height) { // 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) { ownerContext.setProp('overflowX', !!ownerContext.headerContext.state.boxPlan.tooNarrow); } } }, 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.getScrollbarSize().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()); me.callParent([ ownerContext ]); if (nodeContainer) { nodeContainer.setWidth(ownerContext.headerContext.props.contentWidth); } // Inform any buffered renderer about completion of the layout of its view if (me.owner.bufferedRenderer) { me.owner.bufferedRenderer.afterTableLayout(ownerContext); } if (ownerGrid) { ownerGrid.syncRowHeightOnNextLayout = false; } }, 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]; // Partnet 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, name; me.rowHeight = me.rowEl.offsetHeight; for (name in els) { els[name].height = els[name].el.offsetHeight; } }, 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[]} els An array of DOM elements from which to fill this NodeCache. * @return {Ext.view.NodeCache} this */ fill: function(newElements, startIndex, fixedNodes) { fixedNodes = fixedNodes || 0; var me = this, elements = me.elements = {}, i, len = newElements.length - fixedNodes; if (!startIndex) { startIndex = 0; } for (i = 0; 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 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} el 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, inKeys, key, elements = me.elements, el, deleteCount, keyIndex = 0, 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. 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, vm = view.lookupViewModel(), 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; // The content height MUST be measurable by the caller (the buffered renderer), so data must be flushed to it immediately. if (vm) { vm.notify(); } 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 }, "private": { doScrollTo: function(x, y, animate) { var lockingScroller; if (y != null) { lockingScroller = this.getLockingScroller(); if (lockingScroller) { lockingScroller.doScrollTo(null, y, animate); y = null; } } this.callParent([ x, y, animate ]); } } }); /** * 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 */ /** * @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=[];', '%}', '', // 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: [ '', // Do NOT emit a tag in case the nextTpl has to emit a column sizer element. // Browser will create a tbody tag when it encounters the first '{%', 'this.nextTpl.applyOut(values, out, parent)', '%}', '', { priority: 9999 } ], rowTpl: [ '{%', 'var dataRowCls = values.recordIndex === -1 ? "" : " ' + Ext.baseCSSPrefix + 'grid-row";', '%}', '', '' + '{%', 'parent.view.renderCell(values, parent.record, parent.recordIndex, parent.rowIndex, xindex - 1, out, parent)', '%}', '', '', { priority: 0 } ], cellTpl: [ '{tdStyle}"', '', ' role="presentation"', '', ' role="{cellRole}" tabindex="-1"', '', ' data-columnid="{[values.column.getItemId()]}">', '
    {style}" ', '{cellInnerAttr:attributes}>{value}
    ', '', { priority: 0 } ], /** * @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: [ Ext.baseCSSPrefix + 'grid-cell ' + Ext.baseCSSPrefix + 'grid-td' ] }, // for styles shared between cell and rowwrap /** * @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 table cell is clicked. * @param {Ext.view.Table} this * @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 rowdblclick * Fired when table cell is double clicked. * @param {Ext.view.Table} this * @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 rowcontextmenu * Fired when table cell is right clicked. * @param {Ext.view.Table} this * @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 rowmousedown * Fired when the mousedown event is captured on the cell. * @param {Ext.view.Table} this * @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 rowmouseup * Fired when the mouseup event is captured on the cell. * @param {Ext.view.Table} this * @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 rowkeydown * Fired when the keydown event is captured on the cell. * @param {Ext.view.Table} this * @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 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). * @cancelable */ 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.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, doFirstLastClasses = (me.firstCls || me.lastCls) && (toIdx === 0 || toIdx === colCount || fromIdx === 0 || fromIdx === lastIndex), i, j, rows, len, tr, cells, colGroups; // 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 elements in all 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 * @private */ getCell: function(record, column) { var row = this.getRow(record); if (row) { if (typeof column === 'number') { column = this.getHeaderAtIndex(column); } return Ext.fly(row).down(column.getCellSelector()); } }, /** * 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, i, features, feature, len; // Row container element emitted by tpl me.tpl = this.lookupTpl('tpl'); // The rowTpl emits a
    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(); len = features ? features.length : 0; for (i = 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 = 0, len; if (features) { result = []; len = features.length; for (; 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(); } }, 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(); } 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 rendere 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.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); 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 return Ext.fly(div).down(me.getNodeContainerSelector(), true).firstChild; }, /** * @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, result, range = document.createRange ? document.createRange() : null; 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 Ext.fly(div).saveTabbableState({ skipSelf: true, includeHidden: true }); div = Ext.fly(div).down(me.getNodeContainerSelector(), true); 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 = 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.down(bodySelector, true)); } if (!me.hasLoadingHeight) { // Suspend layouts in case the superclass requests a layout. We might too, so they // must be coalesced. Ext.suspendLayouts(); me.callParent(arguments); // 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. 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, widgetCount, i, widgets, widget, recordId, result = me.callParent(arguments); // 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]; // Check that the upward link injected into the widget leads // to this View (locked views) if (me.isAncestor(widget)) { 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(''); for (i = 0; i < len; i++) { column = columns[i]; width = column.lastBox ? column.lastBox.width : Ext.grid.header.Container.prototype.defaultWidth; out.push(''); } out.push(''); }, */ renderColumnSizer: function(values, out) { var columns = values.columns || this.getGridColumns(), len = columns.length, i, column, width; out.push(''); for (i = 0; i < len; i++) { column = columns[i]; width = column.cellWidth ? column.cellWidth : Ext.grid.header.Container.prototype.defaultWidth; out.push(''); } out.push(''); }, /** * @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 {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 {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, 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 (column.renderer && column.renderer.call) { fullIndex = me.ownerCt.columnManager.getHeaderIndex(column); value = column.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 fly; if ((!nodeInfo && nodeInfo !== 0) || !this.rendered) { return null; } // An event if (nodeInfo.target) { nodeInfo = nodeInfo.target; } // An id if (Ext.isString(nodeInfo)) { return Ext.fly(nodeInfo).down(this.rowSelector, true); } // Row index if (Ext.isNumber(nodeInfo)) { fly = this.all.item(nodeInfo); return fly && fly.down(this.rowSelector, true); } // Record if (nodeInfo.isModel) { return this.getRowByRecord(nodeInfo); } fly = Ext.fly(nodeInfo); // Passed an item, go down and get the row if (fly.is(this.itemSelector)) { return this.getRowFromItem(fly); } // Passed a child element of a row return fly.findParent(this.rowSelector, this.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 Ext.fly(result).down(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 `` elements as well as `` elements // `` element is inserted if the first row does not have the regular cell patten (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); if (cell) { cell.addCls(this.selectedCellCls); cell.dom.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) { if (position) { var row = this.getRow(position.row), header = this.ownerCt.getColumnManager().getHeaderAtIndex(position.column); if (header && row) { return Ext.fly(row).down(this.getCellSelector(header), returnDom); } } 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) { if (position) { var view = position.view || this, row = view.getRow(position.record || position.row), header; header = view.getColumnByPosition(position); if (header && row) { return Ext.fly(row).down(view.getCellSelector(header), returnDom); } } 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 *wthin* 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, actinoable 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.NagivationModel#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') { 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. 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) { // 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; // setActionableMode focuses the *first* tabbable element in the cell. // If focus if entering into another element (eg multiple action icons in an ActionColumn), then redirect it. Ext.fly(focusTarget).focus(); } } // 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, // 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)); // If the blur was caused by a refresh, we expect things to be refocused. if (!me.refreshing) { // Ignore this event if we do not actually contain focus. // CellEditors are rendered into the view's encapculating element, // So focusleave will fire when they are programatically 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, supressFocus) { var me = this; if (highlight) { me.addItemCls(rowIdx, me.focusedItemCls); if (!supressFocus) { 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. /** * @override Ext.view.View * 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} pos 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, cell, focusTask = me.getFocusTask(); 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. 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.isNormalView && me.lockingPartner.grid.isVisible() && !me.lockingPartner.grid.collapsed) ? me.lockingPartner : 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 && fromComponent.getView() === me) { if (!focusPosition) { focusPosition = newPosition; } focusPosition.setColumn(fromComponent); } // Tabbing in from the neighbouring TableView (eg, locking). // Go to column zero, same record else if (fromComponent.isTableView) { 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()).y)) { focusPosition.setRow(store.getAt(Math.min(focusPosition.rowIdx, store.getCount() - 1))); } } else // All else failes, find the first focusable cell. { focusPosition = newPosition; // Find the first focusable cell. targetCell = me.ownerGrid.view.el.down(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.scrollIntoView(el, hscroll, animate); } }, 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) { operation = operation || Ext.data.Model.EDIT; var me = this, recordIndex = me.store.indexOf(record), rowTpl = me.rowTpl, markDirty = me.markDirty, dirtyCls = me.dirtyCls, clearDirty = operation !== Ext.data.Model.EDIT, 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, defaultRenderer, scope, elData, emptyValue; 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 = Ext.fly(oldItemDom).down(column.getCellSelector(), true); // 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; // jshint ignore:line 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. 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 , we must use the full render pathway to create a whole new TableView item if (me.getRowClass || !me.getRowFromItem(oldItemDom) || (updateTypeFlags & 1) || (// jshint ignore:line 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; if (attName !== 'id') { oldItemDom.setAttribute(attName, newAttrs[attrIndex].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, Ext.fly(newItemDom).down(me.rowSelector, true), columnsToUpdate); } // 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. 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 = Ext.fly(oldItemDom).down(column.getCellSelector(), true); 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   if (column.producesHTML || emptyValue) { cellFly.down(me.innerSelector, true).innerHTML = value; } else { cellFly.down(me.innerSelector, true).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 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; for (var 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) { var me = this, newAttrs, attLen, attName, attrIndex, colCount = columnsToUpdate.length, colIndex, column, oldCell, newCell, cellSelector = me.getCellSelector(), elData; // 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; if (attName !== 'id') { oldRow.setAttribute(attName, newAttrs[attrIndex].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. oldRow = Ext.get(oldRow); newRow = Ext.get(newRow); for (colIndex = 0; colIndex < colCount; colIndex++) { column = columnsToUpdate[colIndex]; // Pluck out cells using the column's unique cell selector. // Becuse in a wrapped row, there may be several TD elements. cellSelector = me.getCellSelector(column); oldCell = oldRow.selectNode(cellSelector); newCell = newRow.selectNode(cellSelector); // Copy new cell attributes across. newAttrs = newCell.attributes; attLen = newAttrs.length; for (attrIndex = 0; attrIndex < attLen; attrIndex++) { attName = newAttrs[attrIndex].name; if (attName !== 'id') { oldCell.setAttribute(attName, newAttrs[attrIndex].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. oldCell = Ext.fly(oldCell).selectNode(me.innerSelector); newCell = Ext.fly(newCell).selectNode(me.innerSelector); Ext.fly(oldCell).syncContent(newCell); } }, /** * @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. */ refresh: function() { var me = this, scroller; 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); } } }, 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.fromElement(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 = Ext.fly(item).down(me.rowSelector, true); // 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; } } } // 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)); } 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); 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; } } } } 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 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.el.query(header.getCellInnerSelector()), originalWidth = header.getWidth(), i = 0, ln = cells.length, columnSizer = me.body.select(me.getColumnSizerSelector(header)), max = Math.max, widthAdjust = 0, 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 columnSizer.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 < 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 columnSizer.setWidth(originalWidth); return maxWidth; }, 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} position 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 5.5.0 * @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, node, limit = (distance < 0) ? 0 : (store.isBufferedStore ? store.getTotalCount() : store.getCount()) - 1, increment = limit ? 1 : -1, testIndex = store.indexOf(startRec), 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, count = (me.dataSource.isBufferedStore ? me.dataSource.getTotalCount() : me.dataSource.getCount()), result = me.indexOf(me.all.first()) - 1; 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.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. 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.Element.getActiveElement(true), focusCell = focusPosition && focusPosition.view === me && focusPosition.getCell(), refocusRow, refocusCol; // 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. if (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 !== activeElement) { me.suspendActionableMode(); } else // Clear position, otherwise the setPosition onthe 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 // closeure not called. activeElement.resumeFocusEvents(); // 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 inthe store), or the row index. return function() { // May have changed due to reconfigure store = me.dataSource; // If we still have data, attempt to refocus in the same mode. 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. refocusRow = Math.min(focusPosition.rowIdx, me.all.getCount() - 1); refocusCol = Math.min(focusPosition.colIdx, me.getVisibleColumnManager().getColumns().length - 1); focusPosition = new Ext.grid.CellContext(me).setPosition(store.contains(focusPosition.record) ? focusPosition.record : refocusRow, refocusCol); if (actionableMode) { me.resumeActionableMode(focusPosition); } else { // Pass "preventNavigation" as true so that that does not cause selection. navModel.setPosition(focusPosition, null, null, null, true); } } 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: { /* * 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 * @return {Boolean} Returns `false` if the mode did not change. * @private */ setActionableMode: function(enabled, position) { var me = this, navModel = me.getNavigationModel(), activeEl, actionables = me.grid.actionables, len = actionables.length, i, record, column, isActionable = false, lockingPartner, cell; // No mode change. // ownerGrid's call will NOT fire mode change event upon false return. if (me.actionableMode === enabled) { // If we're not actinoable 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(); // 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(); } // Ext.grid.NavigationModel#onFocusMove will NOT react and navigate because the actionableMode // flag is still set at this point. 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 // programatically 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(), 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); // Deactivate remaining tabbables in the row we were last actionable upon. if (me.actionPosition) { prevRow = Ext.get(me.all.item(me.actionPosition.rowIdx, true)); if (prevRow && focusRow !== prevRow) { 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 = position.getCell(); 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); } } // 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 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 = focusRow; me.actionableMode = me.ownerGrid.actionableMode = true; // Clear current position on entry into actionable mode navModel.setPosition(); navModel.actionPosition = me.actionPosition = position; 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, cellIdx; if (lockingPartner && lockingPartner.grid.isVisible()) { rowIdx = me.all.indexOf(prevRow); // TAB out of right side of view if (forward) { cellIdx = 0; // If normal side go to next row in locked side if (me.isNormalView) { rowIdx++; } } else // TAB out of left side of view { cellIdx = lockingPartner.getVisibleColumnManager().getColumns().length - 1; // If locked side go to previous row in normal side 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. for (i = (forward ? 0 : columnCount - 1); (forward ? i < columnCount : i > -1) && !focusTarget; i = i + (forward ? 1 : -1)) { column = columns[i]; position.setColumn(column); focusCell = Ext.fly(focusRow).down(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 = position.getCell(); if (focusCell) { focusRow = position.getNode(true); // TODO Nige? // If the focusCell is available (when using features with colspan the cell won't be there) and // If there are restored tabbable elements rendered in the cell, or an Actionable is activated on this cell... //if (focusCell && (focusCell.restoreTabbableState(/* skipSelf */ true).length || isActionable)) { // tabbableChildren = focusCell.findTabbableElements(); // 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 = Ext.get(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 platformm 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.dom) { 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 maximim 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; } } }); Ext.define('Ext.rtl.view.Table', { override: 'Ext.view.Table', rtlCellTpl: [ '{tdStyle}" tabindex="-1" {ariaCellAttr} data-columnid="{[values.column.getItemId()]}">', '
    {style}" {ariaCellInnerAttr}>{value}
    ', '', { priority: 0 } ], beforeRender: function() { var me = this; me.callParent(); if (me.getInherited().rtl) { me.addCellTpl(me.lookupTpl('rtlCellTpl')); } }, getCellPaddingAfter: function(cell) { return Ext.fly(cell).getPadding(this.getInherited().rtl ? 'l' : 'r'); } }); /** * Grids are an excellent way of showing large amounts of tabular data on the client side. * Essentially a supercharged ``, 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('{1}', 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', requires: [ 'Ext.view.Table' ], alias: [ 'widget.gridpanel', 'widget.grid' ], alternateClassName: [ 'Ext.list.ListView', 'Ext.ListView', 'Ext.grid.GridPanel' ], viewType: 'tableview', ariaRole: 'grid', lockable: false, /** * @cfg {Boolean} rowLines False to remove row line styling */ rowLines: true }); // Columns config is required in Grid /** * @cfg {Ext.grid.column.Column[]/Object} 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() { 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]; // 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(); widget.hidden = true; } }, getWidget: function(ownerId, widgetCfg) { var me = this, widgets = me.widgets || (me.widgets = {}), result; // Only spin up an attached ViewModel when we instantiate our first managed Widget // which uses binding. if (widgetCfg.bind && !me.viewModel) { me.viewModel = Ext.Factory.viewModel({ parent: me.ownerGrid.lookupViewModel(), data: { record: me.record, recordIndex: me.recordIndex } }, me.ownerGrid.rowViewModel); } if (!(result = widgets[ownerId])) { result = widgets[ownerId] = Ext.widget(Ext.apply({ viewModel: me.viewModel, _rowContext: me }, widgetCfg)); // 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 { result.hidden = false; } return result; }, getWidgets: function() { var widgets = this.widgets, id, result = []; for (id in widgets) { result.push(widgets[id]); } return result; }, 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(); } }); /** * @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. */ // saveBtnText: 'Update', // // cancelBtnText: 'Cancel', // // errorsText: 'Errors', // // 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', _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.layout = { type: 'hbox', align: 'middle' }; 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({ 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({ // not user scrollable, but needs a Scroller instance for syncing with view scrollable: { x: false, y: false }, flex: 1, id: grid.id + '-normal-editor-cells', layout: { type: 'hbox', align: 'middle' } }) ]; // 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) { 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')); width = column.getWidth() - field._marginWidth; 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(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 if (context && (row = view.getRow(context.record))) { context.row = row; 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) { // If a column header added, process its leaves if (column.isGroupHeader) { column = column.getGridColumns(); } //this.preventReposition = true; this.addFieldsForColumn(column); this.insertColumnEditor(column); this.preventReposition = false; }, insertColumnEditor: function(column) { 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; } fieldContainer = column.isLocked() ? me.lockedColumnContainer : me.normalColumnContainer; // Insert the column's field into the editor panel. fieldContainer.insert(column.getIndex(), 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 (IE/Edge only). if ((Ext.isIE || Ext.isEdge) && 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().scrollIntoView(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().scrollIntoView(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 }); } 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()); 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(), scrollingView = me.scrollingView, // 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; } 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.getScrollbarSize().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.handleAfterShow(); } 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 '
      ' + errors.join('') + '
    '; }, createErrorListItem: function(e, name) { e = name ? name + ': ' + e : e; return '
  • ' + e + '
  • '; }, doDestroy: function() { var me = this; if (me.wrapAnim) { Ext.fx.Manager.removeAnim(me.wrapAnim); me.wrapAnim = null; } me.floatingButtons = me.tooltip = Ext.destroy(me.floatingButtons, me.tooltip); 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: [ '', '' ].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; } 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; 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) { indicatorY = Ext.fly(node).getY() - view.el.getY() - 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(arguments); 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, // 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, me.overRecord, me.currentPosition); dropHandled = true; me.fireViewEvent('drop', targetNode, data, me.overRecord, me.currentPosition); }, cancelDrop: function() { me.invalidateDrop(); dropHandled = true; } }, performOperation = false; if (me.valid) { performOperation = me.fireViewEvent('beforedrop', targetNode, data, me.overRecord, me.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: '', indicatorCls: Ext.baseCSSPrefix + 'grid-drop-indicator', handleNodeDrop: function(data, record, position) { var view = this.view, store = view.getStore(), crossView = view !== data.view, 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 (crossView) { /* * Remove from the source store only if we are moving to a different store. */ 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 unless dropping in the same view. // In which case we do not disturb the selection. if (crossView) { 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', requires: [ 'Ext.dd.DragTracker', 'Ext.util.Region' ], alias: 'plugin.gridheaderresizer', 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, bug 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, 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.getScrollbarSize().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 mousedownb 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); } }); Ext.define('Ext.rtl.grid.plugin.HeaderResizer', { override: 'Ext.grid.plugin.HeaderResizer', onBeforeStart: function(e) { var me = this; if (this.headerCt.isOppositeRootDirection()) { // cache the activeHd because it will be cleared. me.dragHd = me.activeHd; if (!!me.dragHd && !me.headerCt.dragging) { // 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.tracker.getXY()[0]; this.tracker.constrainTo = this.getConstrainRegion(); return true; } else { me.headerCt.dragging = false; return false; } } else { return this.callParent(arguments); } }, adjustColumnWidth: function(offsetX) { if (this.headerCt.isOppositeRootDirection()) { offsetX = -offsetX; } this.callParent([ offsetX ]); }, adjustConstrainRegion: function(region, t, r, b, l) { return this.headerCt.isOppositeRootDirection() ? region.adjust(t, -l, b, -r) : this.callParent(arguments); }, calculateDragX: function(gridSection) { var gridX = gridSection.getX(), mouseX = this.tracker.getXY('point')[0]; if (this.headerCt.isOppositeRootDirection()) { return mouseX - gridX + this.xDelta; } else { return this.callParent(arguments); } }, getMovingMarker: function(markerOwner) { if (this.headerCt.isOppositeRootDirection()) { return markerOwner.getLhsMarker(); } else { return markerOwner.getRhsMarker(); } }, setMarkerX: function(marker, x) { var headerCt = this.headerCt; if (headerCt.getInherited().rtl && !headerCt.isOppositeRootDirection()) { marker.rtlSetLocalX(x); } else { this.callParent(arguments); } } }); /** * @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)) { 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: " " }); 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: " " }); } 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, to, fromPanel, 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.sealed && !to.ownerCt.sealed)) { 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; } 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(':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', requires: [ 'Ext.grid.header.DragZone', 'Ext.grid.header.DropZone' ], alias: 'plugin.gridheaderreorderer', 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', 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' ], mixins: [ 'Ext.util.FocusableContainer' ], border: true, alias: 'widget.headercontainer', 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' * } * } * */ // sortAscText: 'Sort Ascending', // // sortDescText: 'Sort Descending', // // sortClearText: 'Clear Sort', // // 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) enableFocusableContainer: 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.enableFocusableContainer = 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); }, 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.resizable) { header.onTitleElDblClick(e, targetEl.dom); } } } } }, blockNextEvent: function() { this.blockEvents = true; Ext.asap(this.unblockEvents, this); }, unblockEvents: function() { this.blockEvents = false; }, onHeaderCtMouseDown: function(e, target) { var targetCmp = Ext.Component.fromElement(target), cols, i, len, scrollable, col; if (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() { var me = this; return me.isRootHeader ? me : me.up('[isRootHeader]'); }, doDestroy: function() { var me = this; if (me.menu) { me.menu.un('hide', me.onMenuHide, me); } me.menuTask.cancel(); Ext.destroy(me.visibleColumnManager, me.columnManager, me.menu); me.callParent(); }, applyColumnsState: function(columnsState, storeState) { if (!columnsState) { return; } var me = this, items = me.items.items, count = items.length, i = 0, length, c, col, columnState, index, moved = false, newOrder = [], newCols = []; 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); me.purgeCache(); } }, 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; var stateId = c.getStateId(); 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::deatchOnRemove gets processed after // onRemove. if (c.rendered) { me.detachComponent(c); } // 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} [event] 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, isTouch = clickEvent && clickEvent.pointerType === 'touch'; // 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 grid columns 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; } 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, availFlex = me.el.dom.clientWidth - (view.el.dom.scrollHeight > view.el.dom.clientHeight ? Ext.getScrollbarSize().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; }, privates: { beginChildHide: function() { ++this.childHideCount; }, endChildHide: function() { --this.childHideCount; }, getFocusables: function() { return this.isRootHeader ? this.getVisibleGridColumns() : this.items.items; }, createFocusableContainerKeyNav: function(el) { var me = this; return new Ext.util.KeyNav(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) { me.initFocusableContainer(); } } } }); /** * 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', { 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', enableFocusableContainer: 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' ], /** * @private * @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`. */ headerWrap: false, renderTpl: [ '', '{%this.renderContainer(out,values)%}' ], /** * @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. * **Note**: to have a clickable header with no text displayed you can use the default of ` ` aka ` `. */ 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 ` ` aka ` `. */ 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 `<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 '' + value + ''; * } * }, { * text: 'Email', * dataIndex: 'email', * flex: 1, * editor: { * xtype: 'textfield', * allowBlank: false * } * }], * plugins: { * ptype: '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 '' + value + ''; * } * * @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 '' + value + ''; * } * * @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 '' + value + ''; * } * * @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. */ 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, /** * @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 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.align); // Set up the renderer types: 'renderer', 'editRenderer', and 'summaryRenderer' me.setupRenderer(); me.setupRenderer('edit'); me.setupRenderer('summary'); // Initialize as a HeaderContainer me.callParent(arguments); }, beforeLayout: function() { var items = this.items, len, i, hasFlexedChildren; if (!Ext.isArray(items)) { items = items.items; } len = items.length; if (len) { for (i = 0; !hasFlexedChildren && i < len; i++) { hasFlexedChildren = items[i].flex; } // If all children have been given a width, we must fall back to shrinkwrapping them. if (!hasFlexedChildren) { this.flex = null; } } this.callParent(); }, onAdded: function(container, pos, instanced) { var me = this; me.callParent([ container, pos, instanced ]); 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, 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 { // Have the sorter spec decoded by the collection that will host it. ret = me.getRootHeaderCt().up('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._alignMap[align] || 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) { // type can be null or 'edit', or 'summary' type = type || 'column'; var me = this, format = me[me.formatterNames[type]], renderer = me[me.rendererNames[type]], isColumnRenderer = type === 'column', parser, dynamic; 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 || renderer.length > 1; } } // 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(); me[me.formatterNames[type]] = null; // processed - trees come back here to add its renderer // Set up the correct property: 'renderer', 'editRenderer', or 'summaryRenderer' me[me.rendererNames[type]] = me.bindFormatter(format); } }, getView: function() { var rootHeaderCt = this.getRootHeaderCt(); if (rootHeaderCt) { return rootHeaderCt.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. 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.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(); me.callParent(arguments); if (rootHeaderCt && (oldWidth != null || me.flex) && width !== oldWidth) { rootHeaderCt.onHeaderResize(me, width); } }, 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])'); me.getRootHeaderCt().autoSizeColumn(leafColumns[leafColumns.length - 1]); 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; // 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); // If we are a isolated layout due to being one half of a locking asembly // where one is collapsed, the top level Ext.grid.locking.Lockable#afterLayout // will NOT have been called, so we have to explicitly run it here. if (grid.ownerGrid.lockable && grid.isLayoutRoot()) { grid.ownerGrid.syncLockableLayout(); } return; } me.getRootHeaderCt().autoSizeColumn(me); }, isEmptyText: function(text) { return text == null || text === ' ' || text === ' ' || 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) 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, sorter = me.getSorter(); // 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) { if (direction) { sorter.setDirection(direction); } store.sort(sorter, grid.multiColumnSort ? 'multi' : 'replace'); } else { store.sort(me.getSortParam(), direction, grid.multiColumnSort ? 'multi' : 'replace'); } Ext.resumeLayouts(true); // If we are a isolated layout due to being one half of a locking asembly // where one is collapsed, the top level Ext.grid.locking.Lockable#afterLayout // will NOT have been called, so we have to explicitly run it here. if (grid.ownerGrid.lockable && grid.isLayoutRoot()) { grid.ownerGrid.syncLockableLayout(); } }, /** * 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() { return this.locked || !!this.up('[isColumn][locked]', '[isRootHeader]'); }, 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); // If we are a isolated layout due to being one half of a locking asembly // where one is collapsed, the top level Ext.grid.locking.Lockable#afterLayout // will NOT have been called, so we have to explicitly run it here. if (rootHeaderCt.grid.ownerGrid.lockable && rootHeaderCt.grid.isLayoutRoot()) { rootHeaderCt.grid.ownerGrid.syncLockableLayout(); } 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); // If we are a isolated layout due to being one half of a locking asembly // where one is collapsed, the top level Ext.grid.locking.Lockable#afterLayout // will NOT have been called, so we have to explicitly run it here. if (rootHeaderCt.grid.ownerGrid.lockable && rootHeaderCt.grid.isLayoutRoot()) { rootHeaderCt.grid.ownerGrid.syncLockableLayout(); } 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) { // 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) { var len = changedFieldNames.length, i, field; 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 = this.getView(); // We must explicitly access the view's cell selector as well as this column's own ID class because // elements are given this column's ID class. // If we are still atached to a view. If not, the identifying class will do. return (view ? view.getCellSelector() : '') + '.' + this.getCellId(); }, 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('' + this.dirtyText + ''); } }, 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(); }, 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; } } } }, 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. */ Ext.define('Ext.rtl.grid.column.Column', { override: 'Ext.grid.column.Column', isAtStartEdge: function(e, margin) { var me = this, offset; if (!me.getInherited().rtl !== !Ext.rootInheritedState.rtl) { // jshint ignore:line offset = me.getX() + me.getWidth() - e.getXY()[0]; // To the right of the first column, not over if (offset < 0 && this.getIndex() === 0) { return false; } return (offset <= me.getHandleWidth(e)); } else { return me.callParent([ e, margin ]); } }, isAtEndEdge: function(e, margin) { var me = this; return (!me.getInherited().rtl !== !Ext.rootInheritedState.rtl) ? (// jshint ignore:line e.getXY()[0] - me.getX() <= me.getHandleWidth(e)) : me.callParent([ e, margin ]); }, privates: { _alignMap: { start: 'right', end: 'left' } } }); /** * @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: [{ * icon: 'extjs-build/examples/shared/icons/fam/cog_edit.png', // Use a URL in the icon config * 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.Glyph' ], /** * @cfg {Number/String} glyph * @inheritdoc Ext.panel.Header#glyph * @since 6.2.0 */ /** * @cfg {String} [icon=Ext#BLANK_IMAGE_URL] * @inheritdoc Ext.panel.Header#icon */ /** * @cfg {String} 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. * @cfg {Ext.view.Table} handler.view The owning TableView. * @cfg {Number} handler.rowIndex The row index clicked on. * @cfg {Number} handler.colIndex The column index clicked on. * @cfg {Object} handler.item The clicked item (or this Column if multiple {@link #cfg-items} were not configured). * @cfg {Event} handler.e The click event. * @cfg {Ext.data.Model} handler.record The Record underlying the clicked row. * @cfg {HTMLElement} handler.row The table row clicked upon. * @controllable */ /** * @cfg {Object} scope * The scope (`this` reference) in which the `{@link #handler}`, * `{@link #getClass}`, `{@link #cfg-isDisabled}` 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-isDisabled} method. */ /** * @cfg {Boolean} [stopSelection=true] * 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 #iconCls}. * @cfg {Object} getClass.v The value of the column's configured field (if any). * @cfg {Object} getClass.metadata An object in which you may set the following attributes: * @cfg {String} getClass.metadata.css A CSS class name to add to the cell's TD element. * @cfg {String} 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} getClass.r The Record providing the data. * @cfg {Number} getClass.rowIndex The row index. * @cfg {Number} getClass.colIndex The column index. * @cfg {Ext.data.Store} getClass.store The Store which is providing the data Model. */ /** * @cfg {Function} isDisabled A function which determines whether the action item for any row is disabled and returns `true` or `false`. * @cfg {Ext.view.Table} isDisabled.view The owning TableView. * @cfg {Number} isDisabled.rowIndex The row index. * @cfg {Number} isDisabled.colIndex The column index. * @cfg {Object} isDisabled.item The clicked item (or this Column if multiple {@link #cfg-items} were not configured). * @cfg {Ext.data.Model} isDisabled.record The Record underlying the row. */ /** * @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 */ /** * @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`. * @cfg {Ext.view.Table} items.isDisabled.view The owning TableView. * @cfg {Number} items.isDisabled.rowIndex The row index. * @cfg {Number} items.isDisabled.colIndex The column index. * @cfg {Object} items.isDisabled.item The clicked item (or this Column if multiple {@link #cfg-items} were not configured). * @cfg {Ext.data.Model} items.isDisabled.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`, `isDisabled` 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 `isDisabled` 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=Actions] * Text to display in this column's menu item if no {@link #text} was specified as a header. */ menuText: 'Actions', /** * @cfg {Number} [itemTabIndex=0] Default tabIndex attribute value for each action item. */ itemTabIndex: 0, /** * @cfg {String} [itemAriaRole="button"] Default ARIA role for each action item. */ itemAriaRole: 'button', maskOnDisable: false, // Disable means the action(s) ignoreExport: true, 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 ]); // 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)); } 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 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'; for (i = 0; i < len; i++) { item = items[i]; icon = item.icon; glyph = item.glyph; disabled = item.disabled || (item.isDisabled ? Ext.callback(item.isDisabled, 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="' + tooltip + '"' : '') + (icon ? '/>' : glyph ? (' style="font-family:' + glyph.fontFamily + '">' + glyph.character + '') : '>'); } 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); // 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)]; disabled = item.disabled || (item.isDisabled ? Ext.callback(item.isDisabled, 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)) { 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 the handler moved focus outside of the view, do not allow this event to propagate // to cause any navigation. if (view.destroyed) { return false; } else { // If the record was deleted by the handler, refresh // the position based upon coordinates. if (!e.position.getNode()) { e.position.refresh(); } if (!view.el.contains(Ext.Element.getActiveElement())) { 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 []; }, 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. */ trueText: 'true', // // /** * @cfg {String} falseText * The string returned by the renderer when the column value is falsey (but not undefined). */ falseText: 'false', // /** * @cfg {String} undefinedText * The string returned by the renderer when the column value is undefined. */ undefinedText: ' ', defaultFilterType: 'boolean', /** * @cfg {Object} renderer * @hide */ /** * @cfg {Object} scope * @hide */ /** * @cfg {Boolean} 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 checked cell. */ /** * @cfg {String} checkedTooltip * The tooltip text to show upon hover of an unchecked 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} 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} 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 method may be invoked more than once in an event, so defer its actual invocation. // For example it's invoked in the renderer and updater and they may be called from a loop. this.updateHeaderState = Ext.Function.createAnimationFrame(config.updateHeaderState || this.updateHeaderState); this.scope = this; this.callParent(arguments); }, 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, position, text, anncEl; 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) { position = 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) { // This is called on a timer, so ignore if it fires after destruction if (!this.destroyed && this.headerCheckbox) { this.setHeaderStatus(this.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 = me.tooltip; if (me.invert) { value = !value; } if (me.disabled) { cellValues.tdCls += ' ' + me.disabledCls; } if (value) { cls += ' ' + me.checkboxCheckedCls; tip = me.checkedTooltip || 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 ''; }, 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, result; // Only proceed if we NEED to change 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 = me.tooltip; if (me.invert) { value = !value; } if (value) { tip = me.checkedTooltip || tip; } if (tip) { cell.setAttribute('data-qtip', tip); } else { cell.removeAttribute('data-qtip'); } cell = Ext.fly(cell); if (me.useAriaElements) { me.updateCellAriaDescription(null, value, cell); } cell[me.disabled ? 'addCls' : 'removeCls'](me.disabledCls); 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.dom.setAttribute('aria-describedby', me.id + '-cell-description' + (!isSelected ? '-not' : '') + '-selected'); } } }, 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(''); if (me.useAriaElements) { out.push('' + me.headerDeselectText + '' + '' + me.headerSelectText + '' + '' + me.rowDeselectText + '' + '' + me.rowSelectText + ''); } } } }); /** * 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', { extend: 'Ext.grid.column.Column', alias: [ 'widget.datecolumn' ], requires: [ 'Ext.Date' ], alternateClassName: 'Ext.grid.DateColumn', 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 {Boolean} 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' ], requires: [ 'Ext.util.Format' ], alternateClassName: 'Ext.grid.NumberColumn', defaultFilterType: 'number', // /** * @cfg {String} format * A formatting string as used by {@link Ext.util.Format#number} to format a numeric value for this Column. */ format: '0,000.00', // /** * @cfg {Object} renderer * @hide */ /** * @cfg {Object} scope * @hide */ /** * @cfg {Boolean} 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); } }); /** * 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', { 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: " ", /** * @cfg {Number} width * The default width in pixels of the row number column. */ width: 23, /** * @cfg {Boolean} sortable * @hide */ sortable: false, /** * @cfg {Boolean} [draggable=false] * 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, align: 'right', /** * @cfg {Boolean} producesHTML * @inheritdoc */ producesHTML: false, 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 me.width = me.width; me.callParent(arguments); // Override any setting from the HeaderContainer's defaults me.sortable = false; me.scope = me; }, resizable: false, hideable: false, menuDisabled: true, dataIndex: '', cls: Ext.baseCSSPrefix + 'row-numberer', tdCls: Ext.baseCSSPrefix + 'grid-cell-row-numberer ' + Ext.baseCSSPrefix + 'grid-cell-special', innerCls: Ext.baseCSSPrefix + 'grid-cell-inner-row-numberer', rowspan: undefined, defaultRenderer: function(value, metaData, record, rowIdx, colIdx, dataSource, view) { var rowspan = this.rowspan, page = dataSource.currentPage, result = view.store.indexOf(record); 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) { Ext.fly(cell).down(this.getView().innerSelector, true).innerHTML = this.defaultRenderer(value, null, record, null, null, dataSource, view); } }); /** * 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', { extend: 'Ext.grid.column.Column', alias: [ 'widget.templatecolumn' ], requires: [ 'Ext.XTemplate' ], alternateClassName: 'Ext.grid.TemplateColumn', /** * @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(arguments); }, 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. * * Ths simplest way is to use data binding. Each cell widget 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}
    The record which backs the grid row. * - `recordIndex` : {@link Number}
    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 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 * 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', alias: 'widget.widgetcolumn', mixins: [ 'Ext.mixin.StyleCacher' ], config: { /** * @cfg 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: {} }, ignoreExport: true, /** * @cfg * @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}
    The record which backs the grid row. * - `recordIndex` : {@link Number}
    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: * `getWidgetRecord` - Returns the {@link Ext.data.Model record} the widget is associated with. * `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); }, 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; me.listenerScopeFn = function(defaultScope) { if (defaultScope === 'this') { return this; } return me.resolveListenerScope(defaultScope); }; // 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 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; 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.getId(), me.widget, record); result.resolveListenerScope = me.listenerScopeFn; result.getWidgetRecord = me.widgetRecordDecorator; result.getWidgetColumn = me.widgetColumnDecorator; result.measurer = me; result.ownerCmp = me.getView(); // 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.dom.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); }, onViewRefresh: function(view, records) { Ext.suspendLayouts(); this.onItemAdd(records); Ext.resumeLayouts(true); }, 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 `
    ` 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 `` 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 `// 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'; } 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 */ toggleSummaryRow: function(visible, /* private */ 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().refresh(); } }, 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 ], 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 {Boolean} group True to aggregate in grouped mode * @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, summaryRows, 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); }, 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(), // We need to know all of the possible unique group names. The only way to know this is to check itemGroupKeys, which will keep a // list of all potential group names. It's not enough to get the key of the existing groups since the collection may be filtered. groupNames = groups && Ext.Array.unique(Ext.Object.getValues(groups.itemGroupKeys)), isCollapsed = false, oldMetaGroupCache = groupingFeature.getCache(), oldItem, metaGroup, metaGroupCache, i, len, featureGrouper, group, groupName, groupPlaceholder, key, modelData, Model; groupingFeature.invalidateCache(); // Get a new cache since we invalidated the old one. metaGroupCache = groupingFeature.getCache(); 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) { Model = store.getModel(); 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. if (me.badGrouperKey === key && (featureGrouper = groupingFeature.getGrouper(groupField))) { // We must reset the value b/c store.group() will call into this method again! groupingFeature.startCollapsed = collapseAll; store.group(featureGrouper); return; } oldItem = oldMetaGroupCache[key]; metaGroup = metaGroupCache[key] = groupingFeature.getMetaGroup(key); if (oldItem) { metaGroup.isCollapsed = oldItem.isCollapsed; } // Remove the group name from the list of all possible group names. This is how we'll know if any remaining groups // in the old cache should be retained. Ext.Array.splice(groupNames, Ext.Array.indexOf(groupNames, key), 1); isCollapsed = metaGroup.isCollapsed = collapseAll || metaGroup.isCollapsed; // If group is collapsed, then represent it by one dummy row which is never visible, but which acts // as a start and end group trigger. if (isCollapsed) { modelData = {}; modelData[groupField] = key; metaGroup.placeholder = groupPlaceholder = new Model(modelData); groupPlaceholder.isNonData = groupPlaceholder.isCollapsedPlaceholder = true; groupPlaceholder.groupKey = key; data.add(groupPlaceholder); } else // Expanded group - add the group's child records. { data.insert(me.data.length, group.items); } } if (groupNames.length) { // The remainig group names in this list may refer to potential groups that have been filtered/sorted. If the group // name exists in the old cache, we must retain it b/c the groups could be recreated. See EXTJS-15755 for an example. // Anything left in the old cache can be discarded. for (i = 0 , len = groupNames.length; i < len; i++) { groupName = groupNames[i]; metaGroupCache[groupName] = oldMetaGroupCache[groupName]; } } oldMetaGroupCache = null; } 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, 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; 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, 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; 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) { var store = this.store, Model = store.getModel(), modelData = {}, key = group.getGroupKey(), groupPlaceholder; modelData[store.getGroupField()] = key; groupPlaceholder = metaGroup.placeholder = new Model(modelData); 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.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())) { return me.onDataChanged(); } // 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. * * @example * 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' }, * { text: 'Seniority', dataIndex: 'seniority' } * ], * features: [{ftype:'grouping'}], * width: 200, * height: 275, * 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. */ Ext.define('Ext.grid.feature.Grouping', { extend: 'Ext.grid.feature.Feature', mixins: { summary: 'Ext.grid.feature.AbstractSummary' }, requires: [ 'Ext.grid.feature.GroupStore' ], alias: 'feature.grouping', 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: ', * '
    {name:this.formatName}
    ', * { * formatName: function(name) { * return Ext.String.trim(name); * } * } * ] * * - Example 3 (Template Instance): * * groupHeaderTpl: Ext.create('Ext.XTemplate', * 'Group: ', * '
    {name:this.formatName}
    ', * { * 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. */ groupByText: 'Group by this field', // // /** * @cfg {String} [showGroupsText="Show in groups"] * Text displayed in the grid header for enabling/disabling grouping. */ 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, // expandTip: 'Click to expand. CTRL key collapses all others', // // collapseTip: 'Click to collapse. CTRL/click collapses all others', // showSummaryRow: false, 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);', '}', '%}', '', '', // 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); %}', '', '', '', '', // Only output the first row if this is *not* a collapsed group '', '{%', 'values.itemClasses.length = 0;', 'this.nextTpl.applyOut(values, out, parent);', '%}', '', '', '{%me.outputSummaryRecord(values.summaryRecord, values, out, parent);%}', '', '', '{%this.nextTpl.applyOut(values, out, parent);%}', '', { 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'); 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); } } } } ], relayedEvents: [ 'groupcollapse', 'groupexpand' ], init: function(grid) { var me = this, view = me.view, store = me.getGridStore(), lockPartner, 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; } else // If it's a local store we can build a grouped store for use as the view's dataSource { // Share the GroupStore between both sides of a locked grid lockPartner = me.lockingPartner; if (lockPartner && lockPartner.dataSource) { me.dataSource = view.dataSource = dataSource = lockPartner.dataSource; } else { me.dataSource = view.dataSource = dataSource = new Ext.grid.feature.GroupStore(me, store); } } 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 (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.view.getStore(); }, 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; }, createCache: function() { var metaGroupCache = this.metaGroupCache = {}, lockingPartner = this.lockingPartner; if (lockingPartner) { lockingPartner.metaGroupCache = metaGroupCache; } return metaGroupCache; }, getCache: function() { return this.metaGroupCache || this.createCache(); }, invalidateCache: function() { var lockingPartner = this.lockingPartner; this.metaGroupCache = null; if (lockingPartner) { lockingPartner.metaGroupCache = null; } }, vetoEvent: function(record, row, rowIndex, e) { // Do not veto mouseover/mouseout if (e.type !== 'mouseover' && e.type !== 'mouseout' && e.type !== 'mouseenter' && e.type !== 'mouseleave' && e.getTarget(this.eventSelector)) { 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 = 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'), groupMenuMeth, 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) { groupMenuMeth = activeHeader.groupable === false || !activeHeader.dataIndex || me.view.headerCt.getVisibleGridColumns().length < 2 ? 'disable' : 'enable'; groupMenuItem[groupMenuMeth](); } // header containing TDs have to span all columns, hiddens 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(groupName); 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'), groupMenuMeth = header.groupable === false || !header.dataIndex || me.view.headerCt.getVisibleGridColumns().length < 2 ? 'disable' : 'enable', groupToggleMenuItem = menu.down('#groupToggleMenuItem'), isGrouped = me.grid.getStore().isGrouped(); groupMenuItem[groupMenuMeth](); if (groupToggleMenuItem) { groupToggleMenuItem.setChecked(isGrouped, true); groupToggleMenuItem[isGrouped ? 'enable' : 'disable'](); } Ext.grid.header.Container.prototype.showMenuBy.apply(me, arguments); }, 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, 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); } 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 metaGroupCache = this.metaGroupCache || this.createCache(), key, metaGroup; if (group.isModel) { group = this.getGroup(group); } // An empty string is a valid groupKey so only filter null and undefined. if (group != null) { key = (typeof group === 'string') ? group : group.getGroupKey(); metaGroup = metaGroupCache[key]; if (!metaGroup) { // TODO: Break this out into its own method? metaGroup = metaGroupCache[key] = { isCollapsed: false, lastGroup: null, lastGroupGeneration: null, lastFilterGeneration: null, aggregateRecord: new Ext.data.Model() }; } } 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.metaGroupCache[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) { if (e.pointerType === 'mouse') { e.preventDefault(); } }, /** * Toggle between expanded/collapsed state when clicking on * the group. * @private */ onGroupClick: function(view, rowElement, groupName, e) { var me = this, metaGroupCache = me.getCache(), groupIsCollapsed = !me.isExpanded(groupName), g; if (me.collapsible) { // 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); return; } if (groupIsCollapsed) { me.expand(groupName); } else { me.collapse(groupName); } } }, setupRowData: function(record, idx, rowValues) { var me = this, recordIndex = rowValues.recordIndex, data = me.refreshData, metaGroupCache = me.getCache(), 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 placheholder 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; rowValues.isFirstRow = rowValues.isLastRow = true; rowValues.groupHeaderCls = me.hdCollapsedCls; rowValues.isCollapsedGroup = rowValues.needsWrap = true; rowValues.groupName = groupName; rowValues.groupRenderInfo = groupRenderInfo; groupRenderInfo.groupField = groupField; groupRenderInfo.name = groupRenderInfo.renderedGroupValue = hasRenderer ? column.renderer(group.getAt(0).get(groupField), {}, record) : groupName; groupRenderInfo.groupValue = items[0].get(groupField); 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 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; }, getAggregateRecord: function(metaGroup, forceNew) { var rec; if (forceNew === true || !metaGroup.aggregateRecord) { rec = new Ext.data.Model(); metaGroup.aggregateRecord = rec; rec.isNonData = rec.isSummary = true; } return metaGroup.aggregateRecord; }, /** * Used by the Grouping Feature when {@link #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, reader = store.getProxy().getReader(), 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 = me.getAggregateRecord(metaGroup); } 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().getByKey(grouper.getGroupString(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; me.invalidateCache(); 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, bufferedStore; if (store && store !== oldStore) { bufferedStore = store.isBufferedStore; if (!dataSource) { Ext.destroy(me.storeListeners); me.setupStoreListeners(store); } // Grouping involves injecting a dataSource in early if (bufferedStore !== oldStore.isBufferedStore) { Ext.raise('Cannot reconfigure grouping switching between buffered and non-buffered stores'); } view.isGrouping = !!store.getGrouper(); dataSource.bindStore(store); } }, populateRecord: function(group, metaGroup, remoteData) { var me = this, view = me.grid.ownerLockable ? me.grid.ownerLockable.view : me.view, store = me.getGridStore(), record = me.getAggregateRecord(metaGroup), // 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 throught 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 }); } } }); /** * This feature adds an aggregate summary row at the bottom of each group that is provided * by the {@link Ext.grid.feature.Grouping} feature. 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 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 two parameters: an array of records, and an array of field values * 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', 'subject', { * name: 'mark', * type: 'int' * }] * }); * * Ext.create('Ext.grid.Panel', { * width: 200, * height: 240, * renderTo: document.body, * features: [{ * groupHeaderTpl: 'Subject: {name}', * ftype: 'groupingsummary' * }], * 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.GroupingSummary', { extend: 'Ext.grid.feature.Grouping', alias: 'feature.groupingsummary', showSummaryRow: true, vetoEvent: function(record, row, rowIndex, e) { var result = this.callParent(arguments); if (result !== false && e.getTarget(this.summaryRowSelector)) { result = false; } return result; } }); /** * 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: '
    ' + record.get("desc") + '
    ', * 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 }, 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);', '%}', '', '', '', '', '', '', '{%', '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 owner = this.owner, destRowBody = Ext.fly(destRow).down(owner.eventSelector, true), sourceRowBody; // Sync the heights of row body elements in each row if they need it. if (destRowBody && (sourceRowBody = Ext.fly(sourceRow).down(owner.eventSelector, true))) { Ext.fly(destRowBody).syncContent(sourceRowBody); } } } ], 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 items = this.view.el.query(this.rowBodyTdSelector), colspan = headerCt.getVisibleGridColumns().length, len = items.length, i; 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', { /* Begin Definitions */ 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', panelBodyCls: Ext.baseCSSPrefix + 'summary-', // turn off feature events. hasFeatureEvent: false, fullSummaryTpl: [ '{%', '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) {', ' 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, 'sycSrc'); 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(arguments); if (dock) { grid.addBodyCls(me.panelBodyCls + dock); grid.headerCt.on({ add: me.onStoreUpdate, afterlayout: 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' ], renderTpl: [ '
    ` 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, 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', 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.showSummaryRow) { 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
    ', '{%', // 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";', '%}', // TODO. Make the group header tabbable with tabIndex="0" and enable grid navigation "Action Mode" // to activate it. '
    ', '
    ', '{[values.groupHeaderTpl.apply(values.groupRenderInfo, parent) || " "]}', '
    ', '
    ', '
    ', '
    {rowBody}
    ', '
    ', '', '
    ', '
    ' ], 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(); }, single: true }); // Stretch the innerCt of the summary bar upon headerCt layout grid.headerCt.afterComponentLayout = Ext.Function.createSequence(grid.headerCt.afterComponentLayout, function() { var width = this.getTableWidth(), innerCt = me.summaryBar.innerCt; me.summaryBar.item.setWidth(width); // "this" is the HeaderContainer. Its 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 (this.tooNarrow) { width += Ext.getScrollbarSize().width; } innerCt.setWidth(width); }); } else { if (grid.bufferedRenderer) { me.wrapsItem = true; view.addRowTpl(Ext.XTemplate.getTpl(me, 'fullSummaryTpl')).summaryFeature = me; view.on('refresh', me.onViewRefresh, me); } else { me.wrapsItem = false; me.view.addFooterFn(me.renderSummaryRow); } } 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, rows; // If we get to here we won't be buffered if (!me.disabled && me.showSummaryRow && !view.addingRows && !view.updatingRows) { record = me.summaryRecord; out.push(''); me.outputSummaryRecord((record && record.isModel) ? record : me.createSummaryRecord(view), values, out, parent); out.push('
    '); } }, 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.appendChild(Ext.fly(view.createRowElement(record, -1)).down(me.summaryRowSelector, true)); } }, createSummaryRecord: function(view) { var me = this, columns = view.headerCt.getGridColumns(), remoteRoot = me.remoteRoot, summaryRecord = me.summaryRecord, colCount = columns.length, i, column, dataIndex, summaryValue, modelData; if (!summaryRecord) { modelData = { id: view.id + '-summary-record' }; summaryRecord = me.summaryRecord = new Ext.data.Model(modelData); } // 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); // If docked, the updated row will need sizing because it's outside the View me.onColumnHeaderLayout(); } 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 onColumnHeaderLayout: function() { var view = this.view, columns = view.headerCt.getVisibleGridColumns(), column, len = columns.length, i, summaryEl = this.summaryBar.el, el; 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 {Number/String} glyph * @inheritdoc Ext.panel.Header#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 {String} [icon=Ext#BLANK_IMAGE_URL] * @inheritdoc Ext.panel.Header#icon */ /** * @cfg {String} 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', focusable: true, ariaRole: 'menuitem', ariaEl: 'itemEl', 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', childEls: [ 'itemEl', 'iconEl', 'textEl', 'arrowEl' ], renderTpl: '' + '{text}' + '' + ' {linkHrefCls}{childElCls}"' + ' href="{href}" ' + ' target="{hrefTarget}"' + ' hidefocus="true"' + // For most browsers the text is already unselectable but Opera needs an explicit unselectable="on". ' unselectable="on"' + '' + ' tabindex="{tabIndex}"' + '' + ' {$}="{.}"' + '>' + '{text}' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '', autoEl: { role: 'presentation' }, 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() { for (var 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(); clearTimeout(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}. * * @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. */ 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; // coerce to bool straight away me.checked = !!me.checked; me.callParent(arguments); 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.setChecked(!me.checked, true); 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; } } this.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)); } }, /** * 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; 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; Ext.menu.Manager.onCheckChange(me, checked); 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: ' ', 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' ], mixins: [ 'Ext.util.FocusableContainer' ], 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 100 */ mouseLeaveDelay: 100, /** * @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. */ /** * @inheritdoc */ focusOnToFront: false, bringParentToFront: false, alignOnScroll: false, // Menus are focusable focusable: true, tabIndex: -1, // 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(arguments); // 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(arguments); // 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() { 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) { keyNav.map.processEventScope = me; keyNav.map.processEvent = 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; } return e; }; // 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(me.el, { eventName: 'keydown', scope: me, esc: me.onEscapeKey }); } me.callParent(arguments); // 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: ' ' }); } // 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.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. me.itemOverTask.delay(me.mouseLeaveDelay, 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(arguments); }, afterShow: function() { var me = this, ariaDom = me.ariaEl.dom; me.callParent(arguments); Ext.menu.Manager.onShow(me); 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.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(arguments); } 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); } }, // 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 activate * Fires when an inactive filter becomes active * @param {Ext.grid.filters.Filters} this */ /** * @event deactivate * Fires when an active filter becomes inactive * @param {Ext.grid.filters.Filters} this */ /** * @event update * Fires when a filter configuration has changed * @param {Ext.grid.filters.Filters} this The filter object. */ /** * 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 'update' 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. * @param {String} key The filter key for columns that support multiple filters. */ 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); } }, // TODO: fire activate/deactivate 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', * 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 * Defaults to 'Yes'. */ yesText: 'Yes', // // /** * @cfg {String} noText * Defaults to 'No'. */ noText: 'No', // updateBuffer: 0, /** * @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, 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', * 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 * } * }, */ 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', * 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="Loading..."] * The text that is displayed while the configured store is loading. */ 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 // // Note we need to make sure it's not the empty store (if it is, the store is being bound to a VM). if (!me.store && !me.options) { gridStore = me.getGridStore(); if (me.value != null && me.active && !gridStore.isEmptyStore) { 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, listeners, itemDefaults, record, gid, idValue, idField, labelValue, labelField, i, item, 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. item = 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. config.value = config.value || []; return this.callParent([ config, key ]); }, getOptionsFromStore: function(store) { var me = this, data = store.getData(), 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', * 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. */ 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', * 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. */ 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', * 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="Filters"] * The text for the filters menu. */ 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 }); me.gridListeners = grid.on({ destroyable: true, scope: me, reconfigure: me.onReconfigure }); me.bindStore(store); if (grid.stateful) { store.statefulFilters = true; } 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) { menu.on({ beforeshow: this.onMenuBeforeShow, scope: this }); }, /** * @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.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) { this.local = !store.getRemoteFilter(); store.getFilters().on('remove', this.onFilterRemove, this); }, 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); }, destroy: function() { var me = this; Ext.destroy(me.lockedEventRelayers, me.normalEventRelayers); me.lockedEventRelayers = me.normalEventRelayers = null; me.callParent(); }, 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, lockedHeaderCt = lockedGrid.headerCt, normalHeaderCt = me.lockable.normalGrid.headerCt, columns = lockedHeaderCt.items.items.concat(normalHeaderCt.items.items), length = columns.length, i, column, switchSides, colState, lockedCount; // 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 && Boolean(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 -= lockedCount; } } // Each side must apply individual column's state lockedHeaderCt.applyColumnsState(columnsState, storeState); normalHeaderCt.applyColumnsState(columnsState, storeState); }, 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.util.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   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 this.getViewForColumn(column).getEl(); }, getCellSelector: function() { return this.normalView.getCellSelector(); }, getItemSelector: function() { return this.normalView.getItemSelector(); }, getViewForColumn: function(column) { var view = this.lockedView, inLocked; view.headerCt.cascade(function(col) { if (col === column) { inLocked = true; return false; } }); return inLocked ? view : this.normalView; }, 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) { args = args || []; var me = this, view = me.lockedView; // 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, initial, propName) { 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 * @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 ]); } }, 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) { var view = this.getViewForColumn(column), row = view.getRow(record); return Ext.fly(row).down(column.getCellSelector()); }, 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); }, 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.lockedViewEventRelayers, me.normalViewEventRelayers); 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; if (Ext.isObject(x)) { lockedX = x.lockedX; if (lockedX) { this.getLockedScroller().scrollTo(lockedX, null, animate); } } this.callParent([ x, y, animate ]); }, updateLockedScroller: function(lockedScroller) { lockedScroller.on('scroll', 'onLockedScroll', this); lockedScroller.setLockingScroller(this); }, updateNormalScroller: function(normalScroller) { normalScroller.on('scroll', 'onNormalScroll', this); normalScroller.setLockingScroller(this); }, getPosition: function() { var me = this, position = me.callParent(); position.x = me.getNormalScroller().getPosition().x; position.lockedX = me.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 = pos.y + ((normalView.headerCt.tooNarrow || lockedView.headerCt.tooNarrow) ? Ext.getScrollbarSize().height : 0); normalView.stretchHeight(height); lockedView.stretchHeight(height); this.callParent([ pos ]); }, doScrollTo: function(x, y, animate) { if (x != null) { this.getNormalScroller().scrollTo(x, null, animate); x = null; } this.callParent([ x, y, animate ]); }, onLockedScroll: function(lockedScroller, x, y) { this.position.lockedX = x; }, onNormalScroll: function(normalScroller, x, y) { this.position.x = x; } } }); /** * @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', // i8n text // unlockText: 'Unlock', // // lockText: 'Lock', // // 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, typeToCreate, xtypes, xtypesLn, xtype, superxtype; if (me.subGridXType) { typeToCreate = me.subGridXType; } else { // Treeness only moves down into the locked side. // The normal side is always just a grid if (!lockedSide) { return 'gridpanel'; } xtypes = me.getXTypes().split('/'); xtypesLn = xtypes.length; xtype = xtypes[xtypesLn - 1]; superxtype = xtypes[xtypesLn - 2]; if (superxtype !== 'tablepanel') { typeToCreate = superxtype; } else { typeToCreate = xtype; } } return typeToCreate; }, // 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; var me = this, store = me.store = Ext.StoreManager.lookup(me.store), lockedViewConfig = me.lockedViewConfig, normalViewConfig = me.normalViewConfig, Obj = Ext.Object, // Hash of {lockedFeatures:[],normalFeatures:[]} allFeatures, // Hash of {topPlugins:[],lockedPlugins:[],normalPlugins:[]} allPlugins, lockedGrid, normalGrid, i, columns, lockedHeaderCt, normalHeaderCt, 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; 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 }); // 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 }); // 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() { delete this.lockedGrid.$initParent; delete this.normalGrid.$initParent; }, 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.style.overflowX = this.normalGrid.headerCt.tooNarrow ? 'scroll' : ''; locked.body.dom.scrollTop = this.getScrollable().getPosition().y; }, onEndLockedFloat: function() { var locked = this.lockedGrid, el = locked.getContentTarget().dom; // 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.syncLockableLayout(); }, 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.layoutCounter && 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); } if (!me.scrollContainer) { me.initScrollContainer(); } me.lastScrollPos = 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, lockedGrid = me.lockedGrid, normalGrid = me.normalGrid, lockedViewEl, normalViewEl, lockedViewDom, normalViewDom, lockedViewRegion, normalViewRegion, scrollbarSize, scrollbarWidth, scrollbarHeight, normalViewWidth, lockedViewWidth, normalViewX, hasVerticalScrollbar, hasHorizontalScrollbar, scrollContainerHeight, scrollBodyHeight, lockedScrollbar, normalScrollbar, scrollbarVisibleCls, scrollHeight, lockedGridVisible, normalGridVisible, scrollBodyDom, viewWidth, scrollBarOnRight; if (!me.isCollapsingOrExpanding && lockedGrid && normalGrid) { lockedGridVisible = lockedGrid.isVisible(true) && !lockedGrid.collapsed; normalGridVisible = normalGrid.isVisible(true); lockedViewEl = lockedGrid.view.el; normalViewEl = normalGrid.view.el; lockedViewDom = lockedViewEl.dom; normalViewDom = normalViewEl.dom; 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.getScrollbarSize(); scrollbarWidth = scrollbarSize.width; scrollbarHeight = scrollbarSize.height; normalViewWidth = normalGridVisible ? normalViewRegion.width : 0; lockedViewWidth = lockedGridVisible ? lockedViewRegion.width : 0; normalViewX = lockedGridVisible ? normalViewRegion.x - lockedViewRegion.x : 0; hasHorizontalScrollbar = (normalGrid.headerCt.tooNarrow || lockedGrid.headerCt.tooNarrow) ? scrollbarHeight : 0; scrollContainerHeight = normalViewRegion.height; scrollBodyHeight = scrollContainerHeight; lockedScrollbar = me.lockedScrollbar; normalScrollbar = me.normalScrollbar; scrollbarVisibleCls = me.scrollbarVisibleCls , scrollBarOnRight = normalViewEl._rtlScrollbarOnRight; 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, scrollbarHeight); normalScrollbar.setSize(normalViewWidth, scrollbarHeight); if (me.getInherited().rtl) { normalScrollbar.rtlSetLocalX(normalViewX); me.normalScrollbarClipper.rtlSetLocalX(normalViewX); } else { normalScrollbar.setLocalX(normalViewX); me.normalScrollbarClipper.setLocalX(normalViewX); } me.scrollContainer.setBox(viewWidth = lockedGridVisible ? lockedViewRegion.union(normalViewRegion) : normalViewRegion); // Account for the scrollbar being stuck at the right in RTL mode // This is a bug which affects Safari. All our layouts assume that // scrollbar always goes at the locale end of content. if (scrollBarOnRight) { if (hasVerticalScrollbar) { scrollBodyDom.style.width = (viewWidth + scrollbarWidth) + 'px'; scrollBodyDom.style.right = -scrollbarWidth + 'px'; normalGrid.headerCt.layout.innerCt.setWidth(normalGrid.headerCt.layout.innerCt.getWidth() + scrollbarWidth); me.verticalScrollbarScroller.setSize({ y: me.scrollable.getSize().y }); me.verticalScrollbar.show(); } else { me.verticalScrollbar.hide(); } } me.getScrollable().scrollTo(me.lastScrollPos); } }, initScrollContainer: function() { var me = this, scrollContainer = me.scrollContainer = me.body.insertFirst({ cls: [ me.scrollContainerCls, me._rtlCls ] }), scrollBody = me.scrollBody = scrollContainer.appendChild({ cls: me.scrollBodyCls }), lockedScrollbar = me.lockedScrollbar = scrollContainer.appendChild({ cls: [ me.scrollbarCls, me.scrollbarCls + '-locked', me._rtlCls ] }), normalScrollbar = me.normalScrollbar = scrollContainer.appendChild({ cls: [ me.scrollbarCls, me._rtlCls ] }), lockedView = me.lockedGrid.view, normalView = me.normalGrid.view, lockedScroller = lockedView.getScrollable(), normalScroller = normalView.getScrollable(), Scroller = Ext.scroll.Scroller, lockedScrollbarScroller, normalScrollbarScroller, lockedScrollbarClipper, normalScrollbarClipper, scrollable; 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', me._rtlCls ] }); normalScrollbarClipper = me.normalScrollbarClipper = scrollBody.appendChild({ cls: [ me.scrollbarClipperCls, me._rtlCls ] }); 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() }); if (normalView.el._rtlScrollbarOnRight) { me.verticalScrollbar = scrollContainer.appendChild({ cls: me.scrollbarCls, style: { top: 0, left: 0, bottom: 0, width: Ext.getScrollbarSize().width + 'px' } }); me.verticalScrollbarScroller = new Scroller({ element: me.verticalScrollbar, x: false, y: true }); me.verticalScrollbarScroller.addPartner(me.scrollable, 'y'); } lockedScrollbarScroller.addPartner(lockedScroller, 'x'); normalScrollbarScroller.addPartner(normalScroller, 'x'); // hideHeaders on a TablePanel means that there will be no scrollable. scrollable = me.lockedGrid.headerCt.getScrollable(); if (scrollable) { lockedScrollbarScroller.addPartner(scrollable, 'x'); } scrollable = me.normalGrid.headerCt.getScrollable(); if (scrollable) { normalScrollbarScroller.addPartner(scrollable, 'x'); } // Tell the lockable.View that it has been rendered. me.view.onPanelRender(scrollBody); }, 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() { this.lockedGrid.ensureVisible.apply(this.lockedGrid, arguments); this.normalGrid.ensureVisible.apply(this.normalGrid, 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) { 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); } }, // 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, lockedView = locked.view, lockedScroller = lockedView.getScrollable(), 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} [header] 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. * @private */ lock: function(activeHd, toIdx, toCt) { var me = this, normalGrid = me.normalGrid, lockedGrid = me.lockedGrid, normalView = normalGrid.view, lockedView = lockedGrid.view, startIndex = normalView.all.startIndex, normalScroller = normalView.getScrollable(), lockedScroller = lockedView.getScrollable(), normalHCt = normalGrid.headerCt, refreshFlags, ownerCt, layoutCount = me.componentLayoutCounter; 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; if (lockedView.bufferedRenderer) { lockedView.bufferedRenderer.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; if (ownerCt) { ownerCt.remove(activeHd, false); } 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; 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); } me.fireEvent('lockcolumn', me, activeHd); Ext.resumeLayouts(true); if (normalScroller) { normalScroller.resumePartnerSync(true); lockedScroller.resumePartnerSync(); } // If we are a isolated layout due to being one half of a locking asembly // where one is collapsed, the top level Ext.grid.locking.Lockable#afterLayout // will NOT have been called, so we have to explicitly run it here. if (me.componentLayoutCounter === layoutCount) { me.syncLockableLayout(); } }, // 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} [header] 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. * @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, layoutCount = me.componentLayoutCounter; // 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; // 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); } me.fireEvent('unlockcolumn', me, activeHd); Ext.resumeLayouts(true); // If we are a isolated layout due to being one half of a locking asembly // where one is collapsed, the top level Ext.grid.locking.Lockable#afterLayout // will NOT have been called, so we have to explicitly run it here. if (me.componentLayoutCounter === layoutCount) { me.syncLockableLayout(); } }, /** * @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 = 0, len; if (features) { if (!Ext.isArray(features)) { features = [ features ]; } lockedFeatures = []; normalFeatures = []; len = features.length; for (; 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, i = 0, len, lockableScope, pluginCls; if (plugins) { if (!Ext.isArray(plugins)) { plugins = [ plugins ]; } topPlugins = []; lockedPlugins = []; normalPlugins = []; len = plugins.length; for (; 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.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.AbstractPlugin', 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, boxready: me.onViewBoxReady, 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; } // Only play the pointer-events;none trick on the platform it is needed on. // Only needed when using DOM scrolling on WebKit. // WebKit does a browser layout when you change the pointer-events style. if (Ext.isWebKit) { me.needsPointerEventsFix = true; scrollerListeners.scrollend = me.onViewScrollEnd; viewListeners.itemmousedown = me.onViewItemMouseDown; } 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 (Boolean(this.variableRowHeight) !== Boolean(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(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 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.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(); }, onViewBoxReady: function(view) { this.refreshScroller(view, this.scrollHeight); }, 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. 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(); me.onViewScrollEnd(); } 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; if (dom) { ownerContext.bodyHeight = dom.offsetHeight; ownerContext.bodyWidth = dom.offsetWidth; } }, /** * @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 = view.body.dom.offsetHeight; 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. 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 = view.body.dom.offsetHeight; // 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) { // Recalculate the view size in rows now that the grid view has changed height me.viewClientHeight = view.lockingPartner ? ((me.scroller && me.scroller.getClientSize().y) || height) : view.el.dom.clientHeight; newViewSize = Math.ceil(height / me.rowHeight) + me.trailingBufferZone + me.leadingBufferZone; me.viewSize = me.setViewSize(newViewSize); } }, stretchView: function(view, scrollRange) { var me = this; // Ensure that both the scroll range AND the positioned view body are in the viewable area. if (me.scrollTop > scrollRange) { me.position = me.scrollTop = Math.max(scrollRange - me.bodyHeight, 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(), y: 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())); // Exchange largest view size as long as the partner has been laid out (and thereby calculated a true view size) if (lockingPartner && !fromLockingPartner && lockingPartner.view.componentLayoutCounter) { if (lockingPartner.viewSize > viewSize) { viewSize = lockingPartner.viewSize; } // If we have not had a layout, we cannot command our partner. // What is happening is that we are being commended to match the partner. else if (view.componentLayoutCounter) { 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) { rows.scroll(Ext.Array.slice(newRecords, rows.endIndex + 1, Infinity), 1, 0); } // Prepend if necessary if (start < rows.startIndex) { oldTop = rows.first(true); 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 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; // 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 // 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, null, true); } view.refreshSize(rows.getCount() !== renderedSize); return; } // Cut into rendered block from above if (startIndex < rows.startIndex && lastAffectedIndex <= rows.endIndex) { me.refreshView(rows.startIndex - oldRecords.length + newRecords.length); return; } if (startIndex < rows.startIndex && lastAffectedIndex <= rows.endIndex && scrollIncrement) { scroller.suspendEvent('scroll'); scroller.scrollTo(null, me.position = me.scrollTop += scrollIncrement); scroller.resumeEvent('scroll'); } // Only need to change display if the view is currently empty, or // change intersects the rendered view. me.refreshView(); }, /** * @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} record 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(); me.onViewScrollEnd(); 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; 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; 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, 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) { me.refreshSize(); } // Locking partner must render the same range if (lockingPartner) { lockingPartner.renderRange(start, end, true, 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; } } }); }, onViewItemMouseDown: function() { var me = this; Ext.getDoc().on({ mouseup: me.onDocumentMouseUp, scope: me, single: true }); me.preservePointerEvents = true; }, onDocumentMouseUp: function() { this.preservePointerEvents = false; }, onViewScroll: function(scroller, x, y) { var me = this, bodyDom = me.view.body.dom, store = me.store, totalCount = (store.getCount()), vscrollDistance, scrollDirection, scrollTop = me.scrollTop = me.scroller.getPosition().y; // 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 play the pointer-events;none trick on the platform it is needed on. // WebKit does a browser layout when you change the pointer-events style. // Stops the jagging DOM scrolling when mouse is over data rows. // But only clear pointer-events if another event hasn't flagged these as necessary // When we click on a partially-visible row, the mousedown will trigger the scroll // but the mouseup/click won't be processed since pointer events are cleared, so no selection will occur if (me.needsPointerEventsFix && !me.preservePointerEvents) { bodyDom.style.pointerEvents = 'none'; } // 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); } } } }, onViewScrollEnd: function() { var me = this, bodyDom = me.view.body.dom; // 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 play the pointer-events;none trick on the platform it is needed on. // WebKit does a browser layout when you change the pointer-events style. // Stops the jagging DOM scrolling when mouse is over data rows. if (me.needsPointerEventsFix) { bodyDom.style.pointerEvents = ''; me.preservePointerEvents = false; } } }, 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 = 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()) { 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.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) { 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, 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; } // 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) { startIndex = me.nextRefreshStartIndex; me.nextRefreshStartIndex = null; } else { startIndex = rows.startIndex; } } // 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, options) { var me = this, view = me.view, scroller = me.scroller, rows = view.all, previousStartIndex = rows.startIndex, previousEndIndex = rows.endIndex, previousFirstItem, previousLastItem, prevRowCount = rows.getCount(), newNodes, viewMoved = startIndex !== rows.startIndex, calculatedTop, scrollIncrement, restoreFocus; // 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) { newNodes = 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; 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. { if (me.scrollTop) { calculatedTop = me.scrollTop = 0; } view.addEmptyText(); } // Keep scroll and rendered block positions synched. if (viewMoved) { me.setBodyTop(calculatedTop); scroller.suspendEvent('scroll'); scroller.scrollTo(null, me.position = me.scrollTop); scroller.resumeEvent('scroll'); } // 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(); view.headerCt.setSortState(); } else { view.refresh(); } // If there are columns to trigger rendering, and the rendered block os 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, fromLockingPartner) { var me = this, rows = me.view.all, store = me.store; // 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, null, fromLockingPartner); } else { if (!me.renderTask) { me.renderTask = new Ext.util.DelayedTask(me.onRangeFetched, me, null, false); } // 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, null, fromLockingPartner ]); } } 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, options, fromLockingPartner) { var me = this, view = me.view, scroller = me.scroller, viewEl = view.el, rows = view.all, increment = 0, calculatedTop, lockingPartner = (view.lockingPartner && !fromLockingPartner && !me.doNotMirror) && view.lockingPartner.bufferedRenderer, variableRowHeight = me.variableRowHeight, oldBodyHeight = me.bodyHeight, layoutCount = view.componentLayoutCounter, activeEl, containsFocus, i, newRows, newTop, newFocus, noOverlap, oldStart, partnerNewRows, pos, removeCount, topAdditionSize, topBufferZone; // 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) { if (!fromLockingPartner) { // Re-cache the scrollTop if there has been an asynchronous call to the server. me.scrollTop = me.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.Element.getActiveElement(true); containsFocus = viewEl.contains(activeEl); // In case the browser does fire synchronous focus events when a focused element is derendered... if (containsFocus) { 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 telporting 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); for (i = 0; i < topAdditionSize; i++) { increment -= newRows[i].offsetHeight; } // 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); 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; 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; } newRows = rows.scroll(Ext.Array.slice(range, rows.endIndex + 1 - start), 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; newRows = rows.scroll(Ext.Array.slice(range, 0, rows.startIndex - start), -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 (variableRowHeight) { // 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) { scroller.scrollTo(null, me.position = (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; scroller.scrollTo(null, me.position = (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; } // We contained focus at the start, check whether activeEl has been derendered. // Focus the cell's column header if so. if (containsFocus) { // Restore active element's focus processing. activeEl.resumeFocusEvents(); if (!viewEl.contains(activeEl)) { pos = view.actionableMode ? view.actionPosition : view.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 view.renderingRows = true; view.onFocusLeave({}); view.renderingRows = false; // Try to focus the contextual column header. // Failing that, look inside it for a tabbable element. // Failing that, focus the view. // Focus MUST NOT just silently die due to DOM removal if (pos.column.focusable) { newFocus = pos.column; } else { newFocus = pos.column.el.findTabbableElements()[0]; } if (!newFocus) { newFocus = view.el; } newFocus.focus(); } } } // Position the item container. newTop = Math.max(Math.floor(newTop), 0); if (view.positionBody) { me.setBodyTop(newTop); } // 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 (newRows && lockingPartner && !lockingPartner.disabled) { // Set the pointers of the partner so that its onRangeFetched believes it is at the correct position. lockingPartner.scrollTop = lockingPartner.position = me.scrollTop; if (lockingPartner.view.ownerCt.isVisible()) { partnerNewRows = lockingPartner.onRangeFetched(range, start, end, options, true); // Sync the row heights if configured to do so, or if one side has variableRowHeight but the other doesn't. // variableRowHeight is just a flag for the buffered rendering to know how to measure row height and // calculate firstVisibleRow and lastVisibleRow. It does not *necessarily* mean that row heights are going // to be asymmetric between sides. For example grouping causes variableRowHeight. But the row heights // each side will be symmetric. // But if one side has variableRowHeight (eg, a cellWrap: true column), and the other does not, that // means there could be asymmetric row heights. if (view.ownerGrid.syncRowHeight || view.ownerGrid.syncRowHeightOnNextLayout || (lockingPartner.variableRowHeight !== variableRowHeight)) { me.syncRowHeights(newRows, partnerNewRows); view.ownerGrid.syncRowHeightOnNextLayout = false; } } if (lockingPartner.bodyTop !== newTop) { lockingPartner.setBodyTop(newTop); } // Set the real scrollY position after the correct data has been rendered there. // It will not handle a scroll because the scrollTop and position have been preset. lockingPartner.scroller.scrollTo(null, me.scrollTop); } // If there's variableRowHeight and the scroll operation did affect that, remeasure now. // We must do this because the RowExpander and RowWidget plugin might make huge differences // in rowHeight, so we might scroll from a zone full of 200 pixel hight rows to a zone of // all 21 pixel high rows. if (me.variableRowHeight && me.bodyHeight !== oldBodyHeight && view.componentLayoutCounter === layoutCount) { delete me.rowHeight; me.refreshSize(); } // If there are columns to trigger rendering, and the rendered block os 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; }, 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; // 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(); if (lockingPartner.rowHeight !== me.rowHeight) { delete lockingPartner.rowHeight; lockingPartner.refreshSize(); } } // body height might have changed with change of rows, and possible syncRowHeights call. me.bodyHeight = lockingPartner.bodyHeight = view.body.dom.offsetHeight; }, setBodyTop: function(bodyTop) { 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 = body.dom.offsetHeight; // 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(); } 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 + elements[target].offsetHeight <= 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 + elements[target].offsetHeight; // 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 me.getFirstVisibleRowIndex() + Math.ceil(clientHeight / me.rowHeight); }, 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); }, // jshint ignore:line 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, options); } }, fireEvent: false }); } }, destroy: function() { var me = this, view = me.view; me.cancelLoad(); if (view && view.el) { view.un('scroll', me.onViewScroll, me); } 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(); } }); Ext.define('Ext.rtl.grid.plugin.BufferedRenderer', { override: 'Ext.grid.plugin.BufferedRenderer', translateBody: function(body, bodyTop) { var scroller = this.view.getScrollable(); if (this.isRTL && Ext.supports.xOriginBug && scroller && scroller.getY()) { body.translate(Ext.getScrollbarSize().width, this.bodyTop = bodyTop); } else { this.callParent([ body, bodyTop ]); } } }); /** * 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 ]; } 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 ]; } 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 recieve 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, 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.fromElement(e.target, 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) { if (this.editing) { var targetComponent = Ext.getCmp(e.getTarget().getAttribute('componentId')); // ESCAPE when a picker is expanded does not cancel the edit if (!(targetComponent && targetComponent.isPickerField && targetComponent.isExpanded)) { 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. * @return {Ext.grid.CellContext/undefined} The editing context based upon the passed record and column */ getEditingContext: function(record, columnHeader) { var me = this, grid = me.grid, colMgr = grid.visibleColumnManager, view, gridRow, rowIdx, colIdx, result, layoutView = me.grid.lockable ? me.grid : me.view; // 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. 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: columnHeader }); 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.CellEditorCellEditor}, 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 {@link Ext.grid.CellEditor 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: { * ptype: '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. */ 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.editors.clear(); } this.callParent(); }, destroy: function() { var me = this; if (me.editors) { me.editors.each(Ext.destroy, Ext); me.editors.clear(); } 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, context, contextGeneration, cell, editor, prevEditor = me.getActiveEditor(), p, editValue; 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 me.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; if (me.beforeEdit(context) === false || me.fireEvent('beforeedit', me, context) === false || context.cancel) { 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); 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) {} 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 || false); // 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(); } // 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; 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 cancel this active editing before caching if (context.view.renderingRows) { if (editor.editing) { me.cancelEdit(); } editor.cacheElement(); } } }, /** * 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(); } 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) { var me = this, editors = me.editors, editorId = column.getItemId(), 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 = new Ext.grid.CellEditor(Ext.apply({ 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; 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 * @private */ getCell: function(record, column) { return this.grid.getView().getCell(record, column); }, 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)) { record.set(context.column.dataIndex, value); // 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); } } }); /** * 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' }, 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; if (comp.rendered) { this.finishInit(comp); } else { me.renderListener = comp.on({ render: function() { me.renderListener = null; me.finishInit(comp); }, destroyable: true, single: true }); } }, /** * 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} 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; ) { 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(); for (var 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 clippy = window.clipboardData; if (clippy && clippy.setData) { clippy.setData("text", data); } else { var me = this, 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 {@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' } }, getCellData: function(format, erase) { var cmp = this.getCmp(), selModel = cmp.getSelectionModel(), ret = [], isRaw = format === 'raw', isText = format === 'text', viewNode, cell, data, dataIndex, lastRecord, column, record, row, view; selModel.getSelected().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.down(column.getCellInnerSelector()); data = cell.dom.innerHTML; if (isText) { data = Ext.util.Format.stripTags(data); } } row.push(data); if (erase && dataIndex) { record.set(dataIndex, null); } }); return Ext.util.TSV.encode(ret); }, getCells: function(format, erase) { var cmp = this.getCmp(), selModel = cmp.getSelectionModel(), ret = [], dataIndex, lastRecord, record, row; selModel.getSelected().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) { var values = Ext.util.TSV.decode(data), 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, navModel = view.getNavigationModel(), destination = navModel.getPosition(), dataIndex, destinationStartColumn, dataObject = {}; // If the view is not focused, use the first cell of the selection as the destination. if (!destination) { view.getSelectionModel().getSelected().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}
    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}
    The source grid view from which the * drag originated * * - **ddel:** HTMLElement
    The drag proxy element which moves with the cursor * * - **item:** HTMLElement
    The grid view node upon which the mousedown event was * registered * * - **records:** {@link Array}
    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: { * ptype: '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 * 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). */ 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, scrollEl; ownerGrid.relayEvents(view, [ 'beforedrop', 'drop' ]); if (me.enableDrag) { if (me.containerScroll) { scrollEl = view.getEl(); } me.dragZone = new Ext.view.DragZone(Ext.apply({ view: view, ddGroup: me.dragGroup || me.ddGroup, dragText: me.dragText, containerScroll: me.containerScroll, scrollEl: scrollEl }, me.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: { * ptype: '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}. */ 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. */ 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); 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} position 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().query('[tabIndex="-1"]').length) { 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) { if (column.isHeader) { var me = this, editor; 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); } } }, // 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) { if (column.isHeader) { var me = this, 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; } }); Ext.define('Ext.rtl.grid.plugin.RowEditing', { override: 'Ext.grid.plugin.RowEditing', initEditorConfig: function() { var cfg = this.callParent(); cfg.rtl = this.grid.getInherited().rtl; return cfg; } }); // feature idea to enable Ajax loading and then the content // cache would actually make sense. Should we dictate that they use // data or support raw html as well? /** * 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 <tr> element which owns the expanded row. * @param {Ext.data.Model} record The record providing the data. * @param {HTMLElement} expandRow The <tr> element containing the expanded data. */ /** * @event collapsebody * **Fired through the grid's View.** * @param {HTMLElement} rowNode The <tr> element which owns the expanded row. * @param {Ext.data.Model} record The record providing the data. * @param {HTMLElement} expandRow The <tr> element containing the expanded data. */ setCmp: function(grid) { var me = this, features; me.callParent(arguments); // 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(arguments); me.grid = grid; view = me.view = grid.getView(); // Bind to view for key and mouse events me.bindView(view); // 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.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 { // 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]) { ownerLockable && (me.grid.syncRowHeightOnNextLayout = true); return; } } }, beforeReconfigure: function(grid, store, columns, oldStore, oldColumns) { var me = this; if (me.viewListeners) { me.viewListeners.destroy(); } if (columns) { me.expanderColumn = new Ext.grid.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, componentLayoutCounter; 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) { componentLayoutCounter = ownerLockable.componentLayoutCounter; // 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); } // The two sides are layout roots. The top grid will not have layed out. // We must postprocess it now. if (ownerLockable && ownerLockable.componentLayoutCounter === componentLayoutCounter) { ownerLockable.syncLockableLayout(); } }, // 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 ''; }, 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 ' '; } }; } }); /** * 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', mixins: [ 'Ext.mixin.Identifiable', 'Ext.mixin.StyleCacher' ], lockableScope: 'top', alias: 'plugin.rowwidget', config: { /** * @cfg 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, 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, 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 (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 }); } } } }, 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 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(); for (itemIndex = rows.startIndex , recordIndex = 0; itemIndex <= rows.endIndex; itemIndex++ , recordIndex++) { me.addWidget(view, records[recordIndex]); } Ext.resumeLayouts(true); }, returnFalse: function() { return false; }, // An injectable resolveListenerScope function for use by the widgets to // link them to the owning view. listenerScopeDecorator: function(defaultScope) { if (defaultScope === 'this') { return this; } return this.ownerCt.resolveListenerScope(defaultScope); }, /** * 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(me.getId() + '-' + view.getId(), widget, record); result.resolveListenerScope = me.listenerScopeDecorator; result.measurer = me; result.ownerCt = view; 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)).down(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) { // Bind widget to record unless it has declared a binding if (widget.defaultBindProperty && !widget.getBind()) { widget.setConfig(widget.defaultBindProperty, record); } 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); } } 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; 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); } 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(); 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 '' + v + ''; * } * } * } * }); * * ####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 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 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 'Yes'; * } else { * return 'No'; * } * } * }, * source: { * Available: true * } * }); * @deprecated 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); } })); 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(); var 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; 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': // Cannot be ".boolean" - YUI hates using reserved words like that editor = me.editors['boolean']; // jshint ignore:line 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)) { // Cannot be ".boolean" - YUI hates using reserved words like that editor = editors['boolean']; } else // jshint ignore:line { 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) { for (var 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, // nameText: 'Name', // // valueText: 'Value', // // dateFormat: 'm/j/Y', // // trueText: 'true', // // 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, store) { var me = this; me.grid = grid; me.store = store; 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', remoteSort: true, requires: [ 'Ext.grid.property.Reader', 'Ext.data.proxy.Memory', 'Ext.grid.property.Property' ], /** * 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; 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. * @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, range, lastRange, rowStart, rowEnd, colStart, colEnd, rowIdx, colIdx, view = me.view, rows = view.all, cell = new Ext.grid.CellContext(view), maxColIdx = view.getVisibleColumnManager().getColumns().length - 1; 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); } }, /** * 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) { 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); } }, setRangeStart: function(startColumn) { var me = this, prevSelection = me.getColumns(); me.startColumn = startColumn; 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. 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]); } }, 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; } var me = this, columns = me.columns, colCount, j, column, values, startIdx, endIdx, i, increment, store, record, prevValues, prevValue, selFirstRowIdx = sel.getFirstRowIndex(), selLastRowIdx = sel.getLastRowIndex(), selectedRowCount = selLastRowIdx - selFirstRowIdx + 1, lastTwoRecords = [], 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, i, column, result = new Array(columns.length); 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; if (!(record.isModel)) { Ext.raise('Row selection must be passed a record'); } var 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) { if (!(record.isModel)) { Ext.raise('Row selection must be passed a record'); } var me = this; 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, i, len, ret = true; 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; } var me = this, result = false, selectedRecords = me.selectedRecords, recIndex, dragRange; // 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); } // If not, check if it is within our drag range if we are in the middle of a drag select if (!result && me.rangeStart != null) { dragRange = me.getRange(); recIndex = me.view.dataSource.indexOf(record); result = recIndex >= dragRange[0] && recIndex <= dragRange[1]; } return result; }, /** * Returns the number of records selected * @return {Number} The number of records selected. */ getCount: function() { var me = this, selectedRecords = me.selectedRecords, result = selectedRecords ? selectedRecords.getCount() : 0, range = me.getRange(), i, store = me.view.dataSource; // If dragging, add all records in the drag that are *not* in the collection for (i = range[0]; i <= range[1]; i++) { if (!selectedRecords || !selectedRecords.byInternalId.get(store.getAt(i).internalId)) { result++; } } return result; }, /** * 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.first()) : -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(), colCount, i, j, context, range, recCount, abort = false; 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]); 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 && me.rangeStart != null) { range = me.getRange(); 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]); 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(); } }, /** * @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. * @private */ setRangeStart: function(start) { // 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; 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. * @private */ setRangeEnd: function(end) { var me = this, range, lastRange, rowIdx, row, view = me.view, store = view.dataSource, rows = view.all, selected = me.selectedRecords, 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; // Loop through the union of last range and current range for (rowIdx = Math.max(Math.min(range[0], lastRange[0]), rows.startIndex) , end = Math.min(Math.max(range[1], lastRange[1]), rows.endIndex); rowIdx <= end; rowIdx++) { row = rows.item(rowIdx); // If we are outside the current range, deselect if (rowIdx < range[0] || rowIdx > range[1]) { // If we are deselecting, also remove from collection if (selected && (rec = selected.byInternalId.get(store.getAt(rowIdx).internalId))) { selected.remove(rec); } view.onRowDeselect(rowIdx); } else { 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() { return this.selectedRecords.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); } }); // Clear the drag range me.setRangeStart(me.lastRange = null); } }, 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() && lastPos.getCell()) { 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(), lastCell = me.lastPos && me.lastPos.getCell(); // Cell corresponding to the position might not be rendered. // This will be called upon scroll if (firstCell && lastCell) { me.enable(); me.handle.alignTo(lastCell, 'c-br'); } else { me.disable(); } }, 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(); } 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(); // 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; } 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.getX(); 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; } 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 *contiguou*s 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 importante 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 } }, /** * @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 #getSelected} 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 #getSelected} 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', isCheckerHd: showCheck, // historically used as a dicriminator property before isCheckColumn 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 ' '; }, /** * @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, cm, range, i; 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.clear(); cm = me.view.ownerGrid.getVisibleColumnManager(); range = Ext.Array.sort([ cm.indexOf(sel.lastColumnSelected), cm.indexOf(header) ], Ext.Array.numericSortFn); for (i = range[0]; i <= range[1]; i++) { me.selectColumn(cm.getHeaderAtIndex(i), true); } } else { if (me.isColumnSelected(header)) { me.deselectColumn(header); me.selected.lastColumnSelected = null; } else { me.selectColumn(header, e.ctrlKey); me.selected.lastColumnSelected = header; } } } }, /** * @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 * @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 || sel.view !== view) { 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); } changed = changed || sel.remove(record); } } if (changed) { me.updateHeaderState(); if (!suppressEvent) { me.fireSelectionChange(); } } }, /** * 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(); } }, /** * 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); } 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 /** * @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, view = me.view, 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 selData = this.selected, rowRange, colCount, colIdx, rowIdx, view, context, selectionChanged; // 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 (selData.isCells) { context = new Ext.grid.CellContext(view); rowRange = selData.getRowRange(); colCount = view.getVisibleColumnManager().getColumns().length; for (rowIdx = rowRange[0]; rowIdx <= rowRange[1]; rowIdx++) { context.setRow(rowIdx); for (colIdx = 0; colIdx < colCount; colIdx++) { context.setColumn(colIdx); view.onCellDeselect(context); } } } // 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)) { this.remove(column); selectionChanged = 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 ? this.fireSelectionChange : this.updateSelectionExtender, this, { 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, sel = me.selected, store = me.view.store, changed = false; // Deselect filtered out records if (sel && sel.isRows && store.isFiltered()) { sel.eachRow(function(rec) { if (!store.contains(rec)) { this.remove(rec); // Maintainer: this is the Rows selection object, *NOT* me. changed = true; } }); } // 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[changed ? 'fireSelectionChange' : 'updateSelectionExtender'](); }, /** * @private */ resetSelection: function(suppressEvent) { var sel = this.selected; if (sel) { sel.clear(); if (!suppressEvent) { this.fireSelectionChange(); } } }, 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, isCheckClick, startDragSelect; // Ignore right click, shift 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.shiftKey || e.altKey || e.pointerType === 'touch' || view.actionableMode) { return; } if (header) { me.mousedownPosition = e.position.clone(); isCheckClick = header === me.checkColumn; if (isCheckClick) { me.checkCellClicked = e.position.getCell(true); } // Differentiate between row and cell selections. if (header === me.numbererColumn || isCheckClick || !me.cellSelect) { // Enforce rowSelect setting if (me.rowSelect) { if (sel && sel.isRows) { if (!e.ctrlKey && !isCheckClick) { sel.clear(); } } else { if (sel) { sel.clear(); } sel = me.selected = new Ext.grid.selection.Rows(view); } startDragSelect = true; } else if (me.columnSelect) { if (sel && sel.isColumns) { if (!e.ctrlKey && !isCheckClick) { sel.clear(); } } else { if (sel) { sel.clear(); } sel = me.selected = new Ext.grid.selection.Columns(view); } startDragSelect = true; } else { return false; } } else { if (sel) { sel.clear(); } if (!sel || !sel.isCells) { sel = me.selected = new Ext.grid.selection.Cells(view); } startDragSelect = true; } me.lastOverRecord = me.lastOverColumn = null; // Add the listener after the view has potentially been corrected Ext.getBody().on('mouseup', me.onMouseUp, me, { single: true, view: sel.view }); // 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 cell * @param opts * @private */ onMouseMove: function(e, target, opts) { var me = this, view = opts.view, record, rowIdx, cell = e.getTarget(view.cellSelector), header = opts.view.getHeaderByCell(cell), selData = me.selected, pos, recChange, colChange; // when the mousedown happenes 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 assing 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)); } } } // Disable until a valid new selection is announced in fireSelectionChange if (me.extensible) { me.extensible.disable(); } if (header) { 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); } 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; } }, /** * Clean up mousemove event * @param e * @param target * @param opts * @private */ onMouseUp: function(e, target, opts) { var me = this, view = opts.view, cell, record; me.checkCellClicked = null; 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)); } } } // Disable until a valid new selection is announced in fireSelectionChange if (me.extensible) { me.extensible.disable(); } view.el.un('mousemove', me.onMouseMove, me); // Copy the records encompassed by the drag range into the record collection if (me.selected.isRows) { 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 (!e.position || !e.position.isEqual(me.mousedownPosition)) { me.fireSelectionChange(); } } }, /** * 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.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 = new Ext.grid.CellContext(view).setPosition(record, navigateEvent.column), keyEvent = navigateEvent.keyEvent, ctrlKey = keyEvent.ctrlKey, shiftKey = keyEvent.shiftKey, keyCode = keyEvent.getKey(), selectionChanged; // 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 && !(ctrlKey || shiftKey))) && sel.getCount() > 1 && !keyEvent.shiftKey && keyEvent.type === 'click') { 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()) { sel.setRangeStart(navigateEvent.previousRecordIndex || 0); } 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); } selectionChanged = true; } else // Navigate event in a normal cell { // Prioritize cell selection over column selection 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(); } me.fireSelectionChange(); } }, /** * 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; }, getNumbererColumnConfig: function() { var me = this; return { xtype: 'rownumberer', width: me.rowNumbererHeaderWidth, editRenderer: ' ', tdCls: me.rowNumbererTdCls, cls: me.rowNumbererHeaderCls, locked: me.hasLockedHeader }; }, /** * 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) { // Always put row selection columns in the locked side if there is one. if (view.isNormalView) { view = view.lockingPartner; } 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 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; 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.view = me.selected.view; return new Ext.grid.selection.SelectionExtender(extensible); }, /** * 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; // Announce that the selection is to be extended, and if no objections, extend it if (me.view.ownerGrid.fireEvent('beforeselectionextend', me.view.ownerGrid, sel, extension) !== false) { sel.extendRange(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. * @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. * @property {Object} styles A styles object for an Element * @property {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.left = 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, targetAnim, duration, animateProps, anim, changeCount, j, propsLen, propName, oldValue, newValue; // 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); anim = Ext.fx.Manager.getFxQueue(me.el.id)[0]; target.$layoutAnim = anim; anim.on({ afteranimate: function() { 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; } if (me.isCollapsingOrExpanding === 1) { target.componentLayout.redoLayout(me); target.afterCollapse(true); } else if (me.isCollapsingOrExpanding === 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. { if (me.isCollapsingOrExpanding === 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) { if (margins === true) { margins = 5; } var type = typeof margins, ret; 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; } var me = this, el = me.el, styles = {}, styleCount = 0, // used as a boolean, the exact count doesn't matter styleInfo = me.styleInfo, info, propName, numericValue, width = dirtyProps.width, height = dirtyProps.height, target = me.target, 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.getScrollbarSize(); 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) { //Ext.log(this.id,'.',propName,' ',name,': ',this.context.getLayoutName(layout)); (layout.blockedBy || (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 }; }); /** * @override Ext.rtl.layout.ContextItem * This override adds RTL support to Ext.layout.ContextItem. */ Ext.define('Ext.rtl.layout.ContextItem', { override: 'Ext.layout.ContextItem', addPositionStyles: function(styles, props) { var x = props.x, y = props.y, count = 0; if (x !== undefined) { styles[this.parent.target.getInherited().rtl ? 'right' : 'left'] = x + 'px'; ++count; } if (y !== undefined) { styles.top = y + 'px'; ++count; } return count; } }); /** * 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); 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; }, getItem: function(target, el) { var id = el.id, items = this.items, item = items[id] || (items[id] = new Ext.layout.ContextItem({ context: this, target: target, el: el })); 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) { if (ancestor.isContainer) { for (var 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) { // When calling a callback, the currentLayout needs to be adjusted so // that whichever layout caused the invalidate is the currentLayout... if (options[name]) { var me = this, 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)) { // jshint ignore:line // A layout that is not done and has no blocks or triggers that will queue it // automatically, must be queued now: me.queueLayout(layout); } }, /** * 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 //------------------------------------------------------------------------- // 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() { if (this.logOn.flush) { var items = this.flushQueue; Ext.log('--- Flush ', items && items.getCount()); } return this.callParent(arguments); }, flushInvalidates: function() { if (this.logOn.flushInvalidate) { Ext.log('>> flushInvalidates'); } var 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]' : ''), (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', { /* Begin Definitions */ alias: [ 'layout.body' ], extend: 'Ext.layout.component.Auto', /* End Definitions */ 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) | (// jshint ignore:line (ownerContext.widthModel.shrinkWrap ? 1 : 0) | (// jshint ignore:line ownerContext.heightModel.shrinkWrap ? 2 : 0)), calcWidth = (containerDimensions & 1) || undefined, // jshint ignore:line calcHeight = (containerDimensions & 2) || undefined, // jshint ignore:line 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; } if (calcHeight && !ownerContext.setContentHeight(contentHeight + targetPadding.height)) { me.done = false; } } } }); Ext.define('Ext.rtl.layout.container.Absolute', { override: 'Ext.layout.container.Absolute', adjustWidthAnchor: function(width, childContext) { if (this.owner.getInherited().rtl) { var padding = this.targetPadding, x = childContext.getStyle('right'); return width - x + padding.right; } else { return this.callParent([ width, childContext ]); } } }); /** * 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 || ' '; 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', uses: [ 'Ext.resizer.BorderSplitterTracker' ], alias: 'widget.bordersplitter', // 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')) { me.insertSplitter(item, index, hidden || !split, 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); }); Ext.define('Ext.rtl.layout.container.Border', { override: 'Ext.layout.container.Border', initLayout: function() { var me = this; if (me.owner.getInherited().rtl) { me.padOnContainerProp = 'right'; me.padNotOnContainerProp = 'left'; me.horzPositionProp = 'right'; } me.callParent(arguments); } }); /** * 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: '

    Welcome to the Wizard!

    Step 1 of 3

    ' * },{ * id: 'card-1', * html: '

    Step 2 of 3

    ' * },{ * id: 'card-2', * html: '

    Congratulations!

    Step 3 of 3 - Complete

    ' * }], * renderTo: Ext.getBody() * }); */ Ext.define('Ext.layout.container.Card', { /* Begin Definitions */ extend: 'Ext.layout.container.Fit', alternateClassName: 'Ext.layout.CardLayout', alias: 'layout.card', /* End Definitions */ 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', alias: [ 'layout.center', 'layout.ux.center' ], alternateClassName: 'Ext.ux.layout.Center', type: 'center', percentRe: /^\d+(?:\.\d+)?\%$/, itemCls: Ext.baseCSSPrefix + 'center-layout-item', childEls: [ 'targetEl' ], renderTpl: [ '' ], targetElCls: Ext.baseCSSPrefix + 'center-target', beginLayout: function(ownerContext) { var me = this, percentRe = me.percentRe, childItems, len, i, itemContext, item, widthModel, heightModel; me.callParent([ ownerContext ]); childItems = ownerContext.childItems; for (i = 0 , len = childItems.length; i < len; ++i) { itemContext = childItems[i]; item = itemContext.target; widthModel = itemContext.widthModel; heightModel = itemContext.heightModel; 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) | (// jshint ignore:line (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' ], beforeBodyTpl: '
    style="border-spacing:{itemSpacing}px">' + '
    ' + '
    style="width:{labelWidth}">' + '
    ' + '
    ' + '
    ', afterBodyTpl: '
    ', 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); // Ensure we don't get duplicate listeners delete cfg.listeners; Ext.apply(me, { plain: true, showSeparator: false, bodyPadding: 0, items: Ext.applyIf({ cls: Ext.baseCSSPrefix + 'menu-color-item', margin: 0, id: me.pickerId, xtype: 'colorpicker' }, cfg) }); me.callParent(arguments); 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 ARIA label for the Date Picker menu */ 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 enableFocusableContainer: false, initComponent: function() { var me = this, cfg, pickerConfig; if (me.pickerCfg) { pickerConfig = Ext.apply({ cls: Ext.baseCSSPrefix + 'menu-date-item', margin: 0, border: false, id: me.pickerId, 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, id: me.pickerId, xtype: 'datepicker' }, cfg); } Ext.apply(me, { showSeparator: false, plain: true, bodyPadding: 0, // remove the body padding from the datepicker menu item so it looks like 3.3 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) { // 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 (this.floating && this.ownerCmp && this.ownerCmp.focus) { this.ownerCmp.focus(); } }, 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'); } }); /** * 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', { alternateClassName: [ 'Ext.PluginManager', 'Ext.PluginMgr' ], singleton: true, 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`). * @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; } }); /** * 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(); } }); Ext.define('Ext.rtl.resizer.BorderSplitterTracker', { override: 'Ext.resizer.BorderSplitterTracker', rtlDirections: { top: 'top', right: 'left', bottom: 'bottom', left: 'right' }, getCollapseDirection: function() { var direction = this.splitter.getCollapseDirection(); if (!this.splitter.getInherited().rtl !== !Ext.rootInheritedState.rtl) { // jshint ignore:line direction = this.rtlDirections[direction]; } return direction; } }); /** * 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; } }); Ext.define('Ext.rtl.resizer.ResizeTracker', { override: 'Ext.resizer.ResizeTracker', _rtlRegionNames: { south: 'south', north: 'north', east: 'west', west: 'east', northeast: 'northwest', southeast: 'southwest', southwest: 'southeast', northwest: 'northeast' }, convertRegionName: function(name) { return (Ext.rootInheritedState.rtl) ? this._rtlRegionNames[name] : 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', { mixins: { observable: 'Ext.util.Observable' }, uses: [ 'Ext.resizer.ResizeTracker', 'Ext.Component' ], alternateClassName: 'Ext.Resizable', 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' ], /** * @cfg {"SINGLE"} mode * Mode of selection. Valid values are: * * - **"SINGLE"** - Only allows selecting one item at a time. This is the default. */ isCellModel: true, /** * @inheritdoc */ 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} position The position to set. May be an object of the form `{row:1, column:2}` * @param {Boolean} suppressEvent True to suppress selection events */ setCurrentPosition: function(pos, suppressEvent, /* private */ 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} position The position to set. May be an object of the form `{row:1, column:2}` * @param {Boolean} suppressEvent True to suppress selection events */ setPosition: function(pos, suppressEvent, /* private */ 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 ]); }, /** * 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.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 effor 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 (pos.record) { if (column && (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, /** * @inheritdoc */ 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 {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="Row selector"] * 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 */ headerAriaLabel: 'Row selector', /** * @cfg {String} [headerSelectText="Press Space to select all rows"] * 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 */ headerSelectText: 'Press Space to select all rows', /** * @cfg {String} [headerDeselectText="Press Space to deselect all rows"] * ARIA description text to announce for the check column's header when it is focused, * {@link #showHeaderCheckbox} is shown, and all rows are selected. */ headerDeselectText: 'Press Space to deselect all rows', /** * @cfg {String} [rowSelectText="Press Space to select this row"] * ARIA description text to announce when check column cell is focused and the row * is not selected. */ rowSelectText: 'Press Space to select this row', /** * @cfg {String} [rowDeselectText="Press Space to deselect this row"] * ARIA description text to announce when check column cell is focused and the row * is selected. */ 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, owner, ownerLockable = view.grid.ownerLockable; 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()) || (view.isNormalView && !me.column)) { me.addCheckbox(view); owner = view.ownerCt; // Listen to the outermost reconfigure event if (view.headerCt.lockedCt) { owner = owner.ownerCt; } // 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, column = me.column, 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, isCheckerHd: showCheck, // historically used as a dicriminator property before isCheckColumn ignoreExport: true, text: me.headerText, width: me.headerWidth, sortable: false, draggable: false, resizable: false, hideable: false, menuDisabled: true, checkOnly: me.checkOnly, checkboxAriaRole: 'presentation', tdCls: me.tdCls, cls: Ext.baseCSSPrefix + 'selmodel-column', editRenderer: me.editRenderer || me.renderEmpty, locked: me.hasLockedHeader(), processEvent: me.processColumnEvent, // 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) { config.tabIndex = undefined; config.ariaRole = 'presentation'; config.focusable = false; config.cellFocusable = false; } 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; }, /** * @private * Process and refire 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. */ processColumnEvent: function(type, view, cell, recordIndex, cellIndex, e, record, row) { var navModel = view.getNavigationModel(); // Fire a navigate event upon SPACE in actionable mode. // SPACE events are ignored by the NavModel in actionable mode. // `this` is the Column instance! if ((e.type === 'keydown' && view.actionableMode && e.getKey() === e.SPACE) || (!this.checkOnly && e.type === this.triggerEvent)) { navModel.fireEvent('navigate', { view: view, navigationModel: navModel, keyEvent: e, position: e.position, recordIndex: recordIndex, record: record, item: e.item, cell: e.position.cellElement, columnIndex: e.position.colIdx, column: e.position.column }); } }, toggleRecord: function(record, recordIndex, checked, cell) { this[checked ? 'select' : 'deselect']([ record ], this.mode !== 'SINGLE'); }, renderEmpty: function() { return ' '; }, // 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, label; 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) { 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 = !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 }); 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 !== undefined) { 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', minWidth: 10, 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: '', 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?'; } 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=10] * 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 */ tipText: null, /** * @inheritdoc */ defaultBindProperty: 'values', /** * @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} newValue The new value which the slider is being changed to. * @param {Number} oldValue The old value which the slider was previously. */ /** * @event change * Fires when the slider value is changed. * @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 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 */ ariaRole: 'slider', focusable: true, needArrowKeys: true, tabIndex: 0, skipLabelForAttribute: true, focusCls: 'slider-focus', childEls: [ 'endEl', 'innerEl' ], // note: {id} here is really {inputId}, but {cmpId} is available fieldSubTpl: [ '
    tabindex="{tabIdx}"', ' {$}="{.}"', ' {$}="{.}"', '>', '', '
    ', { 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 } ], horizontalProp: 'left', initValue: function() { var me = this, extValueFrom = Ext.valueFrom, // Fallback for initial values: values config -> value config -> minValue config -> 0 values = extValueFrom(me.values, [ extValueFrom(me.value, extValueFrom(me.minValue, 0)) ]), i = 0, len = values.length; // Store for use in dirty check me.originalValue = values; // Add a thumb for each value, enforcing configured constraints for (; i < len; i++) { me.addThumb(me.normalizeValue(values[i])); } }, 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 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(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; }, /** * 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. * @return {Ext.slider.Multi} this */ setValue: function(index, value, animate, changeComplete) { var me = this, thumbs = me.thumbs, ariaDom = me.ariaEl.dom, thumb, len, i, values; if (Ext.isArray(index)) { values = index; animate = value; for (i = 0 , len = values.length; i < len; ++i) { thumb = thumbs[i]; if (thumb) { me.setValue(i, values[i], animate); } } 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) !== 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); 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) { 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), a = 0, aLen = arr.length, val; for (; a < aLen; a++) { val = arr[a]; me.setValue(a, val); } me.clearInvalid(); // delete here so we reset back to the original state delete me.wasValid; }, 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() { if (this.rendered) { Ext.destroy(this.thumbs); } this.callParent(); } }); Ext.define('Ext.rtl.slider.Multi', { override: 'Ext.slider.Multi', initComponent: function() { if (this.getInherited().rtl) { this.horizontalProp = 'right'; } this.callParent(); }, onDragStart: function() { this.callParent(arguments); // Cache the width so we don't recalculate it during the drag this._rtlInnerWidth = this.innerEl.getWidth(); }, onDragEnd: function() { this.callParent(arguments); delete this._rtlInnerWidth; }, onKeyDown: function(e) { var key; if (this.getInherited().rtl) { key = e.getKey(); if (key === e.RIGHT) { e.keyCode = e.LEFT; } else if (key === e.LEFT) { e.keyCode = e.RIGHT; } } return this.callParent([ e ]); }, transformTrackPoints: function(pos) { var left, innerWidth; if (this.isOppositeRootDirection()) { left = pos.left; delete pos.left; innerWidth = typeof this._rtlInnerWidth !== 'undefined' ? this._rtlInnerWidth : this.innerEl.getWidth(); pos.right = innerWidth - left; return pos; } else { return this.callParent(arguments); } } }); /** * 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' ], /** * @inheritdoc */ defaultBindProperty: 'value', 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.getValue(), 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); } }, 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 = me.reversePercentageValue(parseInt(thumb.dom.style[me.getThumbPositionStyle()], 10)); 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) { newValue = me.reversePixelValue(trackPoint); 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 * @return {Ext.slider.Multi} this */ setThumbValue: function(index, value, animate, changeComplete) { var me = this, thumb, thumbValue, 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); thumbValue = me.reversePercentageValue(parseInt(thumb.dom.style[me.getThumbPositionStyle()], 10)); // ensures value is contstrained and snapped value = me.normalizeValue(value); if (value !== thumbValue && me.fireEvent('beforechange', me, value, thumbValue, thumb) !== false) { 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].dom.style[me.getThumbPositionStyle()]; value = me.reversePercentageValue(parseInt(value, 10)); } 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.length; for (; i < len; i++) { values.push(me.reversePercentageValue(parseInt(me.thumbs[i].dom.style[me.getThumbPositionStyle()], 10))); } 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({ // jshint ignore:line target: thumb, duration: 350, from: from, to: to }); } }, /** * @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(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()); }, /** * @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.getMinValue() + this.getRange() * (pos / 100); }, 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); } }); Ext.define('Ext.rtl.slider.Widget', { override: 'Ext.slider.Widget', constructor: function(config) { this.callParent([ config ]); if (this.getInherited().rtl) { this.horizontalProp = 'right'; } } }); /** * 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, // /** * @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`. */ // The wording is chosen to be less confusing to blind users. 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', baseCls: Ext.baseCSSPrefix + 'tab-bar', requires: [ 'Ext.tab.Tab', 'Ext.util.Point', 'Ext.layout.component.Body' ], mixins: [ 'Ext.util.FocusableContainer' ], 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', childEls: [ 'body', 'strip' ], _stripCls: Ext.baseCSSPrefix + 'tab-bar-strip', _baseBodyCls: Ext.baseCSSPrefix + 'tab-bar-body', /** * @private */ renderTpl: '{% this.renderTabGuard(out, values, \'before\'); %}' + '' + '{% this.renderTabGuard(out, values, \'after\'); %}' + '', /** * @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 This config is deprecated. It is much easier to 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 This config is deprecated. It is much easier to 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'); } me.callParent(); me.setLayout({ align: initialAlign || (me.getTabStretchMax() ? 'stretchmax' : me._layoutAlign[me.dock]), overflowHandler: initialOverflowHandler || 'scroller' }); 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) { 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); } } } }); Ext.define('Ext.rtl.tab.Bar', { override: 'Ext.tab.Bar', privates: { // rtl hook _getTabAdjustProp: function() { return this.getInherited().rtl ? 'right' : 'left'; }, getCloseXY: function(closeEl, tabX, tabY, tabWidth, tabHeight, closeWidth, closeHeight, direction) { var closeXY, closeX, closeY, xy; if (this.isOppositeRootDirection()) { closeXY = closeEl.getXY(); if (direction === 'right') { closeX = tabX + closeXY[1] - tabY; closeY = tabY + tabHeight - (closeXY[0] - (tabX + tabWidth - tabHeight)) - closeWidth; } else { closeX = tabX + tabWidth - (closeXY[1] - tabY) - closeHeight; closeY = tabY + (closeXY[0] - (tabX + tabWidth - tabHeight)); } xy = [ closeX, closeY ]; } else { xy = this.callParent(arguments); } return xy; } } }); /** * 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 = new Ext.layout.container.Card(Ext.apply({ owner: me, 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, hidden: item.hidden && !item.hiddenByLayout, // only hide if it wasn't hidden by the layout itself 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); } } }, 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; // 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 else if (item.tab && (toActivate = me.tabBar.items.indexOf(me.tabBar.findNextActivatable(item.tab))) !== -1) { me.setActiveTab(toActivate); } me.callParent([ item, autoDestroy ]); 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', xtype: 'breadcrumb', requires: [ 'Ext.data.TreeStore', 'Ext.button.Split' ], mixins: [ 'Ext.util.FocusableContainer' ], isBreadcrumb: true, baseCls: Ext.baseCSSPrefix + 'breadcrumb', layout: { type: 'hbox', align: 'middle' }, config: { /** * @cfg {String} [buttonUI='plain-toolbar'] * 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} * The name of the field in the data model to display in the navigation items of * this breadcrumb toolbar */ displayField: 'text', /** * @cfg {String} [overflowHandler=null] * 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=null] * * 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=null] * * 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=true] * `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' }, publishes: [ 'selection' ], twoWayBindable: [ 'selection' ], _breadcrumbCls: Ext.baseCSSPrefix + 'breadcrumb', _btnCls: Ext.baseCSSPrefix + 'breadcrumb-btn', _folderIconCls: Ext.baseCSSPrefix + 'breadcrumb-icon-folder', _leafIconCls: Ext.baseCSSPrefix + 'breadcrumb-icon-leaf', 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(), cls: me._btnCls + ' ' + me._btnCls + '-' + me.ui, 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', // Toolbar required here because we'll try to decorate it's alternateClassName // with this class' alternate name requires: [ 'Ext.toolbar.Toolbar' ], alias: 'widget.tbfill', alternateClassName: 'Ext.Toolbar.Fill', 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', // Toolbar required here because we'll try to decorate it's alternateClassName // with this class' alternate name requires: [ 'Ext.toolbar.Toolbar' ], alias: 'widget.tbspacer', alternateClassName: 'Ext.Toolbar.Spacer', 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. * @declarativeHandler */ cellTpl: [ '', '
    lineempty" role="presentation">
    ', '
    ', '
    -end-plus {expanderCls}" role="presentation">
    ', '', '
    {checkboxCls}-checked">
    ', '
    ', '', '', 'style="font-family:{glyphFontFamily}"', '', '>{glyph}', '', '', '', '', ' role="presentation" class="{childCls} {baseIconCls} {customIconCls} ', '{baseIconCls}-leafparent-expandedparent {iconCls}" ', 'style="background-image:url({icon})"/>>', '', '', '{value}', '', '{value}', '' ], // 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; me.hasCustomRenderer = me.innerRenderer && me.innerRenderer.length > 1; }, 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 }; }, 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 ]); } }); Ext.define('Ext.rtl.tree.Column', { override: 'Ext.tree.Column', getChildCls: function() { return this._childCls || (this._childCls = (this.getInherited().rtl ? Ext.baseCSSPrefix + 'rtl' : '')); } }); /** * @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: { selectionModel: { type: 'treemodel' } }, /** * @property {Boolean} isTreeView * `true` in this class to identify an object as an instantiated TreeView, or subclass thereof. */ isTreeView: true, 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', ariaRole: 'treegrid', /** * @cfg {Boolean} * @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, toggleOnDblClick: true, stripeRows: false, // 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); } } } ], /** * @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; } 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, fireRemoveEvent = me.hasListeners.remove, 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 ]); } if (fireRemoveEvent) { 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; } // Only fire the event if there's anyone listening if (fireRemoveEvent) { 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(); me.animWraps[animWrap.record.internalId] = queue[id] = 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.isVisible()) { 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(); me.animWraps[animWrap.record.internalId] = queue[id] = 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), parentNode, parentChecked, foundCheck, foundClear, childNodes, matchedChildCount = 0, 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('data') !== 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); } // 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) { e.getTarget(this.cellSelector, 10, true).addCls(this.expanderIconOverCls); }, onExpanderMouseOut: function(e) { e.getTarget(this.cellSelector, 10, true).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: { * model: 'myApp.TerritoryRoot', // Needs to be this so it knows to create 'Country' child nodes * 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  ✈  DFW', * duration: '6h 55m', * expanded: true, * children: [{ * text: 'SFO  ✈  PHX', * duration: '2h 04m', * leaf: true * }, { * text: 'PHX layover', * duration: '2h 36m', * isLayover: true, * leaf: true * }, { * text: 'PHX  ✈  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 }, /** * @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, select, doFocus, separator = (options && options.separator) || '/', 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 ]); } var me = this, field = (options && options.field) || me.store.model.idProperty, separator = (options && options.separator) || '/', callback, scope, keys, rooted, last, node, parentNode, 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.Function.defer(this.expandNode, this.expandDelay, this, [ node ]); }, /** * @private */ cancelExpand: function() { if (this.expandProcId) { clearTimeout(this.expandProcId); this.expandProcId = false; } }, 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; } 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}
    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}
    The source tree view from which the * drag originated * * - **ddel:** HTMLElement
    The drag proxy element which moves with the cursor * * - **item:** HTMLElement
    The tree view node upon which the mousedown event was * registered * * - **records:** {@link Array}
    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: { * ptype: '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 * 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. */ 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; }); /** * 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 = arguments.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; } } }); /** * 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: * '' + * '
    ' + * '' + * '
    {name}
    ' + * '
    ' + * '
    ' * }]; * }, * * 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', layout: 'fit', floating: true, alignOnScroll: false, minWidth: 200, minHeight: 200, border: true, keyMap: { scope: 'this', ESC: 'hide' }, platformConfig: { desktop: { resizable: true }, 'tablet && rtl': { resizable: { handles: 'sw' } }, 'tablet && !rtl': { resizable: { handles: 'se' } } }, defaultListenerScope: true, 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#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() { this.callParent(arguments); // Do not focus if this was invoked by a touch gesture if (!this.invocationEvent || this.invocationEvent.pointerType !== 'touch') { var 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, 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 '' + this.removeRowText + ''; }, 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 of bottom right in 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, cls: Ext.baseCSSPrefix + 'toast', bodyPadding: 10, autoClose: true, plain: false, draggable: false, resizable: false, 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', 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 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, slideInAnimation: 'easeIn', slideBackAnimation: 'bounceOut', slideInDuration: 500, slideBackDuration: 500, hideDuration: 500, 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, stickWhileHover: true, closeOnMouseDown: false, closable: false, 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; }; });