/* Copyright(c) 2011 Company Name */ /** * Implements infinite scrolling of a grid, allowing users can scroll * through thousands of records without the performance penalties of * renderering all the records on screen at once. The grid should be * bound to a *buffered* store with a pageSize specified. * * The number of rows rendered outside the visible area, and the * buffering of pages of data from the remote server for immediate * rendering upon scroll can be controlled by configuring the * {@link Ext.grid.PagingScroller #verticalScroller}. * * You can tell it to create a larger table to provide more scrolling * before a refresh is needed, and also to keep more pages of records * in memory for faster refreshing when scrolling. * * var myStore = Ext.create('Ext.data.Store', { * // ... * buffered: true, * pageSize: 100, * // ... * }); * * var grid = Ext.create('Ext.grid.Panel', { * // ... * autoLoad: true, * verticalScroller: { * trailingBufferZone: 200, // Keep 200 records buffered in memory behind scroll * leadingBufferZone: 5000 // Keep 5000 records buffered in memory ahead of scroll * }, * // ... * }); * * ## Implementation notes * * This class monitors scrolling of the {@link Ext.view.Table * TableView} within a {@link Ext.grid.Panel GridPanel} which is using * a buffered store to only cache and render a small section of a very * large dataset. * * **NB!** The GridPanel will instantiate this to perform monitoring, * this class should never be instantiated by user code. Always use the * {@link Ext.panel.Table#verticalScroller verticalScroller} config. * */ Ext.define('Ext.grid.PagingScroller', { /** * @cfg * @deprecated This config is now ignored. */ percentageFromEdge: 0.35, /** * @cfg * The zone which causes a refresh of the rendered viewport. 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 * The number of extra rows to render on the trailing side of scrolling * **outside the {@link #numFromEdge}** buffer as scrolling proceeds. */ trailingBufferZone: 5, /** * @cfg * The number of extra rows to render on the leading side of scrolling * **outside the {@link #numFromEdge}** buffer as scrolling proceeds. */ leadingBufferZone: 15, /** * @cfg * This is the time in milliseconds to buffer load requests when scrolling the PagingScrollbar. */ scrollToLoadBuffer: 200, // private. Initial value of zero. viewSize: 0, // private. Start at default value rowHeight: 21, // private. Table extent at startup time tableStart: 0, tableEnd: 0, constructor: function(config) { var me = this; me.variableRowHeight = config.variableRowHeight; me.bindView(config.view); Ext.apply(me, config); me.callParent(arguments); }, bindView: function(view) { var me = this, viewListeners = { scroll: { fn: me.onViewScroll, element: 'el', scope: me }, render: me.onViewRender, resize: me.onViewResize, boxready: { fn: me.onViewResize, scope: me, single: true }, // If there are variable row heights, then in beforeRefresh, we have to find a common // row so that we can synchronize the table's top position after the refresh. // Also flag whether the grid view has focus so that it can be refocused after refresh. beforerefresh: me.beforeViewRefresh, refresh: me.onViewRefresh, scope: me }, storeListeners = { guaranteedrange: me.onGuaranteedRange, scope: me }, gridListeners = { reconfigure: me.onGridReconfigure, scope: me }, partner; // If we need unbinding... if (me.view) { if (me.view.el) { me.view.el.un('scroll', me.onViewScroll, me); // un does not understand the element options } partner = view.lockingPartner; if (partner) { partner.un('refresh', me.onLockRefresh, me); } me.view.un(viewListeners); me.store.un(storeListeners); if (me.grid) { me.grid.un(gridListeners); } delete me.view.refreshSize; // Remove the injected refreshSize implementation } me.view = view; me.grid = me.view.up('tablepanel'); me.store = view.store; if (view.rendered) { me.viewSize = me.store.viewSize = Math.ceil(view.getHeight() / me.rowHeight) + me.trailingBufferZone + (me.numFromEdge * 2) + me.leadingBufferZone; } partner = view.lockingPartner; if (partner) { partner.on('refresh', me.onLockRefresh, me); } me.view.mon(me.store.pageMap, { scope: me, clear: me.onCacheClear }); // During scrolling we do not need to refresh the height - the Grid height must be set by config or layout in order to create a scrollable // table just larger than that, so removing the layout call improves efficiency and removes the flicker when the // HeaderContainer is reset to scrollLeft:0, and then resynced on the very next "scroll" event. me.view.refreshSize = Ext.Function.createInterceptor(me.view.refreshSize, me.beforeViewrefreshSize, me); /** * @property {Number} position * Current pixel scroll position of the associated {@link Ext.view.Table View}. */ me.position = 0; // We are created in View constructor. There won't be an ownerCt at this time. if (me.grid) { me.grid.on(gridListeners); } else { me.view.on({ added: function() { me.grid = me.view.up('tablepanel'); me.grid.on(gridListeners); }, single: true }); } me.view.on(me.viewListeners = viewListeners); me.store.on(storeListeners); }, onCacheClear: function() { var me = this; // Do not do anything if view is not rendered, or if the reason for cache clearing is store destruction if (me.view.rendered && !me.store.isDestroyed) { // Temporarily disable scroll monitoring until the scroll event caused by any following *change* of scrollTop has fired. // Otherwise it will attempt to process a scroll on a stale view me.ignoreNextScrollEvent = me.view.el.dom.scrollTop !== 0; me.view.el.dom.scrollTop = 0; delete me.lastScrollDirection; delete me.scrollOffset; delete me.scrollProportion; } }, onGridReconfigure: function (grid) { this.bindView(grid.view); }, // Ensure that the stretcher element is inserted into the View as the first element. onViewRender: function() { var me = this, view = me.view, el = me.view.el, stretcher; me.stretcher = me.createStretcher(view); view = view.lockingPartner; if (view) { stretcher = me.stretcher; me.stretcher = new Ext.CompositeElement(stretcher); me.stretcher.add(me.createStretcher(view)); } }, createStretcher: function(view) { var el = view.el; el.setStyle('position', 'relative'); return el.createChild({ style:{ position: 'absolute', width: '1px', height: 0, top: 0, left: 0 } }, el.dom.firstChild); }, onViewResize: function(view, width, height) { var me = this, newViewSize; newViewSize = Math.ceil(height / me.rowHeight) + me.trailingBufferZone + (me.numFromEdge * 2) + me.leadingBufferZone; if (newViewSize > me.viewSize) { me.viewSize = me.store.viewSize = newViewSize; me.handleViewScroll(me.lastScrollDirection || 1); } }, // Used for variable row heights. Try to find the offset from scrollTop of a common row beforeViewRefresh: function() { var me = this, view = me.view, rows, direction; // Refreshing can cause loss of focus. me.focusOnRefresh = Ext.Element.getActiveElement === view.el.dom; // Only need all this is variableRowHeight if (me.variableRowHeight) { direction = me.lastScrollDirection; me.commonRecordIndex = undefined; // If we are refreshing in response to a scroll, // And we know where the previous start was, // and we're not teleporting out of visible range // and the view is not empty if (direction && (me.previousStart !== undefined) && (me.scrollProportion === undefined) && (rows = view.getNodes()).length) { // We have scrolled downwards if (direction === 1) { // If the ranges overlap, we are going to be able to position the table exactly if (me.tableStart <= me.previousEnd) { me.commonRecordIndex = rows.length - 1; } } // We have scrolled upwards else if (direction === -1) { // If the ranges overlap, we are going to be able to position the table exactly if (me.tableEnd >= me.previousStart) { me.commonRecordIndex = 0; } } // Cache the old offset of the common row from the scrollTop me.scrollOffset = -view.el.getOffsetsTo(rows[me.commonRecordIndex])[1]; // In the new table the common row is at a different index me.commonRecordIndex -= (me.tableStart - me.previousStart); } else { me.scrollOffset = undefined; } } }, onLockRefresh: function(view) { view.table.dom.style.position = 'absolute'; }, // Used for variable row heights. Try to find the offset from scrollTop of a common row // Ensure, upon each refresh, that the stretcher element is the correct height onViewRefresh: function() { var me = this, store = me.store, newScrollHeight, view = me.view, viewEl = view.el, viewDom = viewEl.dom, rows, newScrollOffset, scrollDelta, table = view.table.dom, tableTop, scrollTop; // Refresh causes loss of focus if (me.focusOnRefresh) { viewEl.focus(); me.focusOnRefresh = false; } // Scroll events caused by processing in here must be ignored, so disable for the duration me.disabled = true; // No scroll monitoring is needed if // All data is in view OR // Store is filtered locally. // - scrolling a locally filtered page is obv a local operation within the context of a huge set of pages // so local scrolling is appropriate. if (store.getCount() === store.getTotalCount() || (store.isFiltered() && !store.remoteFilter)) { me.stretcher.setHeight(0); me.position = viewDom.scrollTop = 0; // Chrome's scrolling went crazy upon zeroing of the stretcher, and left the view's scrollTop stuck at -15 // This is the only thing that fixes that me.setTablePosition('absolute'); // We remain disabled now because no scrolling is needed - we have the full dataset in the Store return; } me.stretcher.setHeight(newScrollHeight = me.getScrollHeight()); scrollTop = viewDom.scrollTop; // Flag to the refreshSize interceptor that regular refreshSize postprocessing should be vetoed. me.isScrollRefresh = (scrollTop > 0); // If we have had to calculate the store position from the pure scroll bar position, // then we must calculate the table's vertical position from the scrollProportion if (me.scrollProportion !== undefined) { me.setTablePosition('absolute'); me.setTableTop((me.scrollProportion ? (newScrollHeight * me.scrollProportion) - (table.offsetHeight * me.scrollProportion) : 0) + 'px'); } else { me.setTablePosition('absolute'); me.setTableTop((tableTop = (me.tableStart||0) * me.rowHeight) + 'px'); // ScrollOffset to a common row was calculated in beforeViewRefresh, so we can synch table position with how it was before if (me.scrollOffset) { rows = view.getNodes(); newScrollOffset = -viewEl.getOffsetsTo(rows[me.commonRecordIndex])[1]; scrollDelta = newScrollOffset - me.scrollOffset; me.position = (viewDom.scrollTop += scrollDelta); } // If the table is not fully in view view, scroll to where it is in view. // This will happen when the page goes out of view unexpectedly, outside the // control of the PagingScroller. For example, a refresh caused by a remote sort or filter reverting // back to page 1. // Note that with buffered Stores, only remote sorting is allowed, otherwise the locally // sorted page will be out of order with the whole dataset. else if ((tableTop > scrollTop) || ((tableTop + table.offsetHeight) < scrollTop + viewDom.clientHeight)) { me.lastScrollDirection = -1; me.position = viewDom.scrollTop = tableTop; } } // Re-enable upon function exit me.disabled = false; }, setTablePosition: function(position) { this.setViewTableStyle(this.view, 'position', position); }, setTableTop: function(top){ this.setViewTableStyle(this.view, 'top', top); }, setViewTableStyle: function(view, prop, value) { view.el.child('table', true).style[prop] = value; view = view.lockingPartner; if (view) { view.el.child('table', true).style[prop] = value; } }, beforeViewrefreshSize: function() { // Veto the refreshSize if the refresh is due to a scroll. if (this.isScrollRefresh) { // If we're vetoing refreshSize, attach the table DOM to the View's Flyweight. this.view.table.attach(this.view.el.child('table', true)); return (this.isScrollRefresh = false); } }, onGuaranteedRange: function(range, start, end) { var me = this, ds = me.store; // this should never happen if (range.length && me.visibleStart < range[0].index) { return; } // Cache last table position in dataset so that if we are using variableRowHeight, // we can attempt to locate a common row to align the table on. me.previousStart = me.tableStart; me.previousEnd = me.tableEnd; me.tableStart = start; me.tableEnd = end; ds.loadRecords(range, { start: start }); }, onViewScroll: function(e, t) { var me = this, view = me.view, lastPosition = me.position; me.position = view.el.dom.scrollTop; // Flag set when the scrollTop is programatically set to zero upon cache clear. // We must not attempt to process that as a scroll event. if (me.ignoreNextScrollEvent) { me.ignoreNextScrollEvent = false; return; } // Only check for nearing the edge if we are enabled. // If there is no paging to be done (Store's dataset is all in memory) we will be disabled. if (!me.disabled) { me.lastScrollDirection = me.position > lastPosition ? 1 : -1; // Check the position so we ignore horizontal scrolling if (lastPosition !== me.position) { me.handleViewScroll(me.lastScrollDirection); } } }, handleViewScroll: function(direction) { var me = this, store = me.store, view = me.view, viewSize = me.viewSize, totalCount = store.getTotalCount(), highestStartPoint = totalCount - viewSize, visibleStart = me.getFirstVisibleRowIndex(), visibleEnd = me.getLastVisibleRowIndex(), el = view.el.dom, requestStart, requestEnd; // Only process if the total rows is larger than the visible page size if (totalCount >= viewSize) { // This is only set if we are using variable row height, and the thumb is dragged so that // There are no remaining visible rows to vertically anchor the new table to. // In this case we use the scrollProprtion to anchor the table to the correct relative // position on the vertical axis. me.scrollProportion = undefined; // We're scrolling up if (direction == -1) { // If table starts at record zero, we have nothing to do if (me.tableStart) { if (visibleStart !== undefined) { if (visibleStart < (me.tableStart + me.numFromEdge)) { requestStart = Math.max(0, visibleEnd + me.trailingBufferZone - viewSize); } } // The only way we can end up without a visible start is if, in variableRowHeight mode, the user drags // the thumb up out of the visible range. In this case, we have to estimate the start row index else { // If we have no visible rows to orientate with, then use the scroll proportion me.scrollProportion = el.scrollTop / (el.scrollHeight - el.clientHeight); requestStart = Math.max(0, totalCount * me.scrollProportion - (viewSize / 2) - me.numFromEdge - ((me.leadingBufferZone + me.trailingBufferZone) / 2)); } } } // We're scrolling down else { if (visibleStart !== undefined) { if (visibleEnd > (me.tableEnd - me.numFromEdge)) { requestStart = Math.max(0, visibleStart - me.trailingBufferZone); } } // The only way we can end up without a visible end is if, in variableRowHeight mode, the user drags // the thumb down out of the visible range. In this case, we have to estimate the start row index else { // If we have no visible rows to orientate with, then use the scroll proportion me.scrollProportion = el.scrollTop / (el.scrollHeight - el.clientHeight); requestStart = totalCount * me.scrollProportion - (viewSize / 2) - me.numFromEdge - ((me.leadingBufferZone + me.trailingBufferZone) / 2); } } // We scrolled close to the edge and the Store needs reloading if (requestStart !== undefined) { // The calculation walked off the end; Request the highest possible chunk which starts on an even row count (Because of row striping) if (requestStart > highestStartPoint) { requestStart = highestStartPoint & ~1; requestEnd = totalCount - 1; } // Make sure first row is even to ensure correct even/odd row striping else { requestStart = requestStart & ~1; requestEnd = requestStart + viewSize - 1; } // If range is satsfied within the prefetch buffer, then just draw it from the prefetch buffer if (store.rangeCached(requestStart, requestEnd)) { me.cancelLoad(); store.guaranteeRange(requestStart, requestEnd); } // Required range is not in the prefetch buffer. Ask the store to prefetch it. // We will recieve a guaranteedrange event when that is done. else { me.attemptLoad(requestStart, requestEnd); } } } }, getFirstVisibleRowIndex: function() { var me = this, view = me.view, scrollTop = view.el.dom.scrollTop, rows, count, i, rowBottom; if (me.variableRowHeight) { rows = view.getNodes(); count = rows.length; if (!count) { return; } rowBottom = Ext.fly(rows[0]).getOffsetsTo(view.el)[1]; for (i = 0; i < count; i++) { rowBottom += rows[i].offsetHeight; // Searching for the first visible row, and off the bottom of the clientArea, then there's no visible first row! if (rowBottom > view.el.dom.clientHeight) { return; } // Return the index *within the total dataset* of the first visible row // We cannot use the loop index to offset from the table's start index because of possible intervening group headers. if (rowBottom > 0) { return view.getRecord(rows[i]).index; } } } else { return Math.floor(scrollTop / me.rowHeight); } }, getLastVisibleRowIndex: function() { var me = this, store = me.store, view = me.view, clientHeight = view.el.dom.clientHeight, rows, count, i, rowTop; if (me.variableRowHeight) { rows = view.getNodes(); if (!rows.length) { return; } count = store.getCount() - 1; rowTop = Ext.fly(rows[count]).getOffsetsTo(view.el)[1] + rows[count].offsetHeight; for (i = count; i >= 0; i--) { rowTop -= rows[i].offsetHeight; // Searching for the last visible row, and off the top of the clientArea, then there's no visible last row! if (rowTop < 0) { return; } // Return the index *within the total dataset* of the last visible row. // We cannot use the loop index to offset from the table's start index because of possible intervening group headers. if (rowTop < clientHeight) { return view.getRecord(rows[i]).index; } } } else { return me.getFirstVisibleRowIndex() + Math.ceil(clientHeight / me.rowHeight) + 1; } }, getScrollHeight: function() { var me = this, view = me.view, table, firstRow, store = me.store, deltaHeight = 0, doCalcHeight = !me.hasOwnProperty('rowHeight'); if (me.variableRowHeight) { table = me.view.table.dom; if (doCalcHeight) { me.initialTableHeight = table.offsetHeight; me.rowHeight = me.initialTableHeight / me.store.getCount(); } else { deltaHeight = table.offsetHeight - me.initialTableHeight; // Store size has been bumped because of odd end row. if (store.getCount() > me.viewSize) { deltaHeight -= me.rowHeight; } } } else if (doCalcHeight) { firstRow = view.el.down(view.getItemSelector()); if (firstRow) { me.rowHeight = firstRow.getHeight(false, true); } } return Math.floor(store.getTotalCount() * me.rowHeight) + deltaHeight; }, attemptLoad: function(start, end) { var me = this; if (me.scrollToLoadBuffer) { if (!me.loadTask) { me.loadTask = new Ext.util.DelayedTask(me.doAttemptLoad, me, []); } me.loadTask.delay(me.scrollToLoadBuffer, me.doAttemptLoad, me, [start, end]); } else { me.store.guaranteeRange(start, end); } }, cancelLoad: function() { if (this.loadTask) { this.loadTask.cancel(); } }, doAttemptLoad: function(start, end) { this.store.guaranteeRange(start, end); }, destroy: function() { var me = this, scrollListener = me.viewListeners.scroll; me.store.un({ guaranteedrange: me.onGuaranteedRange, scope: me }); me.view.un(me.viewListeners); if (me.view.rendered) { me.stretcher.remove(); me.view.el.un('scroll', scrollListener.fn, scrollListener.scope); } } }); Ext.define('Ext.grid.Scroller', { constructor: Ext.deprecated() }); /** * @author Don Griffin * * This class is a base for all id generators. It also provides lookup of id generators by * their id. * * Generally, id generators are used to generate a primary key for new model instances. There * are different approaches to solving this problem, so this mechanism has both simple use * cases and is open to custom implementations. A {@link Ext.data.Model} requests id generation * using the {@link Ext.data.Model#idgen} property. * * # Identity, Type and Shared IdGenerators * * It is often desirable to share IdGenerators to ensure uniqueness or common configuration. * This is done by giving IdGenerator instances an id property by which they can be looked * up using the {@link #get} method. To configure two {@link Ext.data.Model Model} classes * to share one {@link Ext.data.SequentialIdGenerator sequential} id generator, you simply * assign them the same id: * * Ext.define('MyApp.data.MyModelA', { * extend: 'Ext.data.Model', * idgen: { * type: 'sequential', * id: 'foo' * } * }); * * Ext.define('MyApp.data.MyModelB', { * extend: 'Ext.data.Model', * idgen: { * type: 'sequential', * id: 'foo' * } * }); * * To make this as simple as possible for generator types that are shared by many (or all) * Models, the IdGenerator types (such as 'sequential' or 'uuid') are also reserved as * generator id's. This is used by the {@link Ext.data.UuidGenerator} which has an id equal * to its type ('uuid'). In other words, the following Models share the same generator: * * Ext.define('MyApp.data.MyModelX', { * extend: 'Ext.data.Model', * idgen: 'uuid' * }); * * Ext.define('MyApp.data.MyModelY', { * extend: 'Ext.data.Model', * idgen: 'uuid' * }); * * This can be overridden (by specifying the id explicitly), but there is no particularly * good reason to do so for this generator type. * * # Creating Custom Generators * * An id generator should derive from this class and implement the {@link #generate} method. * The constructor will apply config properties on new instances, so a constructor is often * not necessary. * * To register an id generator type, a derived class should provide an `alias` like so: * * Ext.define('MyApp.data.CustomIdGenerator', { * extend: 'Ext.data.IdGenerator', * alias: 'idgen.custom', * * configProp: 42, // some config property w/default value * * generate: function () { * return ... // a new id * } * }); * * Using the custom id generator is then straightforward: * * Ext.define('MyApp.data.MyModel', { * extend: 'Ext.data.Model', * idgen: 'custom' * }); * // or... * * Ext.define('MyApp.data.MyModel', { * extend: 'Ext.data.Model', * idgen: { * type: 'custom', * configProp: value * } * }); * * It is not recommended to mix shared generators with generator configuration. This leads * to unpredictable results unless all configurations match (which is also redundant). In * such cases, a custom generator with a default id is the best approach. * * Ext.define('MyApp.data.CustomIdGenerator', { * extend: 'Ext.data.SequentialIdGenerator', * alias: 'idgen.custom', * * id: 'custom', // shared by default * * prefix: 'ID_', * seed: 1000 * }); * * Ext.define('MyApp.data.MyModelX', { * extend: 'Ext.data.Model', * idgen: 'custom' * }); * * Ext.define('MyApp.data.MyModelY', { * extend: 'Ext.data.Model', * idgen: 'custom' * }); * * // the above models share a generator that produces ID_1000, ID_1001, etc.. * */ Ext.define('Ext.data.IdGenerator', { /** * @property {Boolean} isGenerator * `true` in this class to identify an object as an instantiated IdGenerator, or subclass thereof. */ isGenerator: true, /** * Initializes a new instance. * @param {Object} config (optional) Configuration object to be applied to the new instance. */ constructor: function(config) { var me = this; Ext.apply(me, config); if (me.id) { Ext.data.IdGenerator.all[me.id] = me; } }, /** * @cfg {String} id * The id by which to register a new instance. This instance can be found using the * {@link Ext.data.IdGenerator#get} static method. */ getRecId: function (rec) { return rec.modelName + '-' + rec.internalId; }, /** * Generates and returns the next id. This method must be implemented by the derived * class. * * @return {String} The next id. * @method generate * @abstract */ statics: { /** * @property {Object} all * This object is keyed by id to lookup instances. * @private * @static */ all: {}, /** * Returns the IdGenerator given its config description. * @param {String/Object} config If this parameter is an IdGenerator instance, it is * simply returned. If this is a string, it is first used as an id for lookup and * then, if there is no match, as a type to create a new instance. This parameter * can also be a config object that contains a `type` property (among others) that * are used to create and configure the instance. * @static */ get: function (config) { var generator, id, type; if (typeof config == 'string') { id = type = config; config = null; } else if (config.isGenerator) { return config; } else { id = config.id || config.type; type = config.type; } generator = this.all[id]; if (!generator) { generator = Ext.create('idgen.' + type, config); } return generator; } } }); /** * @class Ext.data.JsonP * @singleton * This class is used to create JSONP requests. JSONP is a mechanism that allows for making * requests for data cross domain. More information is available here. */ Ext.define('Ext.data.JsonP', { /* Begin Definitions */ singleton: true, /* End Definitions */ /** * Number of requests done so far. * @private */ requestCount: 0, /** * Hash of pending requests. * @private */ requests: {}, /** * @property timeout * @type Number * A default timeout for any JsonP requests. If the request has not completed in this time the * failure callback will be fired. The timeout is in ms. Defaults to 30000. */ timeout: 30000, /** * @property disableCaching * @type Boolean * True to add a unique cache-buster param to requests. Defaults to true. */ disableCaching: true, /** * @property disableCachingParam * @type String * Change the parameter which is sent went disabling caching through a cache buster. Defaults to '_dc'. */ disableCachingParam: '_dc', /** * @property callbackKey * @type String * Specifies the GET parameter that will be sent to the server containing the function name to be executed when * the request completes. Defaults to callback. Thus, a common request will be in the form of * url?callback=Ext.data.JsonP.callback1 */ callbackKey: 'callback', /** * Makes a JSONP request. * @param {Object} options An object which may contain the following properties. Note that options will * take priority over any defaults that are specified in the class. * * @return {Object} request An object containing the request details. */ request: function(options){ options = Ext.apply({}, options); if (!options.url) { Ext.Error.raise('A url must be specified for a JSONP request.'); } var me = this, disableCaching = Ext.isDefined(options.disableCaching) ? options.disableCaching : me.disableCaching, cacheParam = options.disableCachingParam || me.disableCachingParam, id = ++me.requestCount, callbackName = options.callbackName || 'callback' + id, callbackKey = options.callbackKey || me.callbackKey, timeout = Ext.isDefined(options.timeout) ? options.timeout : me.timeout, params = Ext.apply({}, options.params), url = options.url, name = Ext.name, request, script; params[callbackKey] = name + '.data.JsonP.' + callbackName; if (disableCaching) { params[cacheParam] = new Date().getTime(); } script = me.createScript(url, params, options); me.requests[id] = request = { url: url, params: params, script: script, id: id, scope: options.scope, success: options.success, failure: options.failure, callback: options.callback, callbackKey: callbackKey, callbackName: callbackName }; if (timeout > 0) { request.timeout = setTimeout(Ext.bind(me.handleTimeout, me, [request]), timeout); } me.setupErrorHandling(request); me[callbackName] = Ext.bind(me.handleResponse, me, [request], true); me.loadScript(request); return request; }, /** * Abort a request. If the request parameter is not specified all open requests will * be aborted. * @param {Object/String} request (Optional) The request to abort */ abort: function(request){ var me = this, requests = me.requests, key; if (request) { if (!request.id) { request = requests[request]; } me.handleAbort(request); } else { for (key in requests) { if (requests.hasOwnProperty(key)) { me.abort(requests[key]); } } } }, /** * Sets up error handling for the script * @private * @param {Object} request The request */ setupErrorHandling: function(request){ request.script.onerror = Ext.bind(this.handleError, this, [request]); }, /** * Handles any aborts when loading the script * @private * @param {Object} request The request */ handleAbort: function(request){ request.errorType = 'abort'; this.handleResponse(null, request); }, /** * Handles any script errors when loading the script * @private * @param {Object} request The request */ handleError: function(request){ request.errorType = 'error'; this.handleResponse(null, request); }, /** * Cleans up anu script handling errors * @private * @param {Object} request The request */ cleanupErrorHandling: function(request){ request.script.onerror = null; }, /** * Handle any script timeouts * @private * @param {Object} request The request */ handleTimeout: function(request){ request.errorType = 'timeout'; this.handleResponse(null, request); }, /** * Handle a successful response * @private * @param {Object} result The result from the request * @param {Object} request The request */ handleResponse: function(result, request){ var success = true; if (request.timeout) { clearTimeout(request.timeout); } delete this[request.callbackName]; delete this.requests[request.id]; this.cleanupErrorHandling(request); Ext.fly(request.script).remove(); if (request.errorType) { success = false; Ext.callback(request.failure, request.scope, [request.errorType]); } else { Ext.callback(request.success, request.scope, [result]); } Ext.callback(request.callback, request.scope, [success, result, request.errorType]); }, /** * Create the script tag given the specified url, params and options. The options * parameter is passed to allow an override to access it. * @private * @param {String} url The url of the request * @param {Object} params Any extra params to be sent * @param {Object} options The object passed to {@link #request}. */ createScript: function(url, params, options) { var script = document.createElement('script'); script.setAttribute("src", Ext.urlAppend(url, Ext.Object.toQueryString(params))); script.setAttribute("async", true); script.setAttribute("type", "text/javascript"); return script; }, /** * Loads the script for the given request by appending it to the HEAD element. This is * its own method so that users can override it (as well as {@link #createScript}). * @private * @param request The request object. */ loadScript: function (request) { Ext.getHead().appendChild(request.script); } }); /** * @author Ed Spencer * * Represents a single read or write operation performed by a {@link Ext.data.proxy.Proxy Proxy}. Operation objects are * used to enable communication between Stores and Proxies. Application developers should rarely need to interact with * Operation objects directly. * * Several Operations can be batched together in a {@link Ext.data.Batch batch}. */ Ext.define('Ext.data.Operation', { /** * @cfg {Boolean} synchronous * True if this Operation is to be executed synchronously. This property is inspected by a * {@link Ext.data.Batch Batch} to see if a series of Operations can be executed in parallel or not. */ synchronous: true, /** * @cfg {String} action * The action being performed by this Operation. Should be one of 'create', 'read', 'update' or 'destroy'. */ action: undefined, /** * @cfg {Ext.util.Filter[]} filters * Optional array of filter objects. Only applies to 'read' actions. */ filters: undefined, /** * @cfg {Ext.util.Sorter[]} sorters * Optional array of sorter objects. Only applies to 'read' actions. */ sorters: undefined, /** * @cfg {Ext.util.Grouper[]} groupers * Optional grouping configuration. Only applies to 'read' actions where grouping is desired. */ groupers: undefined, /** * @cfg {Number} start * The start index (offset), used in paging when running a 'read' action. */ start: undefined, /** * @cfg {Number} limit * The number of records to load. Used on 'read' actions when paging is being used. */ limit: undefined, /** * @cfg {Ext.data.Batch} batch * The batch that this Operation is a part of. */ batch: undefined, /** * @cfg {Object} params * Parameters to pass along with the request when performing the operation. */ /** * @cfg {Function} callback * Function to execute when operation completed. * @cfg {Ext.data.Model[]} callback.records Array of records. * @cfg {Ext.data.Operation} callback.operation The Operation itself. * @cfg {Boolean} callback.success True when operation completed successfully. */ callback: undefined, /** * @cfg {Object} scope * Scope for the {@link #callback} function. */ scope: undefined, /** * @property {Boolean} started * The start status of this Operation. Use {@link #isStarted}. * @readonly * @private */ started: false, /** * @property {Boolean} running * The run status of this Operation. Use {@link #isRunning}. * @readonly * @private */ running: false, /** * @property {Boolean} complete * The completion status of this Operation. Use {@link #isComplete}. * @readonly * @private */ complete: false, /** * @property {Boolean} success * Whether the Operation was successful or not. This starts as undefined and is set to true * or false by the Proxy that is executing the Operation. It is also set to false by {@link #setException}. Use * {@link #wasSuccessful} to query success status. * @readonly * @private */ success: undefined, /** * @property {Boolean} exception * The exception status of this Operation. Use {@link #hasException} and see {@link #getError}. * @readonly * @private */ exception: false, /** * @property {String/Object} error * The error object passed when {@link #setException} was called. This could be any object or primitive. * @private */ error: undefined, /** * @property {RegExp} actionCommitRecordsRe * The RegExp used to categorize actions that require record commits. */ actionCommitRecordsRe: /^(?:create|update)$/i, /** * @property {RegExp} actionSkipSyncRe * The RegExp used to categorize actions that skip local record synchronization. This defaults * to match 'destroy'. */ actionSkipSyncRe: /^destroy$/i, /** * Creates new Operation object. * @param {Object} config (optional) Config object. */ constructor: function(config) { Ext.apply(this, config || {}); }, /** * This method is called to commit data to this instance's records given the records in * the server response. This is followed by calling {@link Ext.data.Model#commit} on all * those records (for 'create' and 'update' actions). * * If this {@link #action} is 'destroy', any server records are ignored and the * {@link Ext.data.Model#commit} method is not called. * * @param {Ext.data.Model[]} serverRecords An array of {@link Ext.data.Model} objects returned by * the server. * @markdown */ commitRecords: function (serverRecords) { var me = this, mc, index, clientRecords, serverRec, clientRec, i, len; if (!me.actionSkipSyncRe.test(me.action)) { clientRecords = me.records; if (clientRecords && clientRecords.length) { if (clientRecords.length > 1) { // If this operation has multiple records, client records need to be matched up with server records // so that any data returned from the server can be updated in the client records. If we don't have // a clientIdProperty specified on the model and we've done a create, just assume the data is returned in order. // If it's an update, the records should already have an id which should match what the server returns. if (me.action == 'update' || clientRecords[0].clientIdProperty) { mc = new Ext.util.MixedCollection(); mc.addAll(serverRecords); for (index = clientRecords.length; index--; ) { clientRec = clientRecords[index]; serverRec = mc.findBy(me.matchClientRec, clientRec); // Replace client record data with server record data clientRec.copyFrom(serverRec); } } else { for (i = 0, len = clientRecords.length; i < len; ++i) { clientRec = clientRecords[i]; serverRec = serverRecords[i]; if (clientRec && serverRec) { me.updateRecord(clientRec, serverRec); } } } } else { // operation only has one record, so just match the first client record up with the first server record this.updateRecord(clientRecords[0], serverRecords[0]); } if (me.actionCommitRecordsRe.test(me.action)) { for (index = clientRecords.length; index--; ) { clientRecords[index].commit(); } } } } }, updateRecord: function(clientRec, serverRec) { // if the client record is not a phantom, make sure the ids match before replacing the client data with server data. if(serverRec && (clientRec.phantom || clientRec.getId() === serverRec.getId())) { clientRec.copyFrom(serverRec); } }, // Private. // Record matching function used by commitRecords // IMPORTANT: This is called in the scope of the clientRec being matched matchClientRec: function(record) { var clientRec = this, clientRecordId = clientRec.getId(); if(clientRecordId && record.getId() === clientRecordId) { return true; } // if the server record cannot be found by id, find by internalId. // this allows client records that did not previously exist on the server // to be updated with the correct server id and data. return record.internalId === clientRec.internalId; }, /** * Marks the Operation as started. */ setStarted: function() { this.started = true; this.running = true; }, /** * Marks the Operation as completed. */ setCompleted: function() { this.complete = true; this.running = false; }, /** * Marks the Operation as successful. */ setSuccessful: function() { this.success = true; }, /** * Marks the Operation as having experienced an exception. Can be supplied with an option error message/object. * @param {String/Object} error (optional) error string/object */ setException: function(error) { this.exception = true; this.success = false; this.running = false; this.error = error; }, /** * Returns true if this Operation encountered an exception (see also {@link #getError}) * @return {Boolean} True if there was an exception */ hasException: function() { return this.exception === true; }, /** * Returns the error string or object that was set using {@link #setException} * @return {String/Object} The error object */ getError: function() { return this.error; }, /** * Returns the {@link Ext.data.Model record}s associated with this operation. For read operations the records as set by the {@link Ext.data.proxy.Proxy Proxy} will be returned (returns `null` if the proxy has not yet set the records). * For create, update, and destroy operations the operation's initially configured records will be returned, although the proxy may modify these records' data at some point after the operation is initialized. * @return {Ext.data.Model[]} */ getRecords: function() { var resultSet = this.getResultSet(); return this.records || (resultSet ? resultSet.records : null); }, /** * Returns the ResultSet object (if set by the Proxy). This object will contain the {@link Ext.data.Model model} * instances as well as meta data such as number of instances fetched, number available etc * @return {Ext.data.ResultSet} The ResultSet object */ getResultSet: function() { return this.resultSet; }, /** * Returns true if the Operation has been started. Note that the Operation may have started AND completed, see * {@link #isRunning} to test if the Operation is currently running. * @return {Boolean} True if the Operation has started */ isStarted: function() { return this.started === true; }, /** * Returns true if the Operation has been started but has not yet completed. * @return {Boolean} True if the Operation is currently running */ isRunning: function() { return this.running === true; }, /** * Returns true if the Operation has been completed * @return {Boolean} True if the Operation is complete */ isComplete: function() { return this.complete === true; }, /** * Returns true if the Operation has completed and was successful * @return {Boolean} True if successful */ wasSuccessful: function() { return this.isComplete() && this.success === true; }, /** * @private * Associates this Operation with a Batch * @param {Ext.data.Batch} batch The batch */ setBatch: function(batch) { this.batch = batch; }, /** * Checks whether this operation should cause writing to occur. * @return {Boolean} Whether the operation should cause a write to occur. */ allowWrite: function() { return this.action != 'read'; } }); /** * @author Ed Spencer * * Simple class that represents a Request that will be made by any {@link Ext.data.proxy.Server} subclass. * All this class does is standardize the representation of a Request as used by any ServerProxy subclass, * it does not contain any actual logic or perform the request itself. */ Ext.define('Ext.data.Request', { /** * @cfg {String} action * The name of the action this Request represents. Usually one of 'create', 'read', 'update' or 'destroy'. */ action: undefined, /** * @cfg {Object} params * HTTP request params. The Proxy and its Writer have access to and can modify this object. */ params: undefined, /** * @cfg {String} method * The HTTP method to use on this Request. Should be one of 'GET', 'POST', 'PUT' or 'DELETE'. */ method: 'GET', /** * @cfg {String} url * The url to access on this Request */ url: undefined, /** * Creates the Request object. * @param {Object} [config] Config object. */ constructor: function(config) { Ext.apply(this, config); } }); /** * @author Ed Spencer * * Simple wrapper class that represents a set of records returned by a Proxy. */ Ext.define('Ext.data.ResultSet', { /** * @cfg {Boolean} loaded * True if the records have already been loaded. This is only meaningful when dealing with * SQL-backed proxies. */ loaded: true, /** * @cfg {Number} count * The number of records in this ResultSet. Note that total may differ from this number. */ count: 0, /** * @cfg {Number} total * The total number of records reported by the data source. This ResultSet may form a subset of * those records (see {@link #count}). */ total: 0, /** * @cfg {Boolean} success * True if the ResultSet loaded successfully, false if any errors were encountered. */ success: false, /** * @cfg {Ext.data.Model[]} records (required) * The array of record instances. */ /** * Creates the resultSet * @param {Object} [config] Config object. */ constructor: function(config) { Ext.apply(this, config); /** * @property {Number} totalRecords * Copy of this.total. * @deprecated Will be removed in Ext JS 5.0. Use {@link #total} instead. */ this.totalRecords = this.total; if (config.count === undefined) { this.count = this.records.length; } } }); /** * @author Don Griffin * * This class is a sequential id generator. A simple use of this class would be like so: * * Ext.define('MyApp.data.MyModel', { * extend: 'Ext.data.Model', * idgen: 'sequential' * }); * // assign id's of 1, 2, 3, etc. * * An example of a configured generator would be: * * Ext.define('MyApp.data.MyModel', { * extend: 'Ext.data.Model', * idgen: { * type: 'sequential', * prefix: 'ID_', * seed: 1000 * } * }); * // assign id's of ID_1000, ID_1001, ID_1002, etc. * */ Ext.define('Ext.data.SequentialIdGenerator', { extend: 'Ext.data.IdGenerator', alias: 'idgen.sequential', constructor: function() { var me = this; me.callParent(arguments); me.parts = [ me.prefix, '']; }, /** * @cfg {String} prefix * The string to place in front of the sequential number for each generated id. The * default is blank. */ prefix: '', /** * @cfg {Number} seed * The number at which to start generating sequential id's. The default is 1. */ seed: 1, /** * Generates and returns the next id. * @return {String} The next id. */ generate: function () { var me = this, parts = me.parts; parts[1] = me.seed++; return parts.join(''); } }); /** * @class Ext.data.SortTypes * This class defines a series of static methods that are used on a * {@link Ext.data.Field} for performing sorting. The methods cast the * underlying values into a data type that is appropriate for sorting on * that particular field. If a {@link Ext.data.Field#type} is specified, * the sortType will be set to a sane default if the sortType is not * explicitly defined on the field. The sortType will make any necessary * modifications to the value and return it. * *

* It is also possible to create a custom sortType that can be used throughout * an application. *


Ext.apply(Ext.data.SortTypes, {
    asPerson: function(person){
        // expects an object with a first and last name property
        return person.lastName.toUpperCase() + person.firstName.toLowerCase();
    }    
});

Ext.define('Employee', {
    extend: 'Ext.data.Model',
    fields: [{
        name: 'person',
        sortType: 'asPerson'
    }, {
        name: 'salary',
        type: 'float' // sortType set to asFloat
    }]
});
 * 
*

* @singleton * @docauthor Evan Trimboli */ Ext.define('Ext.data.SortTypes', { singleton: true, /** * Default sort that does nothing * @param {Object} s The value being converted * @return {Object} The comparison value */ none : function(s) { return s; }, /** * The regular expression used to strip tags * @type {RegExp} * @property */ stripTagsRE : /<\/?[^>]+>/gi, /** * Strips all HTML tags to sort on text only * @param {Object} s The value being converted * @return {String} The comparison value */ asText : function(s) { return String(s).replace(this.stripTagsRE, ""); }, /** * Strips all HTML tags to sort on text only - Case insensitive * @param {Object} s The value being converted * @return {String} The comparison value */ asUCText : function(s) { return String(s).toUpperCase().replace(this.stripTagsRE, ""); }, /** * Case insensitive string * @param {Object} s The value being converted * @return {String} The comparison value */ asUCString : function(s) { return String(s).toUpperCase(); }, /** * Date sorting * @param {Object} s The value being converted * @return {Number} The comparison value */ asDate : function(s) { if(!s){ return 0; } if(Ext.isDate(s)){ return s.getTime(); } return Date.parse(String(s)); }, /** * Float sorting * @param {Object} s The value being converted * @return {Number} The comparison value */ asFloat : function(s) { var val = parseFloat(String(s).replace(/,/g, "")); return isNaN(val) ? 0 : val; }, /** * Integer sorting * @param {Object} s The value being converted * @return {Number} The comparison value */ asInt : function(s) { var val = parseInt(String(s).replace(/,/g, ""), 10); return isNaN(val) ? 0 : val; } }); /** * @class Ext.data.Types *

This is a static class containing the system-supplied data types which may be given to a {@link Ext.data.Field Field}.

*

The properties in this class are used as type indicators in the {@link Ext.data.Field Field} class, so to * test whether a Field is of a certain type, compare the {@link Ext.data.Field#type type} property against properties * of this class.

*

Developers may add their own application-specific data types to this class. Definition names must be UPPERCASE. * each type definition must contain three properties:

*
*

For example, to create a VELatLong field (See the Microsoft Bing Mapping API) containing the latitude/longitude value of a datapoint on a map from a JsonReader data block * which contained the properties lat and long, you would define a new data type like this:

*

// Add a new Field data type which stores a VELatLong object in the Record.
Ext.data.Types.VELATLONG = {
    convert: function(v, data) {
        return new VELatLong(data.lat, data.long);
    },
    sortType: function(v) {
        return v.Latitude;  // When sorting, order by latitude
    },
    type: 'VELatLong'
};
*

Then, when declaring a Model, use:


var types = Ext.data.Types; // allow shorthand type access
Ext.define('Unit',
    extend: 'Ext.data.Model',
    fields: [
        { name: 'unitName', mapping: 'UnitName' },
        { name: 'curSpeed', mapping: 'CurSpeed', type: types.INT },
        { name: 'latitude', mapping: 'lat', type: types.FLOAT },
        { name: 'longitude', mapping: 'long', type: types.FLOAT },
        { name: 'position', type: types.VELATLONG }
    ]
});
* @singleton */ Ext.define('Ext.data.Types', { singleton: true, requires: ['Ext.data.SortTypes'] }, function() { var st = Ext.data.SortTypes; Ext.apply(Ext.data.Types, { /** * @property {RegExp} stripRe * A regular expression for stripping non-numeric characters from a numeric value. Defaults to /[\$,%]/g. * This should be overridden for localization. */ stripRe: /[\$,%]/g, /** * @property {Object} AUTO * This data type means that no conversion is applied to the raw data before it is placed into a Record. */ AUTO: { sortType: st.none, type: 'auto' }, /** * @property {Object} STRING * This data type means that the raw data is converted into a String before it is placed into a Record. */ STRING: { convert: function(v) { var defaultValue = this.useNull ? null : ''; return (v === undefined || v === null) ? defaultValue : String(v); }, sortType: st.asUCString, type: 'string' }, /** * @property {Object} INT * This data type means that the raw data is converted into an integer before it is placed into a Record. *

The synonym INTEGER is equivalent.

*/ INT: { convert: function(v) { return v !== undefined && v !== null && v !== '' ? parseInt(String(v).replace(Ext.data.Types.stripRe, ''), 10) : (this.useNull ? null : 0); }, sortType: st.none, type: 'int' }, /** * @property {Object} FLOAT * This data type means that the raw data is converted into a number before it is placed into a Record. *

The synonym NUMBER is equivalent.

*/ FLOAT: { convert: function(v) { return v !== undefined && v !== null && v !== '' ? parseFloat(String(v).replace(Ext.data.Types.stripRe, ''), 10) : (this.useNull ? null : 0); }, sortType: st.none, type: 'float' }, /** * @property {Object} BOOL *

This data type means that the raw data is converted into a boolean before it is placed into * a Record. The string "true" and the number 1 are converted to boolean true.

*

The synonym BOOLEAN is equivalent.

*/ BOOL: { convert: function(v) { if (this.useNull && (v === undefined || v === null || v === '')) { return null; } return v === true || v === 'true' || v == 1; }, sortType: st.none, type: 'bool' }, /** * @property {Object} DATE * This data type means that the raw data is converted into a Date before it is placed into a Record. * The date format is specified in the constructor of the {@link Ext.data.Field} to which this type is * being applied. */ DATE: { convert: function(v) { var df = this.dateFormat, parsed; if (!v) { return null; } if (Ext.isDate(v)) { return v; } if (df) { if (df == 'timestamp') { return new Date(v*1000); } if (df == 'time') { return new Date(parseInt(v, 10)); } return Ext.Date.parse(v, df); } parsed = Date.parse(v); return parsed ? new Date(parsed) : null; }, sortType: st.asDate, type: 'date' } }); Ext.apply(Ext.data.Types, { /** * @property {Object} BOOLEAN *

This data type means that the raw data is converted into a boolean before it is placed into * a Record. The string "true" and the number 1 are converted to boolean true.

*

The synonym BOOL is equivalent.

*/ BOOLEAN: this.BOOL, /** * @property {Object} INTEGER * This data type means that the raw data is converted into an integer before it is placed into a Record. *

The synonym INT is equivalent.

*/ INTEGER: this.INT, /** * @property {Object} NUMBER * This data type means that the raw data is converted into a number before it is placed into a Record. *

The synonym FLOAT is equivalent.

*/ NUMBER: this.FLOAT }); }); /** * @extend Ext.data.IdGenerator * @author Don Griffin * * This class generates UUID's according to RFC 4122. This class has a default id property. * This means that a single instance is shared unless the id property is overridden. Thus, * two {@link Ext.data.Model} instances configured like the following share one generator: * * Ext.define('MyApp.data.MyModelX', { * extend: 'Ext.data.Model', * idgen: 'uuid' * }); * * Ext.define('MyApp.data.MyModelY', { * extend: 'Ext.data.Model', * idgen: 'uuid' * }); * * This allows all models using this class to share a commonly configured instance. * * # Using Version 1 ("Sequential") UUID's * * If a server can provide a proper timestamp and a "cryptographic quality random number" * (as described in RFC 4122), the shared instance can be configured as follows: * * Ext.data.IdGenerator.get('uuid').reconfigure({ * version: 1, * clockSeq: clock, // 14 random bits * salt: salt, // 48 secure random bits (the Node field) * timestamp: ts // timestamp per Section 4.1.4 * }); * * // or these values can be split into 32-bit chunks: * * Ext.data.IdGenerator.get('uuid').reconfigure({ * version: 1, * clockSeq: clock, * salt: { lo: saltLow32, hi: saltHigh32 }, * timestamp: { lo: timestampLow32, hi: timestamptHigh32 } * }); * * This approach improves the generator's uniqueness by providing a valid timestamp and * higher quality random data. Version 1 UUID's should not be used unless this information * can be provided by a server and care should be taken to avoid caching of this data. * * See http://www.ietf.org/rfc/rfc4122.txt for details. */ Ext.define('Ext.data.UuidGenerator', (function () { var twoPow14 = Math.pow(2, 14), twoPow16 = Math.pow(2, 16), twoPow28 = Math.pow(2, 28), twoPow32 = Math.pow(2, 32); function toHex (value, length) { var ret = value.toString(16); if (ret.length > length) { ret = ret.substring(ret.length - length); // right-most digits } else if (ret.length < length) { ret = Ext.String.leftPad(ret, length, '0'); } return ret; } function rand (lo, hi) { var v = Math.random() * (hi - lo + 1); return Math.floor(v) + lo; } function split (bignum) { if (typeof(bignum) == 'number') { var hi = Math.floor(bignum / twoPow32); return { lo: Math.floor(bignum - hi * twoPow32), hi: hi }; } return bignum; } return { extend: 'Ext.data.IdGenerator', alias: 'idgen.uuid', id: 'uuid', // shared by default /** * @property {Number/Object} salt * When created, this value is a 48-bit number. For computation, this value is split * into 32-bit parts and stored in an object with `hi` and `lo` properties. */ /** * @property {Number/Object} timestamp * When created, this value is a 60-bit number. For computation, this value is split * into 32-bit parts and stored in an object with `hi` and `lo` properties. */ /** * @cfg {Number} version * The Version of UUID. Supported values are: * * * 1 : Time-based, "sequential" UUID. * * 4 : Pseudo-random UUID. * * The default is 4. */ version: 4, constructor: function() { var me = this; me.callParent(arguments); me.parts = []; me.init(); }, generate: function () { var me = this, parts = me.parts, ts = me.timestamp; /* The magic decoder ring (derived from RFC 4122 Section 4.2.2): +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | time_low | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | time_mid | ver | time_hi | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |res| clock_hi | clock_low | salt 0 |M| salt 1 | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | salt (2-5) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ time_mid clock_hi (low 6 bits) time_low | time_hi |clock_lo | | | || salt[0] | | | || | salt[1..5] v v v vv v v 0badf00d-aced-1def-b123-dfad0badbeef ^ ^ ^ version | multicast (low bit) | reserved (upper 2 bits) */ parts[0] = toHex(ts.lo, 8); parts[1] = toHex(ts.hi & 0xFFFF, 4); parts[2] = toHex(((ts.hi >>> 16) & 0xFFF) | (me.version << 12), 4); parts[3] = toHex(0x80 | ((me.clockSeq >>> 8) & 0x3F), 2) + toHex(me.clockSeq & 0xFF, 2); parts[4] = toHex(me.salt.hi, 4) + toHex(me.salt.lo, 8); if (me.version == 4) { me.init(); // just regenerate all the random values... } else { // sequentially increment the timestamp... ++ts.lo; if (ts.lo >= twoPow32) { // if (overflow) ts.lo = 0; ++ts.hi; } } return parts.join('-').toLowerCase(); }, getRecId: function (rec) { return rec.getId(); }, /** * @private */ init: function () { var me = this, salt, time; if (me.version == 4) { // See RFC 4122 (Secion 4.4) // o If the state was unavailable (e.g., non-existent or corrupted), // or the saved node ID is different than the current node ID, // generate a random clock sequence value. me.clockSeq = rand(0, twoPow14-1); // we run this on every id generation... salt = me.salt || (me.salt = {}); time = me.timestamp || (me.timestamp = {}); // See RFC 4122 (Secion 4.4) salt.lo = rand(0, twoPow32-1); salt.hi = rand(0, twoPow16-1); time.lo = rand(0, twoPow32-1); time.hi = rand(0, twoPow28-1); } else { // this is run only once per-instance me.salt = split(me.salt); me.timestamp = split(me.timestamp); // Set multicast bit: "the least significant bit of the first octet of the // node ID" (nodeId = salt for this implementation): me.salt.hi |= 0x100; } }, /** * Reconfigures this generator given new config properties. */ reconfigure: function (config) { Ext.apply(this, config); this.init(); } }; }())); /** * @author Ed Spencer * * This singleton contains a set of validation functions that can be used to validate any type of data. They are most * often used in {@link Ext.data.Model Models}, where they are automatically set up and executed. */ Ext.define('Ext.data.validations', { singleton: true, /** * @property {String} presenceMessage * The default error message used when a presence validation fails. */ presenceMessage: 'must be present', /** * @property {String} lengthMessage * The default error message used when a length validation fails. */ lengthMessage: 'is the wrong length', /** * @property {String} formatMessage * The default error message used when a format validation fails. */ formatMessage: 'is the wrong format', /** * @property {String} inclusionMessage * The default error message used when an inclusion validation fails. */ inclusionMessage: 'is not included in the list of acceptable values', /** * @property {String} exclusionMessage * The default error message used when an exclusion validation fails. */ exclusionMessage: 'is not an acceptable value', /** * @property {String} emailMessage * The default error message used when an email validation fails */ emailMessage: 'is not a valid email address', /** * @property {RegExp} emailRe * The regular expression used to validate email addresses */ emailRe: /^([a-zA-Z0-9_\.\-])+\@(([a-zA-Z0-9\-])+\.)+([a-zA-Z0-9]{2,4})+$/, /** * Validates that the given value is present. * For example: * * validations: [{type: 'presence', field: 'age'}] * * @param {Object} config Config object * @param {Object} value The value to validate * @return {Boolean} True if validation passed */ presence: function(config, value) { // No configs read, so allow just value to be passed if (arguments.length === 1) { value = config; } //we need an additional check for zero here because zero is an acceptable form of present data return !!value || value === 0; }, /** * Returns true if the given value is between the configured min and max values. * For example: * * validations: [{type: 'length', field: 'name', min: 2}] * * @param {Object} config Config object * @param {String} value The value to validate * @return {Boolean} True if the value passes validation */ length: function(config, value) { if (value === undefined || value === null) { return false; } var length = value.length, min = config.min, max = config.max; if ((min && length < min) || (max && length > max)) { return false; } else { return true; } }, /** * Validates that an email string is in the correct format * @param {Object} config Config object * @param {String} email The email address * @return {Boolean} True if the value passes validation */ email: function(config, email) { return Ext.data.validations.emailRe.test(email); }, /** * Returns true if the given value passes validation against the configured `matcher` regex. * For example: * * validations: [{type: 'format', field: 'username', matcher: /([a-z]+)[0-9]{2,3}/}] * * @param {Object} config Config object * @param {String} value The value to validate * @return {Boolean} True if the value passes the format validation */ format: function(config, value) { return !!(config.matcher && config.matcher.test(value)); }, /** * Validates that the given value is present in the configured `list`. * For example: * * validations: [{type: 'inclusion', field: 'gender', list: ['Male', 'Female']}] * * @param {Object} config Config object * @param {String} value The value to validate * @return {Boolean} True if the value is present in the list */ inclusion: function(config, value) { return config.list && Ext.Array.indexOf(config.list,value) != -1; }, /** * Validates that the given value is not present in the configured `list`. * For example: * * validations: [{type: 'exclusion', field: 'username', list: ['Admin', 'Operator']}] * * @param {Object} config Config object * @param {String} value The value to validate * @return {Boolean} True if the value is not present in the list */ exclusion: function(config, value) { return config.list && Ext.Array.indexOf(config.list,value) == -1; } }); /** * @author Ed Spencer * * Associations enable you to express relationships between different {@link Ext.data.Model Models}. Let's say we're * writing an ecommerce system where Users can make Orders - there's a relationship between these Models that we can * express like this: * * Ext.define('User', { * extend: 'Ext.data.Model', * fields: ['id', 'name', 'email'], * * hasMany: {model: 'Order', name: 'orders'} * }); * * Ext.define('Order', { * extend: 'Ext.data.Model', * fields: ['id', 'user_id', 'status', 'price'], * * belongsTo: 'User' * }); * * We've set up two models - User and Order - and told them about each other. You can set up as many associations on * each Model as you need using the two default types - {@link Ext.data.HasManyAssociation hasMany} and {@link * Ext.data.BelongsToAssociation belongsTo}. There's much more detail on the usage of each of those inside their * documentation pages. If you're not familiar with Models already, {@link Ext.data.Model there is plenty on those too}. * * **Further Reading** * * - {@link Ext.data.association.HasMany hasMany associations} * - {@link Ext.data.association.BelongsTo belongsTo associations} * - {@link Ext.data.association.HasOne hasOne associations} * - {@link Ext.data.Model using Models} * * # Self association models * * We can also have models that create parent/child associations between the same type. Below is an example, where * groups can be nested inside other groups: * * // Server Data * { * "groups": { * "id": 10, * "parent_id": 100, * "name": "Main Group", * "parent_group": { * "id": 100, * "parent_id": null, * "name": "Parent Group" * }, * "child_groups": [{ * "id": 2, * "parent_id": 10, * "name": "Child Group 1" * },{ * "id": 3, * "parent_id": 10, * "name": "Child Group 2" * },{ * "id": 4, * "parent_id": 10, * "name": "Child Group 3" * }] * } * } * * // Client code * Ext.define('Group', { * extend: 'Ext.data.Model', * fields: ['id', 'parent_id', 'name'], * proxy: { * type: 'ajax', * url: 'data.json', * reader: { * type: 'json', * root: 'groups' * } * }, * associations: [{ * type: 'hasMany', * model: 'Group', * primaryKey: 'id', * foreignKey: 'parent_id', * autoLoad: true, * associationKey: 'child_groups' // read child data from child_groups * }, { * type: 'belongsTo', * model: 'Group', * primaryKey: 'id', * foreignKey: 'parent_id', * associationKey: 'parent_group' // read parent data from parent_group * }] * }); * * Ext.onReady(function(){ * * Group.load(10, { * success: function(group){ * console.log(group.getGroup().get('name')); * * group.groups().each(function(rec){ * console.log(rec.get('name')); * }); * } * }); * * }); * */ Ext.define('Ext.data.association.Association', { alternateClassName: 'Ext.data.Association', /** * @cfg {String} ownerModel * The string name of the model that owns the association. * * **NB!** This config is required when instantiating the Association directly. * However, it cannot be used at all when defining the association as a config * object inside Model, because the name of the model itself will be supplied * automatically as the value of this config. */ /** * @cfg {String} associatedModel * The string name of the model that is being associated with. * * **NB!** This config is required when instantiating the Association directly. * When defining the association as a config object inside Model, the #model * configuration will shadow this config. */ /** * @cfg {String} model * The string name of the model that is being associated with. * * This config option is to be used when defining the association as a config * object within Model. The value is then mapped to #associatedModel when * Association is instantiated inside Model. */ /** * @cfg {String} primaryKey * The name of the primary key on the associated model. In general this will be the * {@link Ext.data.Model#idProperty} of the Model. */ primaryKey: 'id', /** * @cfg {Ext.data.reader.Reader} reader * A special reader to read associated data */ /** * @cfg {String} associationKey * The name of the property in the data to read the association from. Defaults to the name of the associated model. */ defaultReaderType: 'json', isAssociation: true, initialConfig: null, statics: { AUTO_ID: 1000, create: function(association){ if (Ext.isString(association)) { association = { type: association }; } switch (association.type) { case 'belongsTo': return new Ext.data.association.BelongsTo(association); case 'hasMany': return new Ext.data.association.HasMany(association); case 'hasOne': return new Ext.data.association.HasOne(association); //TODO Add this back when it's fixed // case 'polymorphic': // return Ext.create('Ext.data.PolymorphicAssociation', association); default: Ext.Error.raise('Unknown Association type: "' + association.type + '"'); } return association; } }, /** * Creates the Association object. * @param {Object} [config] Config object. */ constructor: function(config) { Ext.apply(this, config); var me = this, types = Ext.ModelManager.types, ownerName = config.ownerModel, associatedName = config.associatedModel, ownerModel = types[ownerName], associatedModel = types[associatedName]; me.initialConfig = config; if (ownerModel === undefined) { Ext.Error.raise("The configured ownerModel was not valid (you tried " + ownerName + ")"); } if (associatedModel === undefined) { Ext.Error.raise("The configured associatedModel was not valid (you tried " + associatedName + ")"); } me.ownerModel = ownerModel; me.associatedModel = associatedModel; /** * @property {String} ownerName * The name of the model that 'owns' the association */ /** * @property {String} associatedName * The name of the model is on the other end of the association (e.g. if a User model hasMany Orders, this is * 'Order') */ Ext.applyIf(me, { ownerName : ownerName, associatedName: associatedName }); me.associationId = 'association' + (++me.statics().AUTO_ID); }, /** * Get a specialized reader for reading associated data * @return {Ext.data.reader.Reader} The reader, null if not supplied */ getReader: function(){ var me = this, reader = me.reader, model = me.associatedModel; if (reader) { if (Ext.isString(reader)) { reader = { type: reader }; } if (reader.isReader) { reader.setModel(model); } else { Ext.applyIf(reader, { model: model, type : me.defaultReaderType }); } me.reader = Ext.createByAlias('reader.' + reader.type, reader); } return me.reader || null; } }); /** * @author Ed Spencer * @class Ext.data.association.BelongsTo * * Represents a many to one association with another model. The owner model is expected to have * a foreign key which references the primary key of the associated model: * * Ext.define('Category', { * extend: 'Ext.data.Model', * fields: [ * { name: 'id', type: 'int' }, * { name: 'name', type: 'string' } * ] * }); * * Ext.define('Product', { * extend: 'Ext.data.Model', * fields: [ * { name: 'id', type: 'int' }, * { name: 'category_id', type: 'int' }, * { name: 'name', type: 'string' } * ], * // we can use the belongsTo shortcut on the model to create a belongsTo association * associations: [ * { type: 'belongsTo', model: 'Category' } * ] * }); * * In the example above we have created models for Products and Categories, and linked them together * by saying that each Product belongs to a Category. This automatically links each Product to a Category * based on the Product's category_id, and provides new functions on the Product model: * * ## Generated getter function * * The first function that is added to the owner model is a getter function: * * var product = new Product({ * id: 100, * category_id: 20, * name: 'Sneakers' * }); * * product.getCategory(function(category, operation) { * // do something with the category object * alert(category.get('id')); // alerts 20 * }, this); * * The getCategory function was created on the Product model when we defined the association. This uses the * Category's configured {@link Ext.data.proxy.Proxy proxy} to load the Category asynchronously, calling the provided * callback when it has loaded. * * The new getCategory function will also accept an object containing success, failure and callback properties * - callback will always be called, success will only be called if the associated model was loaded successfully * and failure will only be called if the associatied model could not be loaded: * * product.getCategory({ * reload: true, // force a reload if the owner model is already cached * callback: function(category, operation) {}, // a function that will always be called * success : function(category, operation) {}, // a function that will only be called if the load succeeded * failure : function(category, operation) {}, // a function that will only be called if the load did not succeed * scope : this // optionally pass in a scope object to execute the callbacks in * }); * * In each case above the callbacks are called with two arguments - the associated model instance and the * {@link Ext.data.Operation operation} object that was executed to load that instance. The Operation object is * useful when the instance could not be loaded. * * Once the getter has been called on the model, it will be cached if the getter is called a second time. To * force the model to reload, specify reload: true in the options object. * * ## Generated setter function * * The second generated function sets the associated model instance - if only a single argument is passed to * the setter then the following two calls are identical: * * // this call... * product.setCategory(10); * * // is equivalent to this call: * product.set('category_id', 10); * * An instance of the owner model can also be passed as a parameter. * * If we pass in a second argument, the model will be automatically saved and the second argument passed to * the owner model's {@link Ext.data.Model#save save} method: * * product.setCategory(10, function(product, operation) { * // the product has been saved * alert(product.get('category_id')); //now alerts 10 * }); * * //alternative syntax: * product.setCategory(10, { * callback: function(product, operation), // a function that will always be called * success : function(product, operation), // a function that will only be called if the load succeeded * failure : function(product, operation), // a function that will only be called if the load did not succeed * scope : this //optionally pass in a scope object to execute the callbacks in * }) * * ## Customisation * * Associations reflect on the models they are linking to automatically set up properties such as the * {@link #primaryKey} and {@link #foreignKey}. These can alternatively be specified: * * Ext.define('Product', { * fields: [...], * * associations: [ * { type: 'belongsTo', model: 'Category', primaryKey: 'unique_id', foreignKey: 'cat_id' } * ] * }); * * Here we replaced the default primary key (defaults to 'id') and foreign key (calculated as 'category_id') * with our own settings. Usually this will not be needed. */ Ext.define('Ext.data.association.BelongsTo', { extend: 'Ext.data.association.Association', alternateClassName: 'Ext.data.BelongsToAssociation', alias: 'association.belongsto', /** * @cfg {String} foreignKey The name of the foreign key on the owner model that links it to the associated * model. Defaults to the lowercased name of the associated model plus "_id", e.g. an association with a * model called Product would set up a product_id foreign key. * * Ext.define('Order', { * extend: 'Ext.data.Model', * fields: ['id', 'date'], * hasMany: 'Product' * }); * * Ext.define('Product', { * extend: 'Ext.data.Model', * fields: ['id', 'name', 'order_id'], // refers to the id of the order that this product belongs to * belongsTo: 'Order' * }); * var product = new Product({ * id: 1, * name: 'Product 1', * order_id: 22 * }, 1); * product.getOrder(); // Will make a call to the server asking for order_id 22 * */ /** * @cfg {String} getterName The name of the getter function that will be added to the local model's prototype. * Defaults to 'get' + the name of the foreign model, e.g. getCategory */ /** * @cfg {String} setterName The name of the setter function that will be added to the local model's prototype. * Defaults to 'set' + the name of the foreign model, e.g. setCategory */ /** * @cfg {String} type The type configuration can be used when creating associations using a configuration object. * Use 'belongsTo' to create a BelongsTo association. * * associations: [{ * type: 'belongsTo', * model: 'User' * }] */ constructor: function(config) { this.callParent(arguments); var me = this, ownerProto = me.ownerModel.prototype, associatedName = me.associatedName, getterName = me.getterName || 'get' + associatedName, setterName = me.setterName || 'set' + associatedName; Ext.applyIf(me, { name : associatedName, foreignKey : associatedName.toLowerCase() + "_id", instanceName: associatedName + 'BelongsToInstance', associationKey: associatedName.toLowerCase() }); ownerProto[getterName] = me.createGetter(); ownerProto[setterName] = me.createSetter(); }, /** * @private * Returns a setter function to be placed on the owner model's prototype * @return {Function} The setter function */ createSetter: function() { var me = this, foreignKey = me.foreignKey; //'this' refers to the Model instance inside this function return function(value, options, scope) { // If we pass in an instance, pull the id out if (value && value.isModel) { value = value.getId(); } this.set(foreignKey, value); if (Ext.isFunction(options)) { options = { callback: options, scope: scope || this }; } if (Ext.isObject(options)) { return this.save(options); } }; }, /** * @private * Returns a getter function to be placed on the owner model's prototype. We cache the loaded instance * the first time it is loaded so that subsequent calls to the getter always receive the same reference. * @return {Function} The getter function */ createGetter: function() { var me = this, associatedName = me.associatedName, associatedModel = me.associatedModel, foreignKey = me.foreignKey, primaryKey = me.primaryKey, instanceName = me.instanceName; //'this' refers to the Model instance inside this function return function(options, scope) { options = options || {}; var model = this, foreignKeyId = model.get(foreignKey), success, instance, args; if (options.reload === true || model[instanceName] === undefined) { instance = Ext.ModelManager.create({}, associatedName); instance.set(primaryKey, foreignKeyId); if (typeof options == 'function') { options = { callback: options, scope: scope || model }; } // Overwrite the success handler so we can assign the current instance success = options.success; options.success = function(rec){ model[instanceName] = rec; if (success) { success.apply(this, arguments); } }; associatedModel.load(foreignKeyId, options); // assign temporarily while we wait for data to return model[instanceName] = instance; return instance; } else { instance = model[instanceName]; args = [instance]; scope = scope || options.scope || model; //TODO: We're duplicating the callback invokation code that the instance.load() call above //makes here - ought to be able to normalize this - perhaps by caching at the Model.load layer //instead of the association layer. Ext.callback(options, scope, args); Ext.callback(options.success, scope, args); Ext.callback(options.failure, scope, args); Ext.callback(options.callback, scope, args); return instance; } }; }, /** * Read associated data * @private * @param {Ext.data.Model} record The record we're writing to * @param {Ext.data.reader.Reader} reader The reader for the associated model * @param {Object} associationData The raw associated data */ read: function(record, reader, associationData){ record[this.instanceName] = reader.read([associationData]).records[0]; } }); /** * @class Ext.data.association.HasOne * * Represents a one to one association with another model. The owner model is expected to have * a foreign key which references the primary key of the associated model: * * Ext.define('Address', { * extend: 'Ext.data.Model', * fields: [ * { name: 'id', type: 'int' }, * { name: 'number', type: 'string' }, * { name: 'street', type: 'string' }, * { name: 'city', type: 'string' }, * { name: 'zip', type: 'string' }, * ] * }); * * Ext.define('Person', { * extend: 'Ext.data.Model', * fields: [ * { name: 'id', type: 'int' }, * { name: 'name', type: 'string' }, * { name: 'address_id', type: 'int'} * ], * // we can use the hasOne shortcut on the model to create a hasOne association * associations: [{ type: 'hasOne', model: 'Address' }] * }); * * In the example above we have created models for People and Addresses, and linked them together * by saying that each Person has a single Address. This automatically links each Person to an Address * based on the Persons address_id, and provides new functions on the Person model: * * ## Generated getter function * * The first function that is added to the owner model is a getter function: * * var person = new Person({ * id: 100, * address_id: 20, * name: 'John Smith' * }); * * person.getAddress(function(address, operation) { * // do something with the address object * alert(address.get('id')); // alerts 20 * }, this); * * The getAddress function was created on the Person model when we defined the association. This uses the * Persons configured {@link Ext.data.proxy.Proxy proxy} to load the Address asynchronously, calling the provided * callback when it has loaded. * * The new getAddress function will also accept an object containing success, failure and callback properties * - callback will always be called, success will only be called if the associated model was loaded successfully * and failure will only be called if the associatied model could not be loaded: * * person.getAddress({ * reload: true, // force a reload if the owner model is already cached * callback: function(address, operation) {}, // a function that will always be called * success : function(address, operation) {}, // a function that will only be called if the load succeeded * failure : function(address, operation) {}, // a function that will only be called if the load did not succeed * scope : this // optionally pass in a scope object to execute the callbacks in * }); * * In each case above the callbacks are called with two arguments - the associated model instance and the * {@link Ext.data.Operation operation} object that was executed to load that instance. The Operation object is * useful when the instance could not be loaded. * * Once the getter has been called on the model, it will be cached if the getter is called a second time. To * force the model to reload, specify reload: true in the options object. * * ## Generated setter function * * The second generated function sets the associated model instance - if only a single argument is passed to * the setter then the following two calls are identical: * * // this call... * person.setAddress(10); * * // is equivalent to this call: * person.set('address_id', 10); * * An instance of the owner model can also be passed as a parameter. * * If we pass in a second argument, the model will be automatically saved and the second argument passed to * the owner model's {@link Ext.data.Model#save save} method: * * person.setAddress(10, function(address, operation) { * // the address has been saved * alert(address.get('address_id')); //now alerts 10 * }); * * //alternative syntax: * person.setAddress(10, { * callback: function(address, operation), // a function that will always be called * success : function(address, operation), // a function that will only be called if the load succeeded * failure : function(address, operation), // a function that will only be called if the load did not succeed * scope : this //optionally pass in a scope object to execute the callbacks in * }) * * ## Customisation * * Associations reflect on the models they are linking to automatically set up properties such as the * {@link #primaryKey} and {@link #foreignKey}. These can alternatively be specified: * * Ext.define('Person', { * fields: [...], * * associations: [ * { type: 'hasOne', model: 'Address', primaryKey: 'unique_id', foreignKey: 'addr_id' } * ] * }); * * Here we replaced the default primary key (defaults to 'id') and foreign key (calculated as 'address_id') * with our own settings. Usually this will not be needed. */ Ext.define('Ext.data.association.HasOne', { extend: 'Ext.data.association.Association', alternateClassName: 'Ext.data.HasOneAssociation', alias: 'association.hasone', /** * @cfg {String} foreignKey The name of the foreign key on the owner model that links it to the associated * model. Defaults to the lowercased name of the associated model plus "_id", e.g. an association with a * model called Person would set up a address_id foreign key. * * Ext.define('Person', { * extend: 'Ext.data.Model', * fields: ['id', 'name', 'address_id'], // refers to the id of the address object * hasOne: 'Address' * }); * * Ext.define('Address', { * extend: 'Ext.data.Model', * fields: ['id', 'number', 'street', 'city', 'zip'], * belongsTo: 'Person' * }); * var Person = new Person({ * id: 1, * name: 'John Smith', * address_id: 13 * }, 1); * person.getAddress(); // Will make a call to the server asking for address_id 13 * */ /** * @cfg {String} getterName The name of the getter function that will be added to the local model's prototype. * Defaults to 'get' + the name of the foreign model, e.g. getAddress */ /** * @cfg {String} setterName The name of the setter function that will be added to the local model's prototype. * Defaults to 'set' + the name of the foreign model, e.g. setAddress */ /** * @cfg {String} type The type configuration can be used when creating associations using a configuration object. * Use 'hasOne' to create a HasOne association. * * associations: [{ * type: 'hasOne', * model: 'Address' * }] */ constructor: function(config) { this.callParent(arguments); var me = this, ownerProto = me.ownerModel.prototype, associatedName = me.associatedName, getterName = me.getterName || 'get' + associatedName, setterName = me.setterName || 'set' + associatedName; Ext.applyIf(me, { name : associatedName, foreignKey : associatedName.toLowerCase() + "_id", instanceName: associatedName + 'HasOneInstance', associationKey: associatedName.toLowerCase() }); ownerProto[getterName] = me.createGetter(); ownerProto[setterName] = me.createSetter(); }, /** * @private * Returns a setter function to be placed on the owner model's prototype * @return {Function} The setter function */ createSetter: function() { var me = this, ownerModel = me.ownerModel, foreignKey = me.foreignKey; //'this' refers to the Model instance inside this function return function(value, options, scope) { if (value && value.isModel) { value = value.getId(); } this.set(foreignKey, value); if (Ext.isFunction(options)) { options = { callback: options, scope: scope || this }; } if (Ext.isObject(options)) { return this.save(options); } }; }, /** * @private * Returns a getter function to be placed on the owner model's prototype. We cache the loaded instance * the first time it is loaded so that subsequent calls to the getter always receive the same reference. * @return {Function} The getter function */ createGetter: function() { var me = this, ownerModel = me.ownerModel, associatedName = me.associatedName, associatedModel = me.associatedModel, foreignKey = me.foreignKey, primaryKey = me.primaryKey, instanceName = me.instanceName; //'this' refers to the Model instance inside this function return function(options, scope) { options = options || {}; var model = this, foreignKeyId = model.get(foreignKey), success, instance, args; if (options.reload === true || model[instanceName] === undefined) { instance = Ext.ModelManager.create({}, associatedName); instance.set(primaryKey, foreignKeyId); if (typeof options == 'function') { options = { callback: options, scope: scope || model }; } // Overwrite the success handler so we can assign the current instance success = options.success; options.success = function(rec){ model[instanceName] = rec; if (success) { success.apply(this, arguments); } }; associatedModel.load(foreignKeyId, options); // assign temporarily while we wait for data to return model[instanceName] = instance; return instance; } else { instance = model[instanceName]; args = [instance]; scope = scope || options.scope || model; //TODO: We're duplicating the callback invokation code that the instance.load() call above //makes here - ought to be able to normalize this - perhaps by caching at the Model.load layer //instead of the association layer. Ext.callback(options, scope, args); Ext.callback(options.success, scope, args); Ext.callback(options.failure, scope, args); Ext.callback(options.callback, scope, args); return instance; } }; }, /** * Read associated data * @private * @param {Ext.data.Model} record The record we're writing to * @param {Ext.data.reader.Reader} reader The reader for the associated model * @param {Object} associationData The raw associated data */ read: function(record, reader, associationData){ var inverse = this.associatedModel.prototype.associations.findBy(function(assoc){ return assoc.type === 'belongsTo' && assoc.associatedName === record.$className; }), newRecord = reader.read([associationData]).records[0]; record[this.instanceName] = newRecord; //if the inverse association was found, set it now on each record we've just created if (inverse) { newRecord[inverse.instanceName] = record; } } }); /** * @author Ed Spencer * * Base Writer class used by most subclasses of {@link Ext.data.proxy.Server}. This class is responsible for taking a * set of {@link Ext.data.Operation} objects and a {@link Ext.data.Request} object and modifying that request based on * the Operations. * * For example a Ext.data.writer.Json would format the Operations and their {@link Ext.data.Model} instances based on * the config options passed to the JsonWriter's constructor. * * Writers are not needed for any kind of local storage - whether via a {@link Ext.data.proxy.WebStorage Web Storage * proxy} (see {@link Ext.data.proxy.LocalStorage localStorage} and {@link Ext.data.proxy.SessionStorage * sessionStorage}) or just in memory via a {@link Ext.data.proxy.Memory MemoryProxy}. */ Ext.define('Ext.data.writer.Writer', { alias: 'writer.base', alternateClassName: ['Ext.data.DataWriter', 'Ext.data.Writer'], /** * @cfg {Boolean} writeAllFields * True to write all fields from the record to the server. If set to false it will only send the fields that were * modified. Note that any fields that have {@link Ext.data.Field#persist} set to false will still be ignored. */ writeAllFields: true, /** * @cfg {String} nameProperty * This property is used to read the key for each value that will be sent to the server. For example: * * Ext.define('Person', { * extend: 'Ext.data.Model', * fields: [{ * name: 'first', * mapping: 'firstName' * }, { * name: 'last', * mapping: 'lastName' * }, { * name: 'age' * }] * }); * new Ext.data.writer.Writer({ * writeAllFields: true, * nameProperty: 'mapping' * }); * * // This will be sent to the server * { * firstName: 'first name value', * lastName: 'last name value', * age: 1 * } * * If the value is not present, the field name will always be used. */ nameProperty: 'name', /* * @property {Boolean} isWriter * `true` in this class to identify an object as an instantiated Writer, or subclass thereof. */ isWriter: true, /** * Creates new Writer. * @param {Object} [config] Config object. */ constructor: function(config) { Ext.apply(this, config); }, /** * Prepares a Proxy's Ext.data.Request object * @param {Ext.data.Request} request The request object * @return {Ext.data.Request} The modified request object */ write: function(request) { var operation = request.operation, records = operation.records || [], len = records.length, i = 0, data = []; for (; i < len; i++) { data.push(this.getRecordData(records[i], operation)); } return this.writeRecords(request, data); }, /** * Formats the data for each record before sending it to the server. This * method should be overridden to format the data in a way that differs from the default. * @param {Ext.data.Model} record The record that we are writing to the server. * @param {Ext.data.Operation} [operation] An operation object. * @return {Object} An object literal of name/value keys to be written to the server. * By default this method returns the data property on the record. */ getRecordData: function(record, operation) { var isPhantom = record.phantom === true, writeAll = this.writeAllFields || isPhantom, nameProperty = this.nameProperty, fields = record.fields, fieldItems = fields.items, data = {}, clientIdProperty = record.clientIdProperty, changes, name, field, key, value, f, fLen; if (writeAll) { fLen = fieldItems.length; for (f = 0; f < fLen; f++) { field = fieldItems[f]; if (field.persist) { name = field[nameProperty] || field.name; value = record.get(field.name); if (field.serialize) { data[name] = field.serialize(value, record); } else if (field.type === Ext.data.Types.DATE && field.dateFormat) { data[name] = Ext.Date.format(value, field.dateFormat); } else { data[name] = value; } } } } else { // Only write the changes changes = record.getChanges(); for (key in changes) { if (changes.hasOwnProperty(key)) { field = fields.get(key); if (field.persist) { name = field[nameProperty] || field.name; value = record.get(field.name); if (field.serialize) { data[name] = field.serialize(value, record); } else if (field.type === Ext.data.Types.DATE && field.dateFormat) { data[name] = Ext.Date.format(value, field.dateFormat); } else { data[name] = value; } } } } } if (isPhantom) { if (clientIdProperty && operation && operation.records.length > 1) { // include clientId for phantom records, if multiple records are being written to the server in one operation. // The server can then return the clientId with each record so the operation can match the server records with the client records data[clientIdProperty] = record.internalId; } } else { // always include the id for non phantoms data[record.idProperty] = record.getId(); } return data; } }); /** * @author Ed Spencer * @class Ext.data.writer.Xml This class is used to write {@link Ext.data.Model} data to the server in an XML format. The {@link #documentRoot} property is used to specify the root element in the XML document. The {@link #record} option is used to specify the element name for each record that will make up the XML document. * @markdown */ Ext.define('Ext.data.writer.Xml', { /* Begin Definitions */ extend: 'Ext.data.writer.Writer', alternateClassName: 'Ext.data.XmlWriter', alias: 'writer.xml', /* End Definitions */ /** * @cfg {String} documentRoot The name of the root element of the document. Defaults to 'xmlData'. * If there is more than 1 record and the root is not specified, the default document root will still be used * to ensure a valid XML document is created. */ documentRoot: 'xmlData', /** * @cfg {String} defaultDocumentRoot The root to be used if {@link #documentRoot} is empty and a root is required * to form a valid XML document. */ defaultDocumentRoot: 'xmlData', /** * @cfg {String} header A header to use in the XML document (such as setting the encoding or version). * Defaults to ''. */ header: '', /** * @cfg {String} record The name of the node to use for each record. Defaults to 'record'. */ record: 'record', //inherit docs writeRecords: function(request, data) { var me = this, xml = [], i = 0, len = data.length, root = me.documentRoot, record = me.record, needsRoot = data.length !== 1, item, key; // may not exist xml.push(me.header || ''); if (!root && needsRoot) { root = me.defaultDocumentRoot; } if (root) { xml.push('<', root, '>'); } for (; i < len; ++i) { item = data[i]; xml.push('<', record, '>'); for (key in item) { if (item.hasOwnProperty(key)) { xml.push('<', key, '>', item[key], ''); } } xml.push(''); } if (root) { xml.push(''); } request.xmlData = xml.join(''); return request; } }); /** * This class provides a DOM ClassList API to buffer access to an element's class. * Instances of this class are created by {@link Ext.layout.ContextItem#getClassList}. */ Ext.define('Ext.layout.ClassList', (function () { var splitWords = Ext.String.splitWords, toMap = Ext.Array.toMap; return { dirty: false, constructor: function (owner) { this.owner = owner; this.map = toMap(this.classes = splitWords(owner.el.className)); }, /** * Adds a single class to the class list. */ add: function (cls) { var me = this; if (!me.map[cls]) { me.map[cls] = true; me.classes.push(cls); if (!me.dirty) { me.dirty = true; me.owner.markDirty(); } } }, /** * Adds one or more classes in an array or space-delimited string to the class list. */ addMany: function (classes) { Ext.each(splitWords(classes), this.add, this); }, contains: function (cls) { return this.map[cls]; }, flush: function () { this.owner.el.className = this.classes.join(' '); this.dirty = false; }, /** * Removes a single class from the class list. */ remove: function (cls) { var me = this; if (me.map[cls]) { delete me.map[cls]; me.classes = Ext.Array.filter(me.classes, function (c) { return c != cls; }); if (!me.dirty) { me.dirty = true; me.owner.markDirty(); } } }, /** * Removes one or more classes in an array or space-delimited string from the class * list. */ removeMany: function (classes) { var me = this, remove = toMap(splitWords(classes)); me.classes = Ext.Array.filter(me.classes, function (c) { if (!remove[c]) { return true; } delete me.map[c]; if (!me.dirty) { me.dirty = true; me.owner.markDirty(); } return false; }); } }; }())); /** * 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', { requires: ['Ext.layout.ClassList'], heightModel: null, widthModel: null, sizeModel: null, 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, remainingChildLayouts: 0, remainingComponentChildLayouts: 0, remainingContainerChildLayouts: 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, el, ownerCt, ownerCtContext, sizeModel, target; Ext.apply(me, config); el = me.el; me.id = el.id; me.lastBox = el.lastBox; // 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 = {}; // the set of cached styles for the element: me.styles = {}; target = me.target; if (target.isComponent) { me.wrapsComponent = true; // 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 = 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. me.sizeModel = sizeModel = target.getSizeModel(ownerCtContext && ownerCtContext.widthModel.pairsByHeightOrdinal[ownerCtContext.heightModel.ordinal]); me.widthModel = sizeModel.width; me.heightModel = sizeModel.height; // 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). } }, /** * 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; me.dirty = me.invalid = false; me.props = {}; 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(); } // 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 = target.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; } 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); } if (me.heightModel.calculated) { me.recoverProp('height', oldProps, oldDirty); } } 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, widthModel = me.widthModel, boxParent; 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 (full, componentChildrenDone, containerChildrenDone, containerLayoutDone) { var me = this, props = me.props, state = me.state; // These properties are only set when they are true: if (componentChildrenDone) { props.componentChildrenDone = true; } if (containerChildrenDone) { props.containerChildrenDone = 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); } }, noFraming: { left: 0, top: 0, right: 0, bottom: 0, width: 0, height: 0 }, /** * Queue the addition of a class name (or array of class names) to this ContextItem's target when next flushed. */ addCls: function(newCls) { this.getClassList().addMany(newCls); }, /** * Queue the removal of a class name (or array of class names) from this ContextItem's target when next flushed. */ removeCls: function(removeCls) { this.getClassList().removeMany(removeCls); }, /** * 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 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; // Flush added/removed classes if (me.classList && me.classList.dirty) { me.classList.flush(); } // 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.lastBox, 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.layout && target.layout.animate; if (targetAnim) { duration = Ext.isNumber(targetAnim) ? targetAnim : 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) { 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); } // Otherwise, undo just the animated properties so the animation can proceed from the old layout. else { me.writeProps(anim.from); } me.el.animate(anim); Ext.fx.Manager.getFxQueue(me.el.id)[0].on({ afteranimate: function() { if (me.isCollapsingOrExpanding === 1) { target.componentLayout.redoLayout(me); target.afterCollapse(true); } else 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; }, /** * Returns a ClassList-like object to buffer access to this item's element's classes. */ getClassList: function () { return this.classList || (this.classList = new Ext.layout.ClassList(this)); }, /** * 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. */ 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; }, getFraming: function () { var me = this; if (!me.framingInfo) { me.framingInfo = me.target.frameSize || me.noFraming; } return me.framingInfo; }, /** * 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, frame, border; if (!info) { frame = me.getFraming(); border = me.getBorderInfo(); me.frameInfo = info = { top : frame.top + border.top, right : frame.right + border.right, bottom: frame.bottom + border.bottom, left : frame.left + border.left, width : frame.width + border.width, height: frame.height + border.height }; } 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, margins, 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; // Option #1 for configuring margins on components is the "margin" config // property. When supplied, this config is written to the DOM during the // render process (see AbstractComponent#initStyles). // // Option #2 is available to some layouts (e.g., Box, Border, Fit) that // handle margin calculations themselves. These layouts support a "margins" // config property on their items and they have a "defaultMargins" config // property. These margin values are added to the "natural" margins read // from the DOM and 0's are written to the DOM after they are added. // To avoid having to do all this on every layout, we cache the results on // the component in the (private) "margin$" property. We identify the cache // results as belonging to the appropriate ownerLayout in case items are // moved around. info = comp.margin$; if (info && info.ownerId !== ownerLayoutId) { // got one but from the wrong owner info = null; // if (manageMargins) { // TODO: clear inline margins (the 0's we wrote last time)??? // } } if (!info) { // if (no cache) // CSS margins are only checked if there isn't a margin property on the component info = me.parseMargins(comp.margin) || me.checkCache('marginInfo'); // Some layouts also support margins and defaultMargins, e.g. Fit, Border, Box if (manageMargins) { margins = me.parseMargins(comp.margins, ownerLayout.defaultMargins); if (margins) { // if (using 'margins' and/or 'defaultMargins') // margin and margins can both be present at the same time and must be combined info = { top: info.top + margins.top, right: info.right + margins.right, bottom: info.bottom + margins.bottom, left: info.left + margins.left }; } 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) { var value = this.getProp(propName); return typeof value != 'undefined'; }, /** * 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) { var value = this.getDomProp(propName); return typeof value != 'undefined'; }, /** * 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 (margins, defaultMargins) { if (margins === true) { margins = 5; } var type = typeof margins, ret; if (type == 'string' || type == 'number') { ret = Ext.util.Format.parseBox(margins); } else if (margins || defaultMargins) { ret = { top: 0, right: 0, bottom: 0, left: 0 }; // base defaults if (defaultMargins) { Ext.apply(ret, this.parseMargins(defaultMargins)); // + layout defaults } Ext.apply(ret, margins); // + config } return ret; }, peek: function (propName) { return this.props[propName]; }, /** * 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(); } } }, 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, borderBox, 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 = {}; } if (propName == 'width' || propName == 'height') { borderBox = me.isBorderBoxValue; if (borderBox == null) { me.isBorderBoxValue = borderBox = !!me.el.isBorderBox(); } if (!borderBox) { me.borderInfo || me.getBorderInfo(); me.paddingInfo || me.getPaddingInfo(); } } 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 /*, private {Boolean} force */) { var me = this, comp = me.target, frameBody, frameInfo, padding; if (height < 0) { height = 0; } if (!me.wrapsComponent) { if (!me.setProp('height', height, dirty)) { return NaN; } } else { height = Ext.Number.constrain(height, comp.minHeight || 0, comp.maxHeight); if (!me.setProp('height', height, dirty)) { return NaN; } frameBody = me.frameBodyContext; if (frameBody){ frameInfo = me.getFrameInfo(); frameBody.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 /*, private {Boolean} force */) { var me = this, comp = me.target, frameBody, frameInfo, padding; if (width < 0) { width = 0; } if (!me.wrapsComponent) { if (!me.setProp('width', width, dirty)) { return NaN; } } else { width = Ext.Number.constrain(width, comp.minWidth || 0, comp.maxWidth); if (!me.setProp('width', width, dirty)) { return NaN; } //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.frameMC) { frameContext = ownerContext.frameContext || (ownerContext.frameContext = ownerContext.getEl('frameMC')); 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, dirtyX = 'x' in dirtyProps, dirtyY = 'y' in dirtyProps, x = dirtyProps.x, y = dirtyProps.y, width = dirtyProps.width, height = dirtyProps.height, isBorderBox = me.isBorderBoxValue, target = me.target, max = Math.max, paddingWidth = 0, paddingHeight = 0, hasWidth, hasHeight, isAbsolute, scrollbarSize, style, targetEl; // 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; } // Non-numeric (eg "auto") go in unchanged. else { styles[propName] = dirtyProps[propName]; } ++styleCount; } } // convert x/y into setPosition (for a component) or left/top styles (for an el) if (dirtyX || dirtyY) { if (target.isComponent) { // Ensure we always pass the current coordinate in if one coordinate has not been dirtied by a calculation cycle. target.setPosition(x||me.props.x, y||me.props.y); } else { // we wrap an element, so convert x/y to styles: if (dirtyX) { styles.left = x + 'px'; ++styleCount; } if (dirtyY) { styles.top = y + 'px'; ++styleCount; } } } // Support for the content-box box model... if (!isBorderBox && (width > 0 || height > 0)) { // no need to subtract from 0 // The width and height values assume the border-box box model, // so we must remove the padding & border to calculate the content-box. if (!(me.borderInfo && me.paddingInfo)) { throw Error("Needed to have gotten the borderInfo and paddingInfo when the width or height was setProp'd"); } if(!me.frameBodyContext) { // Padding needs to be removed only if the element is not framed. paddingWidth = me.paddingInfo.width; paddingHeight = me.paddingInfo.height; } if (width) { width = max(parseInt(width, 10) - (me.borderInfo.width + paddingWidth), 0); styles.width = width + 'px'; ++styleCount; } if (height) { height = max(parseInt(height, 10) - (me.borderInfo.height + paddingHeight), 0); styles.height = height + 'px'; ++styleCount; } } // IE9 strict 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 && Ext.isStrict) { // 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 and border-box: isAbsolute = me.isAbsolute; if (isAbsolute === undefined) { isAbsolute = false; targetEl = me.target.getTargetEl(); style = targetEl.getStyle('position'); if (style == 'absolute') { style = targetEl.getStyle('box-sizing'); isAbsolute = (style == 'border-box'); } me.isAbsolute = isAbsolute; // 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); } } }, 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 = { childrenDone: faux, componentChildrenDone: faux, containerChildrenDone: faux, containerLayoutDone: faux, displayed: faux, done: faux, x: faux, y: faux, // For Ext.grid.ColumnLayout columnWidthsDone: 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 }; }); /** * @private * Base class for Box Layout overflow handlers. These specialized classes are invoked when a Box Layout * (either an HBox or a VBox) has child items that are either too wide (for HBox) or too tall (for VBox) * for its container. */ Ext.define('Ext.layout.container.boxOverflow.None', { alternateClassName: 'Ext.layout.boxOverflow.None', constructor: function(layout, config) { this.layout = layout; Ext.apply(this, config); }, handleOverflow: Ext.emptyFn, clearOverflow: Ext.emptyFn, beginLayout: Ext.emptyFn, beginLayoutCycle: Ext.emptyFn, finishedLayout: Ext.emptyFn, completeLayout: function (ownerContext) { var me = this, plan = ownerContext.state.boxPlan, overflow; if (plan && plan.tooNarrow) { overflow = me.handleOverflow(ownerContext); if (overflow) { if (overflow.reservedSpace) { me.layout.publishInnerCtSize(ownerContext, overflow.reservedSpace); } // TODO: If we need to use the code below then we will need to pass along // the new targetSize as state and use it calculate somehow... // //if (overflow.recalculate) { // ownerContext.invalidate({ // state: { // overflow: overflow // } // }); //} } } else { me.clearOverflow(); } }, onRemove: Ext.emptyFn, /** * @private * Normalizes an item reference, string id or numerical index into a reference to the item * @param {Ext.Component/String/Number} item The item reference, id or index * @return {Ext.Component} The item */ getItem: function(item) { return this.layout.owner.getComponent(item); }, getOwnerType: function(owner){ var type; if (owner.isToolbar) { type = 'toolbar'; } else if (owner.isTabBar) { type = 'tabbar'; } else if (owner.isMenu) { type = 'menu'; } else { type = owner.getXType(); } return type; }, getPrefixConfig: Ext.emptyFn, getSuffixConfig: Ext.emptyFn, getOverflowCls: function() { return ''; } }); /** * Base class that provides a common interface for publishing events. Subclasses are expected to to have a property * "events" with all the events defined, and, optionally, a property "listeners" with configured listeners defined. * * For example: * * Ext.define('Employee', { * mixins: { * observable: 'Ext.util.Observable' * }, * * constructor: function (config) { * // The Observable constructor copies all of the properties of `config` on * // to `this` using {@link Ext#apply}. Further, the `listeners` property is * // processed to add listeners. * // * this.mixins.observable.constructor.call(this, config); * * this.addEvents( * 'fired', * 'quit' * ); * } * }); * * 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.name + " has quit!"); * } * } * }); */ Ext.define('Ext.util.Observable', { /* Begin Definitions */ requires: ['Ext.util.Event'], statics: { /** * Removes **all** added captures from the Observable. * * @param {Ext.util.Observable} o The Observable to release * @static */ releaseCapture: function(o) { o.fireEvent = this.prototype.fireEvent; }, /** * 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) { o.fireEvent = Ext.Function.createInterceptor(o.fireEvent, 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 #addListener}. * @static */ observe: function(cls, listeners) { if (cls) { if (!cls.isObservable) { Ext.applyIf(cls, new this()); this.capture(cls.prototype, cls.fireEvent, 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. * @private */ prepareClass: function (T, mixin) { // 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__). 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 Observable = Ext.util.Observable, 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(); } } }, /* 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 #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 * 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; Ext.apply(me, config); // The subclass may have already initialized it. if (!me.hasListeners) { me.hasListeners = new me.HasListeners(); } me.events = me.events || {}; if (me.listeners) { me.on(me.listeners); me.listeners = null; //Set as an instance property to pre-empt the prototype in case any are set there. } if (me.bubbleEvents) { me.enableBubble(me.bubbleEvents); } }, onClassExtended: function (T) { if (!T.HasListeners) { // Some classes derive from us and some others derive from those classes. All // of these are passed to this method. Ext.util.Observable.prepareClass(T); } }, // @private eventOptionsRe : /^(?:scope|delay|buffer|single|stopEvent|preventDefault|stopPropagation|normalized|args|delegate|element|vertical|horizontal|freezeEvent)$/, /** * Adds listeners to any Observable object (or Ext.Element) which are automatically removed when this Component is * destroyed. * * @param {Ext.util.Observable/Ext.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} 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. * @param {Object} opt (optional) If the `ename` parameter was an event name, this is the * {@link Ext.util.Observable#addListener addListener} options. */ addManagedListener : function(item, ename, fn, scope, options) { var me = this, managedListeners = me.managedListeners = me.managedListeners || [], config; if (typeof ename !== 'string') { options = ename; for (ename in options) { if (options.hasOwnProperty(ename)) { config = options[ename]; if (!me.eventOptionsRe.test(ename)) { me.addManagedListener(item, ename, config.fn || config, config.scope || options.scope, config.fn ? config : options); } } } } else { managedListeners.push({ item: item, ename: ename, fn: fn, scope: scope, options: options }); item.on(ename, fn, scope, options); } }, /** * Removes listeners that were added by the {@link #mon} method. * * @param {Ext.util.Observable/Ext.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 (typeof ename !== 'string') { options = ename; for (ename in options) { if (options.hasOwnProperty(ename)) { config = options[ename]; if (!me.eventOptionsRe.test(ename)) { me.removeManagedListener(item, ename, config.fn || config, config.scope || options.scope); } } } } managedListeners = me.managedListeners ? me.managedListeners.slice() : []; 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 #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) { eventName = eventName.toLowerCase(); var me = this, 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 (event && me.hasListeners[eventName]) { ret = me.continueFireEvent(eventName, Ext.Array.slice(arguments, 1), event.bubble); } return ret; }, /** * Continue to fire event. * @private * * @param {String} eventName * @param {Array} args * @param {Boolean} bubbles */ continueFireEvent: function(eventName, args, bubbles) { var target = this, queue, event, ret = true; do { if (target.eventsSuspended) { if ((queue = target.eventQueue)) { queue.push([eventName, args, bubbles]); } return ret; } else { event = target.events[eventName]; // Continue bubbling if event exists and it is `true` or the handler didn't returns false and it // configure to bubble. if (event && event != true) { if ((ret = event.fire.apply(event, args)) === false) { break; } } } } while (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; }, /** * Appends an event handler to this object. For example: * * myGridPanel.on("mouseover", this.onMouseOver, 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, * mouseover: this.onMouseOver, * mouseout: this.onMouseOut, * 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}, * mouseover: {fn: panel.onMouseOver, scope: panel} * }); * * *Names* of methods in a specified scope may also be used. Note that * `scope` MUST be specified to use this option: * * myGridPanel.on({ * cellClick: {fn: 'onCellClick', scope: this, single: true}, * mouseover: {fn: 'onMouseOver', 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} [fn] The method the event invokes, or *if `scope` is specified, the *name* of the method within * the specified `scope`. Will be called with arguments * given to {@link #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:** Unlike in ExtJS 3.x, 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 {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 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: * * new Ext.panel.Panel({ * title: 'The title', * listeners: { * click: this.handlePanelClick, * element: 'body' * } * }); * * **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 * }); * */ addListener: function(ename, fn, scope, options) { var me = this, config, event, hasListeners, prevListenerCount = 0; if (typeof ename !== 'string') { options = ename; for (ename in options) { if (options.hasOwnProperty(ename)) { config = options[ename]; if (!me.eventOptionsRe.test(ename)) { me.addListener(ename, config.fn || config, config.scope || options.scope, config.fn ? config : options); } } } } else { ename = ename.toLowerCase(); event = me.events[ename]; if (event && event.isEvent) { prevListenerCount = event.listeners.length; } else { me.events[ename] = event = new Ext.util.Event(me, ename); } // Allow listeners: { click: 'onClick', scope: myObject } if (typeof fn === 'string') { if (!(scope[fn] || me[fn])) { Ext.Error.raise('No method named "' + fn + '"'); } fn = scope[fn] || me[fn]; } event.addListener(fn, scope, options); // If a new listener has been added (Event.addListener rejects duplicates of the same fn+scope) // then increment the hasListeners counter if (event.listeners.length !== prevListenerCount) { hasListeners = me.hasListeners; if (hasListeners.hasOwnProperty(ename)) { // if we already have listeners at this level, just increment the count... ++hasListeners[ename]; } else { // otherwise, start the count at 1 (which hides whatever is in our prototype // chain)... hasListeners[ename] = 1; } } } }, /** * 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 #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 #addListener} or the listener will not be removed. */ removeListener: function(ename, fn, scope) { var me = this, config, event, options; if (typeof ename !== 'string') { options = ename; for (ename in options) { if (options.hasOwnProperty(ename)) { config = options[ename]; if (!me.eventOptionsRe.test(ename)) { me.removeListener(ename, config.fn || config, config.scope || options.scope); } } } } else { ename = ename.toLowerCase(); event = me.events[ename]; if (event && event.isEvent) { if (event.removeListener(fn, scope) && !--me.hasListeners[ename]) { // 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 me.hasListeners[ename]; } } } }, /** * Removes all listeners for this object including the managed listeners */ clearListeners: function() { var events = this.events, event, key; for (key in events) { if (events.hasOwnProperty(key)) { event = events[key]; if (event.isEvent) { event.clearListeners(); } } } this.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 managedListeners = this.managedListeners || [], i = 0, len = managedListeners.length; for (; i < len; i++) { this.removeManagedListenerItem(true, managedListeners[i]); } this.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))) { managedListener.item.un(managedListener.ename, managedListener.fn, managedListener.scope); 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); }, /** * Adds the specified events to the list of events which this Observable may fire. * * @param {Object/String...} eventNames Either an object with event names as properties with * a value of `true`. For example: * * this.addEvents({ * storeloaded: true, * storecleared: true * }); * * Or any number of event names as separate parameters. For example: * * this.addEvents('storeloaded', 'storecleared'); * */ addEvents: function(o) { var me = this, events = me.events || (me.events = {}), arg, args, i; if (typeof o == 'string') { for (args = arguments, i = args.length; i--; ) { arg = args[i]; if (!events[arg]) { events[arg] = true; } } } else { Ext.applyIf(me.events, o); } }, /** * 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) { return !!this.hasListeners[ename.toLowerCase()]; }, /** * Suspends the firing of all events. (see {@link #resumeEvents}) * * @param {Boolean} queueSuspended Pass as 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 += 1; if (queueSuspended && !this.eventQueue) { this.eventQueue = []; } }, /** * 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. */ resumeEvents: function() { var me = this, queued = me.eventQueue, qLen, q; if (me.eventsSuspended && ! --me.eventsSuspended) { delete me.eventQueue; if (queued) { qLen = queued.length; for (q = 0; q < qLen; q++) { me.continueFireEvent.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. * * @param {Object} origin The Observable whose events this object is to relay. * @param {String[]} events Array of event names to relay. * @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'. */ relayEvents : function(origin, events, prefix) { var me = this, len = events.length, i = 0, oldName, newName; for (; i < len; i++) { oldName = events[i]; newName = prefix ? prefix + oldName : oldName; // Add the relaying function as a ManagedListener so that it is removed when this.clearListeners is called (usually when _this_ is destroyed) me.mon(origin, oldName, me.createRelayer(newName)); } }, /** * @private * Creates an event handling function which refires the event from this object as the passed event name. * @param newName * @param {Array} beginEnd (optional) The caller can specify on which indices to slice * @returns {Function} */ createRelayer: function(newName, beginEnd){ var me = this; return function() { return me.fireEvent.apply(me, [newName].concat(Array.prototype.slice.apply(arguments, beginEnd || [0, -1]))); }; }, /** * 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.override(Ext.form.field.Base, { * // Add functionality to Field's initComponent to enable the change event to bubble * initComponent : Ext.Function.createSequence(Ext.form.field.Base.prototype.initComponent, function() { * this.enableBubble('change'); * }), * * // We know that we want Field's events to bubble directly to the FormPanel. * getBubbleTarget : function() { * if (!this.formPanel) { * this.formPanel = this.findParentByType('form'); * } * return this.formPanel; * } * }); * * var myForm = new Ext.formPanel({ * 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, length = names.length, events = me.events, ename, event, i; for (i = 0; i < length; ++i) { ename = names[i].toLowerCase(); event = events[ename]; if (!event || typeof event == 'boolean') { events[ename] = event = new Ext.util.Event(me, ename); } // Event must fire if it bubbles (We don't know if anyone up the bubble hierarchy has listeners added) me.hasListeners[ename] = (me.hasListeners[ename]||0) + 1; event.bubble = true; } } } }, function() { var Observable = this, proto = Observable.prototype, HasListeners = function () {}, prepareMixin = function (T) { if (!T.HasListeners) { var proto = T.prototype; // 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) { Observable.prepareClass(U); }); // 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); }; } } }; HasListeners.prototype = { //$$: 42 // to make sure we have a proper prototype }; proto.HasListeners = Observable.HasListeners = HasListeners; Observable.createAlias({ /** * @method * Shorthand for {@link #addListener}. * @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' }); //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); } }); } }); }); /** * @class Ext.util.HashMap *

* 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: { observable: 'Ext.util.Observable' }, /** * @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. */ /** * Creates new HashMap. * @param {Object} config (optional) Config object. */ constructor: function(config) { config = config || {}; var me = this, keyFn = config.keyFn; me.addEvents( /** * @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. */ 'add', /** * @event clear * Fires when the hash is cleared. * @param {Ext.util.HashMap} this. */ 'clear', /** * @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. */ 'remove', /** * @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. */ 'replace' ); me.mixins.observable.constructor.call(me, config); me.clear(true); if (keyFn) { me.getKey = keyFn; } }, /** * 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; if (value === undefined) { value = key; key = me.getKey(value); } if (me.containsKey(key)) { return me.replace(key, value); } me.map[key] = value; ++me.length; 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; if (value === undefined) { value = key; key = me.getKey(value); } if (!me.containsKey(key)) { me.add(key, value); } old = map[key]; map[key] = value; 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; 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) { return this.map[key]; }, /** * Removes all items from the hash. * @return {Ext.util.HashMap} this */ clear: function(/* private */ initial) { var me = this; me.map = {}; me.length = 0; 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) { return this.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. * * The paramaters passed to the function are: *
* @param {Function} fn The function to execute. * @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(), 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; } }); /** * The AbstractPlugin class is the base class from which user-implemented plugins should inherit. * * This class defines the essential API of plugins as used by Components by defining the following methods: * * - `init` : The plugin initialization method which the owning Component calls at Component initialization time. * * The Component passes itself as the sole parameter. * * Subclasses should set up bidirectional links between the plugin and its client Component here. * * - `destroy` : The plugin cleanup method which the owning Component calls at Component destruction time. * * Use this method to break links between the plugin and the Component and to free any allocated resources. * * - `enable` : The base implementation just sets the plugin's `disabled` flag to `false` * * - `disable` : The base implementation just sets the plugin's `disabled` flag to `true` */ Ext.define('Ext.AbstractPlugin', { disabled: false, constructor: function(config) { this.initialConfig = config; Ext.apply(this, config); }, clone: function() { return new this.self(this.initialConfig); }, getCmp: function() { return this.cmp; }, /** * @cfg {String} pluginId * A name for the plugin that can be set at creation time to then retrieve the plugin * through {@link Ext.AbstractComponent#getPlugin getPlugin} method. For example: * * var grid = Ext.create('Ext.grid.Panel', { * plugins: [{ * ptype: 'cellediting', * clicksToEdit: 2, * pluginId: 'cellplugin' * }] * }); * * // later on: * var plugin = grid.getPlugin('cellplugin'); */ /** * @method * The init method is invoked after initComponent method has been run for the client Component. * * The supplied implementation is empty. Subclasses should perform plugin initialization, and set up bidirectional * links between the plugin and its client Component in their own implementation of this method. * @param {Ext.Component} client The client Component which owns this plugin. */ init: Ext.emptyFn, /** * @method * The destroy method is invoked by the owning Component at the time the Component is being destroyed. * * The supplied implementation is empty. Subclasses should perform plugin cleanup in their own implementation of * this method. */ destroy: Ext.emptyFn, /** * The base implementation just sets the plugin's `disabled` flag to `false` * * Plugin subclasses which need more complex processing may implement an overriding implementation. */ enable: function() { this.disabled = false; }, /** * The base implementation just sets the plugin's `disabled` flag to `true` * * Plugin subclasses which need more complex processing may implement an overriding implementation. */ disable: function() { this.disabled = true; } }); /** * A DragTracker listens for drag events on an Element and fires events at the start and end of the drag, * as well as during the drag. This is useful for components such as {@link Ext.slider.Multi}, where there is * an element that can be dragged around to change the Slider's value. * * DragTracker provides a series of template methods that should be overridden to provide functionality * in response to detected drag operations. These are onBeforeStart, onStart, onDrag and onEnd. * See {@link Ext.slider.Multi}'s initEvents function for an example implementation. */ Ext.define('Ext.dd.DragTracker', { uses: ['Ext.util.Region'], mixins: { observable: 'Ext.util.Observable' }, /** * @property {Boolean} active * Indicates whether the user is currently dragging this tracker. * @readonly */ active: false, /** * @property {HTMLElement} dragTarget * The element being dragged. * * Only valid during drag operations. * * If the {@link #delegate} option is used, this will be the delegate element which was mousedowned. * @readonly */ /** * @cfg {Boolean} trackOver * Set to true to fire mouseover and mouseout events when the mouse enters or leaves the target element. * * This is implicitly set when an {@link #overCls} is specified. * * If the {@link #delegate} option is used, these events fire only when a delegate element is entered of left. */ trackOver: false, /** * @cfg {String} overCls * A CSS class to add to the DragTracker's target element when the element (or, if the {@link #delegate} * option is used, when a delegate element) is mouseovered. * * If the {@link #delegate} option is used, these events fire only when a delegate element is entered of left. */ /** * @cfg {Ext.util.Region/Ext.Element} constrainTo * A {@link Ext.util.Region Region} (Or an element from which a Region measurement will be read) * which is used to constrain the result of the {@link #getOffset} call. * * This may be set any time during the DragTracker's lifecycle to set a dynamic constraining region. */ /** * @cfg {Number} tolerance * Number of pixels the drag target must be moved before dragging is * considered to have started. */ tolerance: 5, /** * @cfg {Boolean/Number} autoStart * Specify `true` to defer trigger start by 1000 ms. * Specify a Number for the number of milliseconds to defer trigger start. */ autoStart: false, /** * @cfg {String} delegate * A {@link Ext.DomQuery DomQuery} selector which identifies child elements within the DragTracker's encapsulating * Element which are the tracked elements. This limits tracking to only begin when the matching elements are mousedowned. * * This may also be a specific child element within the DragTracker's encapsulating element to use as the tracked element. */ /** * @cfg {Boolean} [preventDefault=true] * Specify `false` to enable default actions on onMouseDown events. */ /** * @cfg {Boolean} [stopEvent=false] * Specify `true` to stop the `mousedown` event from bubbling to outer listeners from the target element (or its delegates). */ constructor : function(config){ var me = this; Ext.apply(me, config); me.addEvents( /** * @event mouseover * Fires when the mouse enters the DragTracker's target element (or if {@link #delegate} is * used, when the mouse enters a delegate element). * * **Only available when {@link #trackOver} is `true`** * * @param {Object} this * @param {Object} e event object * @param {HTMLElement} target The element mouseovered. */ 'mouseover', /** * @event mouseout * Fires when the mouse exits the DragTracker's target element (or if {@link #delegate} is * used, when the mouse exits a delegate element). * * **Only available when {@link #trackOver} is `true`** * * @param {Object} this * @param {Object} e event object */ 'mouseout', /** * @event mousedown * Fires when the mouse button is pressed down, but before a drag operation begins. The * drag operation begins after either the mouse has been moved by {@link #tolerance} pixels, * or after the {@link #autoStart} timer fires. * * Return false to veto the drag operation. * * @param {Object} this * @param {Object} e event object */ 'mousedown', /** * @event mouseup * @param {Object} this * @param {Object} e event object */ 'mouseup', /** * @event mousemove * Fired when the mouse is moved. Returning false cancels the drag operation. * @param {Object} this * @param {Object} e event object */ 'mousemove', /** * @event beforestart * @param {Object} this * @param {Object} e event object */ 'beforedragstart', /** * @event dragstart * @param {Object} this * @param {Object} e event object */ 'dragstart', /** * @event dragend * @param {Object} this * @param {Object} e event object */ 'dragend', /** * @event drag * @param {Object} this * @param {Object} e event object */ 'drag' ); me.dragRegion = new Ext.util.Region(0,0,0,0); if (me.el) { me.initEl(me.el); } // Dont pass the config so that it is not applied to 'this' again me.mixins.observable.constructor.call(me); if (me.disabled) { me.disable(); } }, /** * Initializes the DragTracker on a given element. * @param {Ext.Element/HTMLElement} el The element */ initEl: function(el) { var me = this; me.el = Ext.get(el); // The delegate option may also be an element on which to listen me.handle = Ext.get(me.delegate); // If delegate specified an actual element to listen on, we do not use the delegate listener option me.delegate = me.handle ? undefined : me.delegate; if (!me.handle) { me.handle = me.el; } // Add a mousedown listener which reacts only on the elements targeted by the delegate config. // We process mousedown to begin tracking. me.mon(me.handle, { mousedown: me.onMouseDown, delegate: me.delegate, scope: me }); // If configured to do so, track mouse entry and exit into the target (or delegate). // The mouseover and mouseout CANNOT be replaced with mouseenter and mouseleave // because delegate cannot work with those pseudoevents. Entry/exit checking is done in the handler. if (me.trackOver || me.overCls) { me.mon(me.handle, { mouseover: me.onMouseOver, mouseout: me.onMouseOut, delegate: me.delegate, scope: me }); } }, disable: function() { this.disabled = true; }, enable: function() { this.disabled = false; }, destroy : function() { this.clearListeners(); delete this.el; }, // When the pointer enters a tracking element, fire a mouseover if the mouse entered from outside. // This is mouseenter functionality, but we cannot use mouseenter because we are using "delegate" to filter mouse targets onMouseOver: function(e, target) { var me = this; if (!me.disabled) { if (Ext.EventManager.contains(e) || me.delegate) { me.mouseIsOut = false; if (me.overCls) { me.el.addCls(me.overCls); } me.fireEvent('mouseover', me, e, me.delegate ? e.getTarget(me.delegate, target) : me.handle); } } }, // When the pointer exits a tracking element, fire a mouseout. // This is mouseleave functionality, but we cannot use mouseleave because we are using "delegate" to filter mouse targets onMouseOut: function(e) { var me = this; if (me.mouseIsDown) { me.mouseIsOut = true; } else { if (me.overCls) { me.el.removeCls(me.overCls); } me.fireEvent('mouseout', me, e); } }, onMouseDown: function(e, target){ var me = this, el; // If this is disabled, or the mousedown has been processed by an upstream DragTracker, return if (me.disabled ||e.dragTracked) { return; } // This information should be available in mousedown listener and onBeforeStart implementations me.dragTarget = me.delegate ? target : me.handle.dom; me.startXY = me.lastXY = e.getXY(); me.startRegion = Ext.fly(me.dragTarget).getRegion(); if (me.fireEvent('mousedown', me, e) === false || me.fireEvent('beforedragstart', me, e) === false || me.onBeforeStart(e) === false) { return; } // Track when the mouse is down so that mouseouts while the mouse is down are not processed. // The onMouseOut method will only ever be called after mouseup. me.mouseIsDown = true; // Flag for downstream DragTracker instances that the mouse is being tracked. e.dragTracked = true; // See Ext.dd.DragDropManager::handleMouseDown el = me.el.dom; if (Ext.isIE && el.setCapture) { el.setCapture(); } if (me.preventDefault !== false) { e.preventDefault(); } Ext.getDoc().on({ scope: me, mouseup: me.onMouseUp, mousemove: me.onMouseMove, selectstart: me.stopSelect }); if (me.autoStart) { me.timer = Ext.defer(me.triggerStart, me.autoStart === true ? 1000 : me.autoStart, me, [e]); } }, onMouseMove: function(e, target){ var me = this, xy = e.getXY(), s = me.startXY; e.preventDefault(); me.lastXY = xy; if (!me.active) { if (Math.max(Math.abs(s[0]-xy[0]), Math.abs(s[1]-xy[1])) > me.tolerance) { me.triggerStart(e); } else { return; } } // Returning false from a mousemove listener deactivates if (me.fireEvent('mousemove', me, e) === false) { me.onMouseUp(e); } else { me.onDrag(e); me.fireEvent('drag', me, e); } }, onMouseUp: function(e) { var me = this; // Clear the flag which ensures onMouseOut fires only after the mouse button // is lifted if the mouseout happens *during* a drag. me.mouseIsDown = false; // If we mouseouted the el *during* the drag, the onMouseOut method will not have fired. Ensure that it gets processed. if (me.mouseIsOut) { me.mouseIsOut = false; me.onMouseOut(e); } e.preventDefault(); // See Ext.dd.DragDropManager::handleMouseDown if (Ext.isIE && document.releaseCapture) { document.releaseCapture(); } me.fireEvent('mouseup', me, e); me.endDrag(e); }, /** * @private * Stop the drag operation, and remove active mouse listeners. */ endDrag: function(e) { var me = this, doc = Ext.getDoc(), wasActive = me.active; doc.un('mousemove', me.onMouseMove, me); doc.un('mouseup', me.onMouseUp, me); doc.un('selectstart', me.stopSelect, me); me.clearStart(); me.active = false; if (wasActive) { me.onEnd(e); me.fireEvent('dragend', me, e); } // Private property calculated when first required and only cached during a drag delete me._constrainRegion; // Remove flag from event singleton. Using "Ext.EventObject" here since "endDrag" is called directly in some cases without an "e" param delete Ext.EventObject.dragTracked; }, triggerStart: function(e) { var me = this; me.clearStart(); me.active = true; me.onStart(e); me.fireEvent('dragstart', me, e); }, clearStart : function() { var timer = this.timer; if (timer) { clearTimeout(timer); delete this.timer; } }, stopSelect : function(e) { e.stopEvent(); return false; }, /** * Template method which should be overridden by each DragTracker instance. Called when the user first clicks and * holds the mouse button down. Return false to disallow the drag * @param {Ext.EventObject} e The event object * @template */ onBeforeStart : function(e) { }, /** * Template method which should be overridden by each DragTracker instance. Called when a drag operation starts * (e.g. the user has moved the tracked element beyond the specified tolerance) * @param {Ext.EventObject} e The event object * @template */ onStart : function(xy) { }, /** * Template method which should be overridden by each DragTracker instance. Called whenever a drag has been detected. * @param {Ext.EventObject} e The event object * @template */ onDrag : function(e) { }, /** * Template method which should be overridden by each DragTracker instance. Called when a drag operation has been completed * (e.g. the user clicked and held the mouse down, dragged the element and then released the mouse button) * @param {Ext.EventObject} e The event object * @template */ onEnd : function(e) { }, /** * Returns the drag target. This is usually the DragTracker's encapsulating element. * * If the {@link #delegate} option is being used, this may be a child element which matches the * {@link #delegate} selector. * * @return {Ext.Element} The element currently being tracked. */ getDragTarget : function(){ return this.dragTarget; }, /** * @private * @returns {Ext.Element} The DragTracker's encapsulating element. */ getDragCt : function(){ return this.el; }, /** * @private * Return the Region into which the drag operation is constrained. * Either the XY pointer itself can be constrained, or the dragTarget element * The private property _constrainRegion is cached until onMouseUp */ getConstrainRegion: function() { var me = this; if (me.constrainTo) { if (me.constrainTo instanceof Ext.util.Region) { return me.constrainTo; } if (!me._constrainRegion) { me._constrainRegion = Ext.fly(me.constrainTo).getViewRegion(); } } else { if (!me._constrainRegion) { me._constrainRegion = me.getDragCt().getViewRegion(); } } return me._constrainRegion; }, getXY : function(constrain){ return constrain ? this.constrainModes[constrain](this, this.lastXY) : this.lastXY; }, /** * Returns the X, Y offset of the current mouse position from the mousedown point. * * This method may optionally constrain the real offset values, and returns a point coerced in one * of two modes: * * - `point` * The current mouse position is coerced into the constrainRegion and the resulting position is returned. * - `dragTarget` * The new {@link Ext.util.Region Region} of the {@link #getDragTarget dragTarget} is calculated * based upon the current mouse position, and then coerced into the constrainRegion. The returned * mouse position is then adjusted by the same delta as was used to coerce the region.\ * * @param constrainMode {String} (Optional) If omitted the true mouse position is returned. May be passed * as `point` or `dragTarget`. See above. * @returns {Number[]} The `X, Y` offset from the mousedown point, optionally constrained. */ getOffset : function(constrain){ var xy = this.getXY(constrain), s = this.startXY; return [xy[0]-s[0], xy[1]-s[1]]; }, constrainModes: { // Constrain the passed point to within the constrain region point: function(me, xy) { var dr = me.dragRegion, constrainTo = me.getConstrainRegion(); // No constraint if (!constrainTo) { return xy; } dr.x = dr.left = dr[0] = dr.right = xy[0]; dr.y = dr.top = dr[1] = dr.bottom = xy[1]; dr.constrainTo(constrainTo); return [dr.left, dr.top]; }, // Constrain the dragTarget to within the constrain region. Return the passed xy adjusted by the same delta. dragTarget: function(me, xy) { var s = me.startXY, dr = me.startRegion.copy(), constrainTo = me.getConstrainRegion(), adjust; // No constraint if (!constrainTo) { return xy; } // See where the passed XY would put the dragTarget if translated by the unconstrained offset. // If it overflows, we constrain the passed XY to bring the potential // region back within the boundary. dr.translateBy(xy[0]-s[0], xy[1]-s[1]); // Constrain the X coordinate by however much the dragTarget overflows if (dr.right > constrainTo.right) { xy[0] += adjust = (constrainTo.right - dr.right); // overflowed the right dr.left += adjust; } if (dr.left < constrainTo.left) { xy[0] += (constrainTo.left - dr.left); // overflowed the left } // Constrain the Y coordinate by however much the dragTarget overflows if (dr.bottom > constrainTo.bottom) { xy[1] += adjust = (constrainTo.bottom - dr.bottom); // overflowed the bottom dr.top += adjust; } if (dr.top < constrainTo.top) { xy[1] += (constrainTo.top - dr.top); // overflowed the top } return xy; } } }); /** * @private * @class Ext.util.LruCache * @extend Ext.util.HashMap * A linked {@link Ext.util.HashMap HashMap} implementation which maintains most recently accessed * items at the end of the list, and purges the cache down to the most recently accessed {@link #maxSize} items * upon add. */ Ext.define('Ext.util.LruCache', { extend: 'Ext.util.HashMap', /** * @cfg {Number} maxSize The maximum size the cache is allowed to grow to before further additions cause * removal of the least recently used entry. */ constructor: function(config) { Ext.apply(this, config); this.callParent([config]); }, /* * @inheritdoc */ add: function(key, newValue) { var me = this, existingKey = me.findKey(newValue), entry; // "new" value is in the list. if (existingKey) { me.unlinkEntry(entry = me.map[existingKey]); entry.prev = me.last; entry.next = null; } // Genuinely new: create an entry for it. else { entry = { prev: me.last, next: null, key: key, value: newValue }; } // If the list is not empty, update the last entry if (me.last) { me.last.next = entry; } // List is empty else { me.first = entry; } me.last = entry; me.callParent([key, entry]); me.prune(); return newValue; }, // @private insertBefore: function(key, newValue, sibling) { var me = this, existingKey, entry; // NOT an assignment. // If there is a following sibling if (sibling = this.map[this.findKey(sibling)]) { existingKey = me.findKey(newValue); // "new" value is in the list. if (existingKey) { me.unlinkEntry(entry = me.map[existingKey]); } // Genuinely new: create an entry for it. else { entry = { prev: sibling.prev, next: sibling, key: key, value: newValue }; } if (sibling.prev) { entry.prev.next = entry; } else { me.first = entry; } entry.next = sibling; sibling.prev = entry; me.prune(); return newValue; } // No following sibling, it's just an add. else { return me.add(key, newValue); } }, /* * @inheritdoc */ get: function(key) { var entry = this.map[key]; if (entry) { // If it's not the end, move to end of list on get if (entry.next) { this.moveToEnd(entry); } return entry.value; } }, /* * @private */ removeAtKey: function(key) { this.unlinkEntry(this.map[key]); return this.callParent(arguments); }, /* * @inheritdoc */ clear: function(/* private */ initial) { this.first = this.last = null; return this.callParent(arguments); }, // private. Only used by internal methods. unlinkEntry: function(entry) { // Stitch the list back up. if (entry) { if (entry.next) { entry.next.prev = entry.prev; } else { this.last = entry.prev; } if (entry.prev) { entry.prev.next = entry.next; } else { this.first = entry.next; } entry.prev = entry.next = null; } }, // private. Only used by internal methods. moveToEnd: function(entry) { this.unlinkEntry(entry); // NOT an assignment. // If the list is not empty, update the last entry if (entry.prev = this.last) { this.last.next = entry; } // List is empty else { this.first = entry; } this.last = entry; }, /* * @private */ getArray: function(isKey) { var arr = [], entry = this.first; while (entry) { arr.push(isKey ? entry.key: entry.value); entry = entry.next; } return arr; }, /** * Executes the specified function once for each item in the cache. * Returning false from the function will cease iteration. * * By default, iteration is from least recently used to most recent. * * The paramaters passed to the function are: *
* @param {Function} fn The function to execute. * @param {Object} scope The scope (this reference) to execute in. Defaults to this LruCache. * @param {Boolean} [reverse=false] Pass true to iterate the list in reverse (most recent first) order. * @return {Ext.util.LruCache} this */ each: function(fn, scope, reverse) { var me = this, entry = reverse ? me.last : me.first, length = me.length; scope = scope || me; while (entry) { if (fn.call(scope, entry.key, entry.value, length) === false) { break; } entry = reverse ? entry.prev : entry.next; } return me; }, /** * @private */ findKey: function(value) { var key, map = this.map; for (key in map) { if (map.hasOwnProperty(key) && map[key].value === value) { return key; } } return undefined; }, /** * Purge the least recently used entries if the maxSize has been exceeded. */ prune: function() { var me = this, purgeCount = me.maxSize ? (me.length - me.maxSize) : 0; if (purgeCount > 0) { for (; me.first && purgeCount; purgeCount--) { me.removeAtKey(me.first.key); } } } /** * @method containsKey * @private */ /** * @method contains * @private */ /** * @method getKeys * @private */ /** * @method getValues * @private */ }); /** * Handles mapping key events to handling functions for an element or a Component. One KeyMap can be used for multiple * actions. * * A KeyMap must be configured with a {@link #target} as an event source which may be an Element or a Component. * * If the target is an element, then the `keydown` event will trigger the invocation of {@link #binding}s. * * It is possible to configure the KeyMap with a custom {@link #eventName} to listen for. This may be useful when the * {@link #target} is a Component. * * The KeyMap's event handling requires that the first parameter passed is a key event. So if the Component's event * signature is different, specify a {@link #processEvent} configuration which accepts the event's parameters and * returns a key event. * * Functions specified in {@link #binding}s are called with this signature : `(String key, Ext.EventObject e)` (if the * match is a multi-key combination the callback will still be called only once). A KeyMap can also handle a string * representation of keys. By default KeyMap starts enabled. * * Usage: * * // map one key by key code * var map = new Ext.util.KeyMap({ * target: "my-element", * key: 13, // or Ext.EventObject.ENTER * fn: myHandler, * scope: myObject * }); * * // map multiple keys to one action by string * var map = new Ext.util.KeyMap({ * target: "my-element", * key: "a\r\n\t", * fn: myHandler, * scope: myObject * }); * * // map multiple keys to multiple actions by strings and array of codes * var map = new Ext.util.KeyMap({ * target: "my-element", * binding: [{ * key: [10,13], * fn: function(){ alert("Return was pressed"); } * }, { * key: "abc", * fn: function(){ alert('a, b or c was pressed'); } * }, { * key: "\t", * ctrl:true, * shift:true, * fn: function(){ alert('Control + shift + tab was pressed.'); } * }] * }); * * Since 4.1.0, KeyMaps can bind to Components and process key-based events fired by Components. * * To bind to a Component, use the single parameter form of constructor and include the Component event name * to listen for, and a `processEvent` implementation which returns the key event for further processing by * the KeyMap: * * var map = new Ext.util.KeyMap({ * target: myGridView, * eventName: 'itemkeydown', * processEvent: function(view, record, node, index, event) { * * // Load the event with the extra information needed by the mappings * event.view = view; * event.store = view.getStore(); * event.record = record; * event.index = index; * return event; * }, * binding: { * key: Ext.EventObject.DELETE, * fn: function(keyCode, e) { * e.store.remove(e.record); * * // Attempt to select the record that's now in its place * e.view.getSelectionModel().select(e.index); * e.view.el.focus(); * } * } * }); */ Ext.define('Ext.util.KeyMap', { alternateClassName: 'Ext.KeyMap', /** * @cfg {Ext.Component/Ext.Element/HTMLElement/String} target * The object on which to listen for the event specified by the {@link #eventName} config option. */ /** * @cfg {Object/Object[][]} binding * Either a single object describing a handling function for s specified key (or set of keys), or * an array of such objects. * @cfg {String/String[]} binding.key A single keycode or an array of keycodes to handle * @cfg {Boolean} binding.shift True to handle key only when shift is pressed, False to handle the * key only when shift is not pressed (defaults to undefined) * @cfg {Boolean} binding.ctrl True to handle key only when ctrl is pressed, False to handle the * key only when ctrl is not pressed (defaults to undefined) * @cfg {Boolean} binding.alt True to handle key only when alt is pressed, False to handle the key * only when alt is not pressed (defaults to undefined) * @cfg {Function} binding.handler The function to call when KeyMap finds the expected key combination * @cfg {Function} binding.fn Alias of handler (for backwards-compatibility) * @cfg {Object} binding.scope The scope of the callback function * @cfg {String} binding.defaultEventAction A default action to apply to the event. Possible values * are: stopEvent, stopPropagation, preventDefault. If no value is set no action is performed. */ /** * @cfg {Object} [processEventScope=this] * The scope (`this` context) in which the {@link #processEvent} method is executed. */ /** * @cfg {Boolean} [ignoreInputFields=false] * Configure this as `true` if there are any input fields within the {@link #target}, and this KeyNav * should not process events from input fields, (`<input>, <textarea> and elements with `contentEditable="true"`) */ /** * @cfg {String} eventName * The event to listen for to pick up key events. */ eventName: 'keydown', constructor: function(config) { var me = this; // Handle legacy arg list in which the first argument is the target. // TODO: Deprecate in V5 if ((arguments.length !== 1) || (typeof config === 'string') || config.dom || config.tagName || config === document || config.isComponent) { me.legacyConstructor.apply(me, arguments); return; } Ext.apply(me, config); me.bindings = []; if (!me.target.isComponent) { me.target = Ext.get(me.target); } if (me.binding) { me.addBinding(me.binding); } else if (config.key) { me.addBinding(config); } me.enable(); }, /** * @private * Old constructor signature * @param {String/HTMLElement/Ext.Element/Ext.Component} el The element or its ID, or Component to bind to * @param {Object} binding The binding (see {@link #addBinding}) * @param {String} [eventName="keydown"] The event to bind to */ legacyConstructor: function(el, binding, eventName){ var me = this; Ext.apply(me, { target: Ext.get(el), eventName: eventName || me.eventName, bindings: [] }); if (binding) { me.addBinding(binding); } me.enable(); }, /** * Add a new binding to this KeyMap. * * Usage: * * // Create a KeyMap * var map = new Ext.util.KeyMap(document, { * key: Ext.EventObject.ENTER, * fn: handleKey, * scope: this * }); * * //Add a new binding to the existing KeyMap later * map.addBinding({ * key: 'abc', * shift: true, * fn: handleKey, * scope: this * }); * * @param {Object/Object[]} binding A single KeyMap config or an array of configs. * The following config object properties are supported: * @param {String/Array} binding.key A single keycode or an array of keycodes to handle. * @param {Boolean} binding.shift True to handle key only when shift is pressed, * False to handle the keyonly when shift is not pressed (defaults to undefined). * @param {Boolean} binding.ctrl True to handle key only when ctrl is pressed, * False to handle the key only when ctrl is not pressed (defaults to undefined). * @param {Boolean} binding.alt True to handle key only when alt is pressed, * False to handle the key only when alt is not pressed (defaults to undefined). * @param {Function} binding.handler The function to call when KeyMap finds the * expected key combination. * @param {Function} binding.fn Alias of handler (for backwards-compatibility). * @param {Object} binding.scope The scope of the callback function. * @param {String} binding.defaultEventAction A default action to apply to the event. * Possible values are: stopEvent, stopPropagation, preventDefault. If no value is * set no action is performed.. */ addBinding : function(binding){ var keyCode = binding.key, processed = false, key, keys, keyString, i, len; if (Ext.isArray(binding)) { for (i = 0, len = binding.length; i < len; i++) { this.addBinding(binding[i]); } return; } if (Ext.isString(keyCode)) { keys = []; keyString = keyCode.toUpperCase(); for (i = 0, len = keyString.length; i < len; ++i){ keys.push(keyString.charCodeAt(i)); } keyCode = keys; processed = true; } if (!Ext.isArray(keyCode)) { keyCode = [keyCode]; } if (!processed) { for (i = 0, len = keyCode.length; i < len; ++i) { key = keyCode[i]; if (Ext.isString(key)) { keyCode[i] = key.toUpperCase().charCodeAt(0); } } } this.bindings.push(Ext.apply({ keyCode: keyCode }, binding)); }, /** * Process the {@link #eventName event} from the {@link #target}. * @private * @param {Ext.EventObject} event */ handleTargetEvent: (function() { var tagRe = /input|textarea/i; return function(event) { var me = this, bindings, i, len, target, contentEditable; if (this.enabled) { //just in case bindings = this.bindings; i = 0; len = bindings.length; // Process the event event = me.processEvent.apply(me||me.processEventScope, arguments); // Ignore events from input fields if configured to do so if (me.ignoreInputFields) { target = event.target; contentEditable = target.contentEditable; // contentEditable will default to inherit if not specified, only check if the // attribute has been set or explicitly set to true // http://html5doctor.com/the-contenteditable-attribute/ if (tagRe.test(target.tagName) || (contentEditable === '' || contentEditable === 'true')) { return; } } // If the processor does not return a keyEvent, we can't process it. // Allow them to return false to cancel processing of the event if (!event.getKey) { return event; } for(; i < len; ++i){ this.processBinding(bindings[i], event); } } } }()), /** * @cfg {Function} processEvent * An optional event processor function which accepts the argument list provided by the * {@link #eventName configured event} of the {@link #target}, and returns a keyEvent for processing by the KeyMap. * * This may be useful when the {@link #target} is a Component with s complex event signature, where the event is not * the first parameter. Extra information from the event arguments may be injected into the event for use by the handler * functions before returning it. */ processEvent: function(event){ return event; }, /** * Process a particular binding and fire the handler if necessary. * @private * @param {Object} binding The binding information * @param {Ext.EventObject} event */ processBinding: function(binding, event){ if (this.checkModifiers(binding, event)) { var key = event.getKey(), handler = binding.fn || binding.handler, scope = binding.scope || this, keyCode = binding.keyCode, defaultEventAction = binding.defaultEventAction, i, len, keydownEvent = new Ext.EventObjectImpl(event); for (i = 0, len = keyCode.length; i < len; ++i) { if (key === keyCode[i]) { if (handler.call(scope, key, event) !== true && defaultEventAction) { keydownEvent[defaultEventAction](); } break; } } } }, /** * Check if the modifiers on the event match those on the binding * @private * @param {Object} binding * @param {Ext.EventObject} event * @return {Boolean} True if the event matches the binding */ checkModifiers: function(binding, e) { var keys = ['shift', 'ctrl', 'alt'], i = 0, len = keys.length, val, key; for (; i < len; ++i){ key = keys[i]; val = binding[key]; if (!(val === undefined || (val === e[key + 'Key']))) { return false; } } return true; }, /** * Shorthand for adding a single key listener. * * @param {Number/Number[]/Object} key Either the numeric key code, array of key codes or an object with the * following options: `{key: (number or array), shift: (true/false), ctrl: (true/false), alt: (true/false)}` * @param {Function} fn The function to call * @param {Object} [scope] The scope (`this` reference) in which the function is executed. * Defaults to the browser window. */ on: function(key, fn, scope) { var keyCode, shift, ctrl, alt; if (Ext.isObject(key) && !Ext.isArray(key)) { keyCode = key.key; shift = key.shift; ctrl = key.ctrl; alt = key.alt; } else { keyCode = key; } this.addBinding({ key: keyCode, shift: shift, ctrl: ctrl, alt: alt, fn: fn, scope: scope }); }, /** * Returns true if this KeyMap is enabled * @return {Boolean} */ isEnabled : function() { return this.enabled; }, /** * Enables this KeyMap */ enable: function() { var me = this; if (!me.enabled) { me.target.on(me.eventName, me.handleTargetEvent, me); me.enabled = true; } }, /** * Disable this KeyMap */ disable: function() { var me = this; if (me.enabled) { me.target.removeListener(me.eventName, me.handleTargetEvent, me); me.enabled = false; } }, /** * Convenience function for setting disabled/enabled by boolean. * @param {Boolean} disabled */ setDisabled : function(disabled) { if (disabled) { this.disable(); } else { this.enable(); } }, /** * Destroys the KeyMap instance and removes all handlers. * @param {Boolean} removeTarget True to also remove the {@link #target} */ destroy: function(removeTarget) { var me = this, target = me.target; me.bindings = []; me.disable(); if (removeTarget === true) { if (target.isComponent) { target.destroy(); } else { target.remove(); } } delete me.target; } }); /** * @class Ext.util.Memento * This class manages a set of captured properties from an object. These captured properties * can later be restored to an object. */ Ext.define('Ext.util.Memento', (function () { function captureOne (src, target, prop, prefix) { src[prefix ? prefix + prop : prop] = target[prop]; } function removeOne (src, target, prop) { delete src[prop]; } function restoreOne (src, target, prop, prefix) { var name = prefix ? prefix + prop : prop, value = src[name]; if (value || src.hasOwnProperty(name)) { restoreValue(target, prop, value); } } function restoreValue (target, prop, value) { if (Ext.isDefined(value)) { target[prop] = value; } else { delete target[prop]; } } function doMany (doOne, src, target, props, prefix) { if (src) { if (Ext.isArray(props)) { var p, pLen = props.length; for (p = 0; p < pLen; p++) { doOne(src, target, props[p], prefix); } } else { doOne(src, target, props, prefix); } } } return { /** * @property data * The collection of captured properties. * @private */ data: null, /** * @property target * The default target object for capture/restore (passed to the constructor). */ target: null, /** * Creates a new memento and optionally captures properties from the target object. * @param {Object} target The target from which to capture properties. If specified in the * constructor, this target becomes the default target for all other operations. * @param {String/String[]} props The property or array of properties to capture. */ constructor: function (target, props) { if (target) { this.target = target; if (props) { this.capture(props); } } }, /** * Captures the specified properties from the target object in this memento. * @param {String/String[]} props The property or array of properties to capture. * @param {Object} target The object from which to capture properties. */ capture: function (props, target, prefix) { var me = this; doMany(captureOne, me.data || (me.data = {}), target || me.target, props, prefix); }, /** * Removes the specified properties from this memento. These properties will not be * restored later without re-capturing their values. * @param {String/String[]} props The property or array of properties to remove. */ remove: function (props) { doMany(removeOne, this.data, null, props); }, /** * Restores the specified properties from this memento to the target object. * @param {String/String[]} props The property or array of properties to restore. * @param {Boolean} clear True to remove the restored properties from this memento or * false to keep them (default is true). * @param {Object} target The object to which to restore properties. */ restore: function (props, clear, target, prefix) { doMany(restoreOne, this.data, target || this.target, props, prefix); if (clear !== false) { this.remove(props); } }, /** * Restores all captured properties in this memento to the target object. * @param {Boolean} clear True to remove the restored properties from this memento or * false to keep them (default is true). * @param {Object} target The object to which to restore properties. */ restoreAll: function (clear, target) { var me = this, t = target || this.target, data = me.data, prop; for (prop in data) { if (data.hasOwnProperty(prop)) { restoreValue(t, prop, data[prop]); } } if (clear !== false) { delete me.data; } } }; }())); /** * Represents a filter that can be applied to a {@link Ext.util.MixedCollection MixedCollection}. Can either simply * filter on a property/value pair or pass in a filter function with custom logic. Filters are always used in the * context of MixedCollections, though {@link Ext.data.Store Store}s frequently create them when filtering and searching * on their records. Example usage: * * //set up a fictional MixedCollection containing a few people to filter on * var allNames = new Ext.util.MixedCollection(); * allNames.addAll([ * {id: 1, name: 'Ed', age: 25}, * {id: 2, name: 'Jamie', age: 37}, * {id: 3, name: 'Abe', age: 32}, * {id: 4, name: 'Aaron', age: 26}, * {id: 5, name: 'David', age: 32} * ]); * * var ageFilter = new Ext.util.Filter({ * property: 'age', * value : 32 * }); * * var longNameFilter = new Ext.util.Filter({ * filterFn: function(item) { * return item.name.length > 4; * } * }); * * //a new MixedCollection with the 3 names longer than 4 characters * var longNames = allNames.filter(longNameFilter); * * //a new MixedCollection with the 2 people of age 32: * var youngFolk = allNames.filter(ageFilter); * */ Ext.define('Ext.util.Filter', { /* Begin Definitions */ /* End Definitions */ /** * @cfg {String} property * The property to filter on. Required unless a {@link #filterFn} is passed */ /** * @cfg {Function} filterFn * A custom filter function which is passed each item in the {@link Ext.util.MixedCollection} in turn. Should return * true to accept each item or false to reject it */ /** * @cfg {Boolean} anyMatch * True to allow any match - no regex start/end line anchors will be added. */ anyMatch: false, /** * @cfg {Boolean} exactMatch * True to force exact match (^ and $ characters added to the regex). Ignored if anyMatch is true. */ exactMatch: false, /** * @cfg {Boolean} caseSensitive * True to make the regex case sensitive (adds 'i' switch to regex). */ caseSensitive: false, /** * @cfg {String} root * Optional root property. This is mostly useful when filtering a Store, in which case we set the root to 'data' to * make the filter pull the {@link #property} out of the data object of each item */ /** * Creates new Filter. * @param {Object} [config] Config object */ constructor: function(config) { var me = this; Ext.apply(me, config); //we're aliasing filter to filterFn mostly for API cleanliness reasons, despite the fact it dirties the code here. //Ext.util.Sorter takes a sorterFn property but allows .sort to be called - we do the same here me.filter = me.filter || me.filterFn; if (me.filter === undefined) { if (me.property === undefined || me.value === undefined) { // Commented this out temporarily because it stops us using string ids in models. TODO: Remove this once // Model has been updated to allow string ids // Ext.Error.raise("A Filter requires either a property or a filterFn to be set"); } else { me.filter = me.createFilterFn(); } me.filterFn = me.filter; } }, /** * @private * Creates a filter function for the configured property/value/anyMatch/caseSensitive options for this Filter */ createFilterFn: function() { var me = this, matcher = me.createValueMatcher(), property = me.property; return function(item) { var value = me.getRoot.call(me, item)[property]; return matcher === null ? value === null : matcher.test(value); }; }, /** * @private * Returns the root property of the given item, based on the configured {@link #root} property * @param {Object} item The item * @return {Object} The root property of the object */ getRoot: function(item) { var root = this.root; return root === undefined ? item : item[root]; }, /** * @private * Returns a regular expression based on the given value and matching options */ createValueMatcher : function() { var me = this, value = me.value, anyMatch = me.anyMatch, exactMatch = me.exactMatch, caseSensitive = me.caseSensitive, escapeRe = Ext.String.escapeRegex; if (value === null) { return value; } if (!value.exec) { // not a regex value = String(value); if (anyMatch === true) { value = escapeRe(value); } else { value = '^' + escapeRe(value); if (exactMatch === true) { value += '$'; } } value = new RegExp(value, caseSensitive ? '' : 'i'); } return value; } }); /** * Represents a single sorter that can be applied to a Store. The sorter is used * to compare two values against each other for the purpose of ordering them. Ordering * is achieved by specifying either: * * - {@link #property A sorting property} * - {@link #sorterFn A sorting function} * * As a contrived example, we can specify a custom sorter that sorts by rank: * * Ext.define('Person', { * extend: 'Ext.data.Model', * fields: ['name', 'rank'] * }); * * Ext.create('Ext.data.Store', { * model: 'Person', * proxy: 'memory', * sorters: [{ * sorterFn: function(o1, o2){ * var getRank = function(o){ * var name = o.get('rank'); * if (name === 'first') { * return 1; * } else if (name === 'second') { * return 2; * } else { * return 3; * } * }, * rank1 = getRank(o1), * rank2 = getRank(o2); * * if (rank1 === rank2) { * return 0; * } * * return rank1 < rank2 ? -1 : 1; * } * }], * data: [{ * name: 'Person1', * rank: 'second' * }, { * name: 'Person2', * rank: 'third' * }, { * name: 'Person3', * rank: 'first' * }] * }); */ Ext.define('Ext.util.Sorter', { /** * @cfg {String} property * The property to sort by. Required unless {@link #sorterFn} is provided. The property is extracted from the object * directly and compared for sorting using the built in comparison operators. */ /** * @cfg {Function} sorterFn * A specific sorter function to execute. Can be passed instead of {@link #property}. This sorter function allows * for any kind of custom/complex comparisons. The sorterFn receives two arguments, the objects being compared. The * function should return: * * - -1 if o1 is "less than" o2 * - 0 if o1 is "equal" to o2 * - 1 if o1 is "greater than" o2 */ /** * @cfg {String} root * Optional root property. This is mostly useful when sorting a Store, in which case we set the root to 'data' to * make the filter pull the {@link #property} out of the data object of each item */ /** * @cfg {Function} transform * A function that will be run on each value before it is compared in the sorter. The function will receive a single * argument, the value. */ /** * @cfg {String} direction * The direction to sort by. */ direction: "ASC", constructor: function(config) { var me = this; Ext.apply(me, config); if (me.property === undefined && me.sorterFn === undefined) { Ext.Error.raise("A Sorter requires either a property or a sorter function"); } me.updateSortFunction(); }, /** * @private * Creates and returns a function which sorts an array by the given property and direction * @return {Function} A function which sorts by the property/direction combination provided */ createSortFunction: function(sorterFn) { var me = this, property = me.property, direction = me.direction || "ASC", modifier = direction.toUpperCase() == "DESC" ? -1 : 1; //create a comparison function. Takes 2 objects, returns 1 if object 1 is greater, //-1 if object 2 is greater or 0 if they are equal return function(o1, o2) { return modifier * sorterFn.call(me, o1, o2); }; }, /** * @private * Basic default sorter function that just compares the defined property of each object */ defaultSorterFn: function(o1, o2) { var me = this, transform = me.transform, v1 = me.getRoot(o1)[me.property], v2 = me.getRoot(o2)[me.property]; if (transform) { v1 = transform(v1); v2 = transform(v2); } return v1 > v2 ? 1 : (v1 < v2 ? -1 : 0); }, /** * @private * Returns the root property of the given item, based on the configured {@link #root} property * @param {Object} item The item * @return {Object} The root property of the object */ getRoot: function(item) { return this.root === undefined ? item : item[this.root]; }, /** * Set the sorting direction for this sorter. * @param {String} direction The direction to sort in. Should be either 'ASC' or 'DESC'. */ setDirection: function(direction) { var me = this; me.direction = direction ? direction.toUpperCase() : direction; me.updateSortFunction(); }, /** * Toggles the sorting direction for this sorter. */ toggle: function() { var me = this; me.direction = Ext.String.toggle(me.direction, "ASC", "DESC"); me.updateSortFunction(); }, /** * Update the sort function for this sorter. * @param {Function} [fn] A new sorter function for this sorter. If not specified it will use the default * sorting function. */ updateSortFunction: function(fn) { var me = this; fn = fn || me.sorterFn || me.defaultSorterFn; me.sort = me.createSortFunction(fn); } }); /** * General purpose inflector class that {@link #pluralize pluralizes}, {@link #singularize singularizes} and * {@link #ordinalize ordinalizes} words. Sample usage: * * //turning singular words into plurals * Ext.util.Inflector.pluralize('word'); //'words' * Ext.util.Inflector.pluralize('person'); //'people' * Ext.util.Inflector.pluralize('sheep'); //'sheep' * * //turning plurals into singulars * Ext.util.Inflector.singularize('words'); //'word' * Ext.util.Inflector.singularize('people'); //'person' * Ext.util.Inflector.singularize('sheep'); //'sheep' * * //ordinalizing numbers * Ext.util.Inflector.ordinalize(11); //"11th" * Ext.util.Inflector.ordinalize(21); //"21st" * Ext.util.Inflector.ordinalize(1043); //"1043rd" * * # Customization * * The Inflector comes with a default set of US English pluralization rules. These can be augmented with additional * rules if the default rules do not meet your application's requirements, or swapped out entirely for other languages. * Here is how we might add a rule that pluralizes "ox" to "oxen": * * Ext.util.Inflector.plural(/^(ox)$/i, "$1en"); * * Each rule consists of two items - a regular expression that matches one or more rules, and a replacement string. In * this case, the regular expression will only match the string "ox", and will replace that match with "oxen". Here's * how we could add the inverse rule: * * Ext.util.Inflector.singular(/^(ox)en$/i, "$1"); * * Note that the ox/oxen rules are present by default. */ Ext.define('Ext.util.Inflector', { /* Begin Definitions */ singleton: true, /* End Definitions */ /** * @private * The registered plural tuples. Each item in the array should contain two items - the first must be a regular * expression that matchers the singular form of a word, the second must be a String that replaces the matched * part of the regular expression. This is managed by the {@link #plural} method. * @property {Array} plurals */ plurals: [ [(/(quiz)$/i), "$1zes" ], [(/^(ox)$/i), "$1en" ], [(/([m|l])ouse$/i), "$1ice" ], [(/(matr|vert|ind)ix|ex$/i), "$1ices" ], [(/(x|ch|ss|sh)$/i), "$1es" ], [(/([^aeiouy]|qu)y$/i), "$1ies" ], [(/(hive)$/i), "$1s" ], [(/(?:([^f])fe|([lr])f)$/i), "$1$2ves"], [(/sis$/i), "ses" ], [(/([ti])um$/i), "$1a" ], [(/(buffal|tomat|potat)o$/i), "$1oes" ], [(/(bu)s$/i), "$1ses" ], [(/(alias|status|sex)$/i), "$1es" ], [(/(octop|vir)us$/i), "$1i" ], [(/(ax|test)is$/i), "$1es" ], [(/^person$/), "people" ], [(/^man$/), "men" ], [(/^(child)$/), "$1ren" ], [(/s$/i), "s" ], [(/$/), "s" ] ], /** * @private * The set of registered singular matchers. Each item in the array should contain two items - the first must be a * regular expression that matches the plural form of a word, the second must be a String that replaces the * matched part of the regular expression. This is managed by the {@link #singular} method. * @property {Array} singulars */ singulars: [ [(/(quiz)zes$/i), "$1" ], [(/(matr)ices$/i), "$1ix" ], [(/(vert|ind)ices$/i), "$1ex" ], [(/^(ox)en/i), "$1" ], [(/(alias|status)es$/i), "$1" ], [(/(octop|vir)i$/i), "$1us" ], [(/(cris|ax|test)es$/i), "$1is" ], [(/(shoe)s$/i), "$1" ], [(/(o)es$/i), "$1" ], [(/(bus)es$/i), "$1" ], [(/([m|l])ice$/i), "$1ouse" ], [(/(x|ch|ss|sh)es$/i), "$1" ], [(/(m)ovies$/i), "$1ovie" ], [(/(s)eries$/i), "$1eries"], [(/([^aeiouy]|qu)ies$/i), "$1y" ], [(/([lr])ves$/i), "$1f" ], [(/(tive)s$/i), "$1" ], [(/(hive)s$/i), "$1" ], [(/([^f])ves$/i), "$1fe" ], [(/(^analy)ses$/i), "$1sis" ], [(/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$/i), "$1$2sis"], [(/([ti])a$/i), "$1um" ], [(/(n)ews$/i), "$1ews" ], [(/people$/i), "person" ], [(/s$/i), "" ] ], /** * @private * The registered uncountable words * @property {String[]} uncountable */ uncountable: [ "sheep", "fish", "series", "species", "money", "rice", "information", "equipment", "grass", "mud", "offspring", "deer", "means" ], /** * Adds a new singularization rule to the Inflector. See the intro docs for more information * @param {RegExp} matcher The matcher regex * @param {String} replacer The replacement string, which can reference matches from the matcher argument */ singular: function(matcher, replacer) { this.singulars.unshift([matcher, replacer]); }, /** * Adds a new pluralization rule to the Inflector. See the intro docs for more information * @param {RegExp} matcher The matcher regex * @param {String} replacer The replacement string, which can reference matches from the matcher argument */ plural: function(matcher, replacer) { this.plurals.unshift([matcher, replacer]); }, /** * Removes all registered singularization rules */ clearSingulars: function() { this.singulars = []; }, /** * Removes all registered pluralization rules */ clearPlurals: function() { this.plurals = []; }, /** * Returns true if the given word is transnumeral (the word is its own singular and plural form - e.g. sheep, fish) * @param {String} word The word to test * @return {Boolean} True if the word is transnumeral */ isTransnumeral: function(word) { return Ext.Array.indexOf(this.uncountable, word) != -1; }, /** * Returns the pluralized form of a word (e.g. Ext.util.Inflector.pluralize('word') returns 'words') * @param {String} word The word to pluralize * @return {String} The pluralized form of the word */ pluralize: function(word) { if (this.isTransnumeral(word)) { return word; } var plurals = this.plurals, length = plurals.length, tuple, regex, i; for (i = 0; i < length; i++) { tuple = plurals[i]; regex = tuple[0]; if (regex == word || (regex.test && regex.test(word))) { return word.replace(regex, tuple[1]); } } return word; }, /** * Returns the singularized form of a word (e.g. Ext.util.Inflector.singularize('words') returns 'word') * @param {String} word The word to singularize * @return {String} The singularized form of the word */ singularize: function(word) { if (this.isTransnumeral(word)) { return word; } var singulars = this.singulars, length = singulars.length, tuple, regex, i; for (i = 0; i < length; i++) { tuple = singulars[i]; regex = tuple[0]; if (regex == word || (regex.test && regex.test(word))) { return word.replace(regex, tuple[1]); } } return word; }, /** * Returns the correct {@link Ext.data.Model Model} name for a given string. Mostly used internally by the data * package * @param {String} word The word to classify * @return {String} The classified version of the word */ classify: function(word) { return Ext.String.capitalize(this.singularize(word)); }, /** * Ordinalizes a given number by adding a prefix such as 'st', 'nd', 'rd' or 'th' based on the last digit of the * number. 21 -> 21st, 22 -> 22nd, 23 -> 23rd, 24 -> 24th etc * @param {Number} number The number to ordinalize * @return {String} The ordinalized number */ ordinalize: function(number) { var parsed = parseInt(number, 10), mod10 = parsed % 10, mod100 = parsed % 100; //11 through 13 are a special case if (11 <= mod100 && mod100 <= 13) { return number + "th"; } else { switch(mod10) { case 1 : return number + "st"; case 2 : return number + "nd"; case 3 : return number + "rd"; default: return number + "th"; } } } }, function() { //aside from the rules above, there are a number of words that have irregular pluralization so we add them here var irregulars = { alumnus: 'alumni', cactus : 'cacti', focus : 'foci', nucleus: 'nuclei', radius: 'radii', stimulus: 'stimuli', ellipsis: 'ellipses', paralysis: 'paralyses', oasis: 'oases', appendix: 'appendices', index: 'indexes', beau: 'beaux', bureau: 'bureaux', tableau: 'tableaux', woman: 'women', child: 'children', man: 'men', corpus: 'corpora', criterion: 'criteria', curriculum: 'curricula', genus: 'genera', memorandum: 'memoranda', phenomenon: 'phenomena', foot: 'feet', goose: 'geese', tooth: 'teeth', antenna: 'antennae', formula: 'formulae', nebula: 'nebulae', vertebra: 'vertebrae', vita: 'vitae' }, singular; for (singular in irregulars) { this.plural(singular, irregulars[singular]); this.singular(irregulars[singular], singular); } }); /** * An internal Queue class. * @private */ Ext.define('Ext.util.Queue', { constructor: function() { this.clear(); }, add : function(obj) { var me = this, key = me.getKey(obj); if (!me.map[key]) { ++me.length; me.items.push(obj); me.map[key] = 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; } }); /** * Provides precise pixel measurements for blocks of text so that you can determine exactly how high and * wide, in pixels, a given block of text will be. Note that when measuring text, it should be plain text and * should not contain any HTML, otherwise it may not be measured correctly. * * The measurement works by copying the relevant CSS styles that can affect the font related display, * then checking the size of an element that is auto-sized. Note that if the text is multi-lined, you must * provide a **fixed width** when doing the measurement. * * If multiple measurements are being done on the same element, you create a new instance to initialize * to avoid the overhead of copying the styles to the element repeatedly. */ Ext.define('Ext.util.TextMetrics', { statics: { shared: null, /** * Measures the size of the specified text * @param {String/HTMLElement} el The element, dom node or id from which to copy existing CSS styles * that can affect the size of the rendered text * @param {String} text The text to measure * @param {Number} fixedWidth (optional) If the text will be multiline, you have to set a fixed width * in order to accurately measure the text height * @return {Object} An object containing the text's size `{width: (width), height: (height)}` * @static */ measure: function(el, text, fixedWidth){ var me = this, shared = me.shared; if(!shared){ shared = me.shared = new me(el, fixedWidth); } shared.bind(el); shared.setFixedWidth(fixedWidth || 'auto'); return shared.getSize(text); }, /** * Destroy the TextMetrics instance created by {@link #measure}. * @static */ destroy: function(){ var me = this; Ext.destroy(me.shared); me.shared = null; } }, /** * Creates new TextMetrics. * @param {String/HTMLElement/Ext.Element} bindTo The element or its ID to bind to. * @param {Number} [fixedWidth] A fixed width to apply to the measuring element. */ constructor: function(bindTo, fixedWidth){ var measure = this.measure = Ext.getBody().createChild({ cls: Ext.baseCSSPrefix + 'textmetrics' }); this.el = Ext.get(bindTo); measure.position('absolute'); measure.setLeftTop(-1000, -1000); measure.hide(); if (fixedWidth) { measure.setWidth(fixedWidth); } }, /** * Returns the size of the specified text based on the internal element's style and width properties * @param {String} text The text to measure * @return {Object} An object containing the text's size `{width: (width), height: (height)}` */ getSize: function(text){ var measure = this.measure, size; measure.update(text); size = measure.getSize(); measure.update(''); return size; }, /** * Binds this TextMetrics instance to a new element * @param {String/HTMLElement/Ext.Element} el The element or its ID. */ bind: function(el){ var me = this; me.el = Ext.get(el); me.measure.setStyle( me.el.getStyles('font-size','font-style', 'font-weight', 'font-family','line-height', 'text-transform', 'letter-spacing') ); }, /** * Sets a fixed width on the internal measurement element. If the text will be multiline, you have * to set a fixed width in order to accurately measure the text height. * @param {Number} width The width to set on the element */ setFixedWidth : function(width){ this.measure.setWidth(width); }, /** * Returns the measured width of the specified text * @param {String} text The text to measure * @return {Number} width The width in pixels */ getWidth : function(text){ this.measure.dom.style.width = 'auto'; return this.getSize(text).width; }, /** * Returns the measured height of the specified text * @param {String} text The text to measure * @return {Number} height The height in pixels */ getHeight : function(text){ return this.getSize(text).height; }, /** * Destroy this instance */ destroy: function(){ var me = this; me.measure.remove(); delete me.el; delete me.measure; } }, function(){ Ext.Element.addMethods({ /** * Returns the width in pixels of the passed text, or the width of the text in this Element. * @param {String} text The text to measure. Defaults to the innerHTML of the element. * @param {Number} [min] The minumum value to return. * @param {Number} [max] The maximum value to return. * @return {Number} The text width in pixels. * @member Ext.dom.Element */ getTextWidth : function(text, min, max){ return Ext.Number.constrain(Ext.util.TextMetrics.measure(this.dom, Ext.value(text, this.dom.innerHTML, true)).width, min || 0, max || 1000000); } }); }); /** * This mixin enables classes to declare relationships to child elements and provides the * mechanics for acquiring the {@link Ext.Element elements} and storing them on an object * instance as properties. * * This class is used by {@link Ext.Component components} and {@link Ext.layout.container.Container container layouts} to * manage their child elements. * * A typical component that uses these features might look something like this: * * Ext.define('Ext.ux.SomeComponent', { * extend: 'Ext.Component', * * childEls: [ * 'bodyEl' * ], * * renderTpl: [ * '
' * ], * * // ... * }); * * The `childEls` array lists one or more relationships to child elements managed by the * component. The items in this array can be either of the following types: * * - String: the id suffix and property name in one. For example, "bodyEl" in the above * example means a "bodyEl" property will be added to the instance with the result of * {@link Ext#get} given "componentId-bodyEl" where "componentId" is the component instance's * id. * - Object: with a `name` property that names the instance property for the element, and * one of the following additional properties: * - `id`: The full id of the child element. * - `itemId`: The suffix part of the id to which "componentId-" is prepended. * - `select`: A selector that will be passed to {@link Ext#select}. * - `selectNode`: A selector that will be passed to {@link Ext.DomQuery#selectNode}. * * The example above could have used this instead to achieve the same result: * * childEls: [ * { name: 'bodyEl', itemId: 'bodyEl' } * ] * * When using `select`, the property will be an instance of {@link Ext.CompositeElement}. In * all other cases, the property will be an {@link Ext.Element} or `null` if not found. * * Care should be taken when using `select` or `selectNode` to find child elements. The * following issues should be considered: * * - Performance: using selectors can be slower than id lookup by a factor 10x or more. * - Over-selecting: selectors are applied after the DOM elements for all children have * been rendered, so selectors can match elements from child components (including nested * versions of the same component) accidentally. * * This above issues are most important when using `select` since it returns multiple * elements. * * **IMPORTANT** * Unlike a `renderTpl` where there is a single value for an instance, `childEls` are aggregated * up the class hierarchy so that they are effectively inherited. In other words, if a * class where to derive from `Ext.ux.SomeComponent` in the example above, it could also * have a `childEls` property in the same way as `Ext.ux.SomeComponent`. * * Ext.define('Ext.ux.AnotherComponent', { * extend: 'Ext.ux.SomeComponent', * * childEls: [ * // 'bodyEl' is inherited * 'innerEl' * ], * * renderTpl: [ * '
' * '
' * '
' * ], * * // ... * }); * * The `renderTpl` contains both child elements and unites them in the desired markup, but * the `childEls` only contains the new child element. The {@link #applyChildEls} method * takes care of looking up all `childEls` for an instance and considers `childEls` * properties on all the super classes and mixins. * * @private */ Ext.define('Ext.util.ElementContainer', { childEls: [ // empty - this solves a couple problems: // 1. It ensures that all classes have a childEls (avoid null ptr) // 2. It prevents mixins from smashing on their own childEls (these are gathered // specifically) ], constructor: function () { var me = this, childEls; // if we have configured childEls, we need to merge them with those from this // class, its bases and the set of mixins... if (me.hasOwnProperty('childEls')) { childEls = me.childEls; delete me.childEls; me.addChildEls.apply(me, childEls); } }, destroy: function () { var me = this, childEls = me.getChildEls(), child, childName, i, k; for (i = childEls.length; i--; ) { childName = childEls[i]; if (typeof childName != 'string') { childName = childName.name; } child = me[childName]; if (child) { me[childName] = null; // better than delete since that changes the "shape" child.remove(); } } }, /** * Adds each argument passed to this method to the {@link Ext.AbstractComponent#cfg-childEls childEls} array. */ addChildEls: function () { var me = this, args = arguments; if (me.hasOwnProperty('childEls')) { me.childEls.push.apply(me.childEls, args); } else { me.childEls = me.getChildEls().concat(Array.prototype.slice.call(args)); } me.prune(me.childEls, false); }, /** * Sets references to elements inside the component. * @private */ applyChildEls: function(el, id) { var me = this, childEls = me.getChildEls(), baseId, childName, i, selector, value; baseId = (id || me.id) + '-'; for (i = childEls.length; i--; ) { childName = childEls[i]; if (typeof childName == 'string') { // We don't use Ext.get because that is 3x (or more) slower on IE6-8. Since // we know the el's are children of our el we use getById instead: value = el.getById(baseId + childName); } else { if ((selector = childName.select)) { value = Ext.select(selector, true, el.dom); // a CompositeElement } else if ((selector = childName.selectNode)) { value = Ext.get(Ext.DomQuery.selectNode(selector, el.dom)); } else { // see above re:getById... value = el.getById(childName.id || (baseId + childName.itemId)); } childName = childName.name; } me[childName] = value; } }, getChildEls: function () { var me = this, self; // If an instance has its own childEls, that is the complete set: if (me.hasOwnProperty('childEls')) { return me.childEls; } // Typically, however, the childEls is a class-level concept, so check to see if // we have cached the complete set on the class: self = me.self; return self.$childEls || me.getClassChildEls(self); }, getClassChildEls: function (cls) { var me = this, result = cls.$childEls, childEls, i, length, forked, mixin, mixins, name, parts, proto, supr, superMixins; if (!result) { // We put the various childEls arrays into parts in the order of superclass, // new mixins and finally from cls. These parts can be null or undefined and // we will skip them later. supr = cls.superclass; if (supr) { supr = supr.self; parts = [supr.$childEls || me.getClassChildEls(supr)]; // super+mixins superMixins = supr.prototype.mixins || {}; } else { parts = []; superMixins = {}; } proto = cls.prototype; mixins = proto.mixins; // since we are a mixin, there will be at least us for (name in mixins) { if (mixins.hasOwnProperty(name) && !superMixins.hasOwnProperty(name)) { mixin = mixins[name].self; parts.push(mixin.$childEls || me.getClassChildEls(mixin)); } } parts.push(proto.hasOwnProperty('childEls') && proto.childEls); for (i = 0, length = parts.length; i < length; ++i) { childEls = parts[i]; if (childEls && childEls.length) { if (!result) { result = childEls; } else { if (!forked) { forked = true; result = result.slice(0); } result.push.apply(result, childEls); } } } cls.$childEls = result = (result ? me.prune(result, !forked) : []); } return result; }, prune: function (childEls, shared) { var index = childEls.length, map = {}, name; while (index--) { name = childEls[index]; if (typeof name != 'string') { name = name.name; } if (!map[name]) { map[name] = 1; } else { if (shared) { shared = false; childEls = childEls.slice(0); } Ext.Array.erase(childEls, index, 1); } } return childEls; }, /** * Removes items in the childEls array based on the return value of a supplied test * function. The function is called with a entry in childEls and if the test function * return true, that entry is removed. If false, that entry is kept. * * @param {Function} testFn The test function. */ removeChildEls: function (testFn) { var me = this, old = me.getChildEls(), keepers = (me.childEls = []), n, i, cel; for (i = 0, n = old.length; i < n; ++i) { cel = old[i]; if (!testFn(cel)) { keepers.push(cel); } } } }); /** * A wrapper class which can be applied to any element. Fires a "click" event while the * mouse is pressed. The interval between firings may be specified in the config but * defaults to 20 milliseconds. * * Optionally, a CSS class may be applied to the element during the time it is pressed. */ Ext.define('Ext.util.ClickRepeater', { extend: 'Ext.util.Observable', /** * Creates new ClickRepeater. * @param {String/HTMLElement/Ext.Element} el The element or its ID to listen on * @param {Object} [config] Config object. */ constructor : function(el, config){ var me = this; me.el = Ext.get(el); me.el.unselectable(); Ext.apply(me, config); me.callParent(); me.addEvents( /** * @event mousedown * Fires when the mouse button is depressed. * @param {Ext.util.ClickRepeater} this * @param {Ext.EventObject} e */ "mousedown", /** * @event click * Fires on a specified interval during the time the element is pressed. * @param {Ext.util.ClickRepeater} this * @param {Ext.EventObject} e */ "click", /** * @event mouseup * Fires when the mouse key is released. * @param {Ext.util.ClickRepeater} this * @param {Ext.EventObject} e */ "mouseup" ); if(!me.disabled){ me.disabled = true; me.enable(); } // allow inline handler if(me.handler){ me.on("click", me.handler, me.scope || me); } }, /** * @cfg {String/HTMLElement/Ext.Element} el * The element to act as a button. */ /** * @cfg {String} pressedCls * A CSS class name to be applied to the element while pressed. */ /** * @cfg {Boolean} accelerate * True if autorepeating should start slowly and accelerate. * "interval" and "delay" are ignored. */ /** * @cfg {Number} interval * The interval between firings of the "click" event (in milliseconds). */ interval : 20, /** * @cfg {Number} delay * The initial delay before the repeating event begins firing. * Similar to an autorepeat key delay. */ delay: 250, /** * @cfg {Boolean} preventDefault * True to prevent the default click event */ preventDefault : true, /** * @cfg {Boolean} stopDefault * True to stop the default click event */ stopDefault : false, timer : 0, /** * Enables the repeater and allows events to fire. */ enable: function(){ if(this.disabled){ this.el.on('mousedown', this.handleMouseDown, this); // IE versions will detect clicks as in sequence as dblclicks // if they happen in quick succession if (Ext.isIE && !(Ext.isStrict && Ext.isIE9)){ this.el.on('dblclick', this.handleDblClick, this); } if(this.preventDefault || this.stopDefault){ this.el.on('click', this.eventOptions, this); } } this.disabled = false; }, /** * Disables the repeater and stops events from firing. */ disable: function(/* private */ force){ if(force || !this.disabled){ clearTimeout(this.timer); if(this.pressedCls){ this.el.removeCls(this.pressedCls); } Ext.getDoc().un('mouseup', this.handleMouseUp, this); this.el.removeAllListeners(); } this.disabled = true; }, /** * Convenience function for setting disabled/enabled by boolean. * @param {Boolean} disabled */ setDisabled: function(disabled){ this[disabled ? 'disable' : 'enable'](); }, eventOptions: function(e){ if(this.preventDefault){ e.preventDefault(); } if(this.stopDefault){ e.stopEvent(); } }, // private destroy : function() { this.disable(true); Ext.destroy(this.el); this.clearListeners(); }, handleDblClick : function(e){ clearTimeout(this.timer); this.el.blur(); this.fireEvent("mousedown", this, e); this.fireEvent("click", this, e); }, // private handleMouseDown : function(e){ clearTimeout(this.timer); this.el.blur(); if(this.pressedCls){ this.el.addCls(this.pressedCls); } this.mousedownTime = new Date(); Ext.getDoc().on("mouseup", this.handleMouseUp, this); this.el.on("mouseout", this.handleMouseOut, this); this.fireEvent("mousedown", this, e); this.fireEvent("click", this, e); // Do not honor delay or interval if acceleration wanted. if (this.accelerate) { this.delay = 400; } // Re-wrap the event object in a non-shared object, so it doesn't lose its context if // the global shared EventObject gets a new Event put into it before the timer fires. e = new Ext.EventObjectImpl(e); this.timer = Ext.defer(this.click, this.delay || this.interval, this, [e]); }, // private click : function(e){ this.fireEvent("click", this, e); this.timer = Ext.defer(this.click, this.accelerate ? this.easeOutExpo(Ext.Date.getElapsed(this.mousedownTime), 400, -390, 12000) : this.interval, this, [e]); }, easeOutExpo : function (t, b, c, d) { return (t==d) ? b+c : c * (-Math.pow(2, -10 * t/d) + 1) + b; }, // private handleMouseOut : function(){ clearTimeout(this.timer); if(this.pressedCls){ this.el.removeCls(this.pressedCls); } this.el.on("mouseover", this.handleMouseReturn, this); }, // private handleMouseReturn : function(){ this.el.un("mouseover", this.handleMouseReturn, this); if(this.pressedCls){ this.el.addCls(this.pressedCls); } this.click(); }, // private handleMouseUp : function(e){ clearTimeout(this.timer); this.el.un("mouseover", this.handleMouseReturn, this); this.el.un("mouseout", this.handleMouseOut, this); Ext.getDoc().un("mouseup", this.handleMouseUp, this); if(this.pressedCls){ this.el.removeCls(this.pressedCls); } this.fireEvent("mouseup", this, e); } }); /** * Represents an HTML fragment template. Templates may be {@link #compile precompiled} for greater performance. * * An instance of this class may be created by passing to the constructor either a single argument, or multiple * arguments: * * # Single argument: String/Array * * The single argument may be either a String or an Array: * * - String: * * var t = new Ext.Template("
Hello {0}.
"); * t.{@link #append}('some-element', ['foo']); * * - Array: * * An Array will be combined with `join('')`. * * var t = new Ext.Template([ * '
', * '{name:trim} {value:ellipsis(10)}', * '
', * ]); * t.{@link #compile}(); * t.{@link #append}('some-element', {id: 'myid', cls: 'myclass', name: 'foo', value: 'bar'}); * * # Multiple arguments: String, Object, Array, ... * * Multiple arguments will be combined with `join('')`. * * var t = new Ext.Template( * '
', * '{name} {value}', * '
', * // a configuration object: * { * compiled: true, // {@link #compile} immediately * } * ); * * # Notes * * - For a list of available format functions, see {@link Ext.util.Format}. * - `disableFormats` reduces `{@link #apply}` time when no formatting is required. */ Ext.define('Ext.Template', { /* Begin Definitions */ requires: ['Ext.dom.Helper', 'Ext.util.Format'], inheritableStatics: { /** * Creates a template from the passed element's value (_display:none_ textarea, preferred) or innerHTML. * @param {String/HTMLElement} el A DOM element or its id * @param {Object} config (optional) Config object * @return {Ext.Template} The created template * @static * @inheritable */ from: function(el, config) { el = Ext.getDom(el); return new this(el.value || el.innerHTML, config || ''); } }, /* End Definitions */ /** * Creates new template. * * @param {String...} html List of strings to be concatenated into template. * Alternatively an array of strings can be given, but then no config object may be passed. * @param {Object} config (optional) Config object */ constructor: function(html) { var me = this, args = arguments, buffer = [], i = 0, length = args.length, value; me.initialConfig = {}; // Allow an array to be passed here so we can // pass an array of strings and an object // at the end if (length === 1 && Ext.isArray(html)) { args = html; length = args.length; } if (length > 1) { for (; i < length; i++) { value = args[i]; if (typeof value == 'object') { Ext.apply(me.initialConfig, value); Ext.apply(me, value); } else { buffer.push(value); } } } else { buffer.push(html); } // @private me.html = buffer.join(''); if (me.compiled) { me.compile(); } }, /** * @property {Boolean} isTemplate * `true` in this class to identify an object as an instantiated Template, or subclass thereof. */ isTemplate: true, /** * @cfg {Boolean} compiled * True to immediately compile the template. Defaults to false. */ /** * @cfg {Boolean} disableFormats * True to disable format functions in the template. If the template doesn't contain * format functions, setting disableFormats to true will reduce apply time. Defaults to false. */ disableFormats: false, re: /\{([\w\-]+)(?:\:([\w\.]*)(?:\((.*?)?\))?)?\}/g, /** * Returns an HTML fragment of this template with the specified values applied. * * @param {Object/Array} values The template values. Can be an array if your params are numeric: * * var tpl = new Ext.Template('Name: {0}, Age: {1}'); * tpl.apply(['John', 25]); * * or an object: * * var tpl = new Ext.Template('Name: {name}, Age: {age}'); * tpl.apply({name: 'John', age: 25}); * * @return {String} The HTML fragment */ apply: function(values) { var me = this, useFormat = me.disableFormats !== true, fm = Ext.util.Format, tpl = me, ret; if (me.compiled) { return me.compiled(values).join(''); } function fn(m, name, format, args) { if (format && useFormat) { if (args) { args = [values[name]].concat(Ext.functionFactory('return ['+ args +'];')()); } else { args = [values[name]]; } if (format.substr(0, 5) == "this.") { return tpl[format.substr(5)].apply(tpl, args); } else { return fm[format].apply(fm, args); } } else { return values[name] !== undefined ? values[name] : ""; } } ret = me.html.replace(me.re, fn); return ret; }, /** * Appends the result of this template to the provided output array. * @param {Object/Array} values The template values. See {@link #apply}. * @param {Array} out The array to which output is pushed. * @return {Array} The given out array. */ applyOut: function(values, out) { var me = this; if (me.compiled) { out.push.apply(out, me.compiled(values)); } else { out.push(me.apply(values)); } return out; }, /** * @method applyTemplate * @member Ext.Template * Alias for {@link #apply}. * @inheritdoc Ext.Template#apply */ applyTemplate: function () { return this.apply.apply(this, arguments); }, /** * Sets the HTML used as the template and optionally compiles it. * @param {String} html * @param {Boolean} compile (optional) True to compile the template. * @return {Ext.Template} this */ set: function(html, compile) { var me = this; me.html = html; me.compiled = null; return compile ? me.compile() : me; }, compileARe: /\\/g, compileBRe: /(\r\n|\n)/g, compileCRe: /'/g, /** * Compiles the template into an internal function, eliminating the RegEx overhead. * @return {Ext.Template} this */ compile: function() { var me = this, fm = Ext.util.Format, useFormat = me.disableFormats !== true, body, bodyReturn; function fn(m, name, format, args) { if (format && useFormat) { args = args ? ',' + args: ""; if (format.substr(0, 5) != "this.") { format = "fm." + format + '('; } else { format = 'this.' + format.substr(5) + '('; } } else { args = ''; format = "(values['" + name + "'] == undefined ? '' : "; } return "'," + format + "values['" + name + "']" + args + ") ,'"; } bodyReturn = me.html.replace(me.compileARe, '\\\\').replace(me.compileBRe, '\\n').replace(me.compileCRe, "\\'").replace(me.re, fn); body = "this.compiled = function(values){ return ['" + bodyReturn + "'];};"; eval(body); return me; }, /** * Applies the supplied values to the template and inserts the new node(s) as the first child of el. * * @param {String/HTMLElement/Ext.Element} el The context element * @param {Object/Array} values The template values. See {@link #applyTemplate} for details. * @param {Boolean} returnElement (optional) true to return a Ext.Element. * @return {HTMLElement/Ext.Element} The new node or Element */ insertFirst: function(el, values, returnElement) { return this.doInsert('afterBegin', el, values, returnElement); }, /** * Applies the supplied values to the template and inserts the new node(s) before el. * * @param {String/HTMLElement/Ext.Element} el The context element * @param {Object/Array} values The template values. See {@link #applyTemplate} for details. * @param {Boolean} returnElement (optional) true to return a Ext.Element. * @return {HTMLElement/Ext.Element} The new node or Element */ insertBefore: function(el, values, returnElement) { return this.doInsert('beforeBegin', el, values, returnElement); }, /** * Applies the supplied values to the template and inserts the new node(s) after el. * * @param {String/HTMLElement/Ext.Element} el The context element * @param {Object/Array} values The template values. See {@link #applyTemplate} for details. * @param {Boolean} returnElement (optional) true to return a Ext.Element. * @return {HTMLElement/Ext.Element} The new node or Element */ insertAfter: function(el, values, returnElement) { return this.doInsert('afterEnd', el, values, returnElement); }, /** * Applies the supplied `values` to the template and appends the new node(s) to the specified `el`. * * For example usage see {@link Ext.Template Ext.Template class docs}. * * @param {String/HTMLElement/Ext.Element} el The context element * @param {Object/Array} values The template values. See {@link #applyTemplate} for details. * @param {Boolean} returnElement (optional) true to return an Ext.Element. * @return {HTMLElement/Ext.Element} The new node or Element */ append: function(el, values, returnElement) { return this.doInsert('beforeEnd', el, values, returnElement); }, doInsert: function(where, el, values, returnElement) { var newNode = Ext.DomHelper.insertHtml(where, Ext.getDom(el), this.apply(values)); return returnElement ? Ext.get(newNode) : newNode; }, /** * Applies the supplied values to the template and overwrites the content of el with the new node(s). * * @param {String/HTMLElement/Ext.Element} el The context element * @param {Object/Array} values The template values. See {@link #applyTemplate} for details. * @param {Boolean} returnElement (optional) true to return a Ext.Element. * @return {HTMLElement/Ext.Element} The new node or Element */ overwrite: function(el, values, returnElement) { var newNode = Ext.DomHelper.overwrite(Ext.getDom(el), this.apply(values)); return returnElement ? Ext.get(newNode) : newNode; } }); /** * A mixin for {@link Ext.container.Container} components that are likely to have form fields in their * items subtree. Adds the following capabilities: * * - Methods for handling the addition and removal of {@link Ext.form.Labelable} and {@link Ext.form.field.Field} * instances at any depth within the container. * - Events ({@link #fieldvaliditychange} and {@link #fielderrorchange}) for handling changes to the state * of individual fields at the container level. * - Automatic application of {@link #fieldDefaults} config properties to each field added within the * container, to facilitate uniform configuration of all fields. * * This mixin is primarily for internal use by {@link Ext.form.Panel} and {@link Ext.form.FieldContainer}, * and should not normally need to be used directly. @docauthor Jason Johnston */ Ext.define('Ext.form.FieldAncestor', { /** * @cfg {Object} fieldDefaults * If specified, the properties in this object are used as default config values for each {@link Ext.form.Labelable} * instance (e.g. {@link Ext.form.field.Base} or {@link Ext.form.FieldContainer}) that is added as a descendant of * this container. Corresponding values specified in an individual field's own configuration, or from the {@link * Ext.container.Container#defaults defaults config} of its parent container, will take precedence. See the * documentation for {@link Ext.form.Labelable} to see what config options may be specified in the fieldDefaults. * * Example: * * new Ext.form.Panel({ * fieldDefaults: { * labelAlign: 'left', * labelWidth: 100 * }, * items: [{ * xtype: 'fieldset', * defaults: { * labelAlign: 'top' * }, * items: [{ * name: 'field1' * }, { * name: 'field2' * }] * }, { * xtype: 'fieldset', * items: [{ * name: 'field3', * labelWidth: 150 * }, { * name: 'field4' * }] * }] * }); * * In this example, field1 and field2 will get labelAlign:'top' (from the fieldset's defaults) and labelWidth:100 * (from fieldDefaults), field3 and field4 will both get labelAlign:'left' (from fieldDefaults and field3 will use * the labelWidth:150 from its own config. */ /** * Initializes the FieldAncestor's state; this must be called from the initComponent method of any components * importing this mixin. * @protected */ initFieldAncestor: function() { var me = this, onSubtreeChange = me.onFieldAncestorSubtreeChange; me.addEvents( /** * @event fieldvaliditychange * Fires when the validity state of any one of the {@link Ext.form.field.Field} instances within this * container changes. * @param {Ext.form.FieldAncestor} this * @param {Ext.form.Labelable} The Field instance whose validity changed * @param {String} isValid The field's new validity state */ 'fieldvaliditychange', /** * @event fielderrorchange * Fires when the active error message is changed for any one of the {@link Ext.form.Labelable} instances * within this container. * @param {Ext.form.FieldAncestor} this * @param {Ext.form.Labelable} The Labelable instance whose active error was changed * @param {String} error The active error message */ 'fielderrorchange' ); // Catch addition and removal of descendant fields me.on('add', onSubtreeChange, me); me.on('remove', onSubtreeChange, me); me.initFieldDefaults(); }, /** * @private Initialize the {@link #fieldDefaults} object */ initFieldDefaults: function() { if (!this.fieldDefaults) { this.fieldDefaults = {}; } }, /** * @private * Handle the addition and removal of components in the FieldAncestor component's child tree. */ onFieldAncestorSubtreeChange: function(parent, child) { var me = this, isAdding = !!child.ownerCt; function handleCmp(cmp) { var isLabelable = cmp.isFieldLabelable, isField = cmp.isFormField; if (isLabelable || isField) { if (isLabelable) { me['onLabelable' + (isAdding ? 'Added' : 'Removed')](cmp); } if (isField) { me['onField' + (isAdding ? 'Added' : 'Removed')](cmp); } } else if (cmp.isContainer) { Ext.Array.forEach(cmp.getRefItems(), handleCmp); } } handleCmp(child); }, /** * Called when a {@link Ext.form.Labelable} instance is added to the container's subtree. * @param {Ext.form.Labelable} labelable The instance that was added * @protected */ onLabelableAdded: function(labelable) { var me = this; // buffer slightly to avoid excessive firing while sub-fields are changing en masse me.mon(labelable, 'errorchange', me.handleFieldErrorChange, me, {buffer: 10}); labelable.setFieldDefaults(me.fieldDefaults); }, /** * Called when a {@link Ext.form.field.Field} instance is added to the container's subtree. * @param {Ext.form.field.Field} field The field which was added * @protected */ onFieldAdded: function(field) { var me = this; me.mon(field, 'validitychange', me.handleFieldValidityChange, me); }, /** * Called when a {@link Ext.form.Labelable} instance is removed from the container's subtree. * @param {Ext.form.Labelable} labelable The instance that was removed * @protected */ onLabelableRemoved: function(labelable) { var me = this; me.mun(labelable, 'errorchange', me.handleFieldErrorChange, me); }, /** * Called when a {@link Ext.form.field.Field} instance is removed from the container's subtree. * @param {Ext.form.field.Field} field The field which was removed * @protected */ onFieldRemoved: function(field) { var me = this; me.mun(field, 'validitychange', me.handleFieldValidityChange, me); }, /** * @private Handle validitychange events on sub-fields; invoke the aggregated event and method */ handleFieldValidityChange: function(field, isValid) { var me = this; me.fireEvent('fieldvaliditychange', me, field, isValid); me.onFieldValidityChange(field, isValid); }, /** * @private Handle errorchange events on sub-fields; invoke the aggregated event and method */ handleFieldErrorChange: function(labelable, activeError) { var me = this; me.fireEvent('fielderrorchange', me, labelable, activeError); me.onFieldErrorChange(labelable, activeError); }, /** * Fired when the validity of any field within the container changes. * @param {Ext.form.field.Field} field The sub-field whose validity changed * @param {Boolean} valid The new validity state * @protected */ onFieldValidityChange: Ext.emptyFn, /** * Fired when the error message of any field within the container changes. * @param {Ext.form.Labelable} field The sub-field whose active error changed * @param {String} error The new active error message * @protected */ onFieldErrorChange: Ext.emptyFn }); /** * Provides easy access to all drag drop components that are registered on a page. Items can be retrieved either * directly by DOM node id, or by passing in the drag drop event that occurred and looking up the event target. */ Ext.define('Ext.dd.Registry', { singleton: true, constructor: function() { this.elements = {}; this.handles = {}; this.autoIdSeed = 0; }, getId: function(el, autogen){ if(typeof el == "string"){ return el; } var id = el.id; if(!id && autogen !== false){ id = "extdd-" + (++this.autoIdSeed); el.id = id; } return id; }, /** * Registers a drag drop element. * * @param {String/HTMLElement} element The id or DOM node to register * @param {Object} data An custom data object that will be passed between the elements that are involved in drag * drop operations. You can populate this object with any arbitrary properties that your own code knows how to * interpret, plus there are some specific properties known to the Registry that should be populated in the data * object (if applicable): * @param {HTMLElement[]} data.handles Array of DOM nodes that trigger dragging for the element being registered. * @param {Boolean} data.isHandle True if the element passed in triggers dragging itself, else false. */ register : function(el, data){ data = data || {}; if (typeof el == "string") { el = document.getElementById(el); } data.ddel = el; this.elements[this.getId(el)] = data; if (data.isHandle !== false) { this.handles[data.ddel.id] = data; } if (data.handles) { var hs = data.handles, i, len; for (i = 0, len = hs.length; i < len; i++) { this.handles[this.getId(hs[i])] = data; } } }, /** * Unregister a drag drop element * @param {String/HTMLElement} element The id or DOM node to unregister */ unregister : function(el){ var id = this.getId(el, false), data = this.elements[id], hs, i, len; if(data){ delete this.elements[id]; if(data.handles){ hs = data.handles; for (i = 0, len = hs.length; i < len; i++) { delete this.handles[this.getId(hs[i], false)]; } } } }, /** * Returns the handle registered for a DOM Node by id * @param {String/HTMLElement} id The DOM node or id to look up * @return {Object} handle The custom handle data */ getHandle : function(id){ if(typeof id != "string"){ // must be element? id = id.id; } return this.handles[id]; }, /** * Returns the handle that is registered for the DOM node that is the target of the event * @param {Event} e The event * @return {Object} handle The custom handle data */ getHandleFromEvent : function(e){ var t = e.getTarget(); return t ? this.handles[t.id] : null; }, /** * Returns a custom data object that is registered for a DOM node by id * @param {String/HTMLElement} id The DOM node or id to look up * @return {Object} data The custom data */ getTarget : function(id){ if(typeof id != "string"){ // must be element? id = id.id; } return this.elements[id]; }, /** * Returns a custom data object that is registered for the DOM node that is the target of the event * @param {Event} e The event * @return {Object} data The custom data */ getTargetFromEvent : function(e){ var t = e.getTarget(); return t ? this.elements[t.id] || this.handles[t.id] : null; } }); /** * @private */ Ext.define('Ext.util.Offset', { /* Begin Definitions */ statics: { fromObject: function(obj) { return new this(obj.x, obj.y); } }, /* End Definitions */ constructor: function(x, y) { this.x = (x != null && !isNaN(x)) ? x : 0; this.y = (y != null && !isNaN(y)) ? y : 0; return this; }, copy: function() { return new Ext.util.Offset(this.x, this.y); }, copyFrom: function(p) { this.x = p.x; this.y = p.y; }, toString: function() { return "Offset[" + this.x + "," + this.y + "]"; }, equals: function(offset) { if(!(offset instanceof this.statics())) { Ext.Error.raise('Offset must be an instance of Ext.util.Offset'); } return (this.x == offset.x && this.y == offset.y); }, round: function(to) { if (!isNaN(to)) { var factor = Math.pow(10, to); this.x = Math.round(this.x * factor) / factor; this.y = Math.round(this.y * factor) / factor; } else { this.x = Math.round(this.x); this.y = Math.round(this.y); } }, isZero: function() { return this.x == 0 && this.y == 0; } }); /** * @class Ext.chart.Callout * A mixin providing callout functionality for Ext.chart.series.Series. */ Ext.define('Ext.chart.Callout', { /* Begin Definitions */ /* End Definitions */ constructor: function(config) { if (config.callouts) { config.callouts.styles = Ext.applyIf(config.callouts.styles || {}, { color: "#000", font: "11px Helvetica, sans-serif" }); this.callouts = Ext.apply(this.callouts || {}, config.callouts); this.calloutsArray = []; } }, renderCallouts: function() { if (!this.callouts) { return; } var me = this, items = me.items, animate = me.chart.animate, config = me.callouts, styles = config.styles, group = me.calloutsArray, store = me.chart.store, len = store.getCount(), ratio = items.length / len, previouslyPlacedCallouts = [], i, count, j, p, item, label, storeItem, display; for (i = 0, count = 0; i < len; i++) { for (j = 0; j < ratio; j++) { item = items[count]; label = group[count]; storeItem = store.getAt(i); display = config.filter(storeItem); if (!display && !label) { count++; continue; } if (!label) { group[count] = label = me.onCreateCallout(storeItem, item, i, display, j, count); } for (p in label) { if (label[p] && label[p].setAttributes) { label[p].setAttributes(styles, true); } } if (!display) { for (p in label) { if (label[p]) { if (label[p].setAttributes) { label[p].setAttributes({ hidden: true }, true); } else if(label[p].setVisible) { label[p].setVisible(false); } } } } config.renderer(label, storeItem); me.onPlaceCallout(label, storeItem, item, i, display, animate, j, count, previouslyPlacedCallouts); previouslyPlacedCallouts.push(label); count++; } } this.hideCallouts(count); }, onCreateCallout: function(storeItem, item, i, display) { var me = this, group = me.calloutsGroup, config = me.callouts, styles = config.styles, width = styles.width, height = styles.height, chart = me.chart, surface = chart.surface, calloutObj = { //label: false, //box: false, lines: false }; calloutObj.lines = surface.add(Ext.apply({}, { type: 'path', path: 'M0,0', stroke: me.getLegendColor() || '#555' }, styles)); if (config.items) { calloutObj.panel = new Ext.Panel({ style: "position: absolute;", width: width, height: height, items: config.items, renderTo: chart.el }); } return calloutObj; }, hideCallouts: function(index) { var calloutsArray = this.calloutsArray, len = calloutsArray.length, co, p; while (len-->index) { co = calloutsArray[len]; for (p in co) { if (co[p]) { co[p].hide(true); } } } } }); /** * @private */ Ext.define('Ext.chart.Shape', { /* Begin Definitions */ singleton: true, /* End Definitions */ circle: function (surface, opts) { return surface.add(Ext.apply({ type: 'circle', x: opts.x, y: opts.y, stroke: null, radius: opts.radius }, opts)); }, line: function (surface, opts) { return surface.add(Ext.apply({ type: 'rect', x: opts.x - opts.radius, y: opts.y - opts.radius, height: 2 * opts.radius, width: 2 * opts.radius / 5 }, opts)); }, square: function (surface, opts) { return surface.add(Ext.applyIf({ type: 'rect', x: opts.x - opts.radius, y: opts.y - opts.radius, height: 2 * opts.radius, width: 2 * opts.radius, radius: null }, opts)); }, triangle: function (surface, opts) { opts.radius *= 1.75; return surface.add(Ext.apply({ type: 'path', stroke: null, path: "M".concat(opts.x, ",", opts.y, "m0-", opts.radius * 0.58, "l", opts.radius * 0.5, ",", opts.radius * 0.87, "-", opts.radius, ",0z") }, opts)); }, diamond: function (surface, opts) { var r = opts.radius; r *= 1.5; return surface.add(Ext.apply({ type: 'path', stroke: null, path: ["M", opts.x, opts.y - r, "l", r, r, -r, r, -r, -r, r, -r, "z"] }, opts)); }, cross: function (surface, opts) { var r = opts.radius; r = r / 1.7; return surface.add(Ext.apply({ type: 'path', stroke: null, path: "M".concat(opts.x - r, ",", opts.y, "l", [-r, -r, r, -r, r, r, r, -r, r, r, -r, r, r, r, -r, r, -r, -r, -r, r, -r, -r, "z"]) }, opts)); }, plus: function (surface, opts) { var r = opts.radius / 1.3; return surface.add(Ext.apply({ type: 'path', stroke: null, path: "M".concat(opts.x - r / 2, ",", opts.y - r / 2, "l", [0, -r, r, 0, 0, r, r, 0, 0, r, -r, 0, 0, r, -r, 0, 0, -r, -r, 0, 0, -r, "z"]) }, opts)); }, arrow: function (surface, opts) { var r = opts.radius; return surface.add(Ext.apply({ type: 'path', path: "M".concat(opts.x - r * 0.7, ",", opts.y - r * 0.4, "l", [r * 0.6, 0, 0, -r * 0.4, r, r * 0.8, -r, r * 0.8, 0, -r * 0.4, -r * 0.6, 0], "z") }, opts)); }, drop: function (surface, x, y, text, size, angle) { size = size || 30; angle = angle || 0; surface.add({ type: 'path', path: ['M', x, y, 'l', size, 0, 'A', size * 0.4, size * 0.4, 0, 1, 0, x + size * 0.7, y - size * 0.7, 'z'], fill: '#000', stroke: 'none', rotate: { degrees: 22.5 - angle, x: x, y: y } }); angle = (angle + 90) * Math.PI / 180; surface.add({ type: 'text', x: x + size * Math.sin(angle) - 10, // Shift here, Not sure why. y: y + size * Math.cos(angle) + 5, text: text, 'font-size': size * 12 / 40, stroke: 'none', fill: '#fff' }); } }); /** * A class that manages a group of {@link Ext.Component#floating} Components and provides z-order management, * and Component activation behavior, including masking below the active (topmost) Component. * * {@link Ext.Component#floating Floating} Components which are rendered directly into the document (such as * {@link Ext.window.Window Window}s) which are {@link Ext.Component#method-show show}n are managed by a * {@link Ext.WindowManager global instance}. * * {@link Ext.Component#floating Floating} Components which are descendants of {@link Ext.Component#floating floating} * *Containers* (for example a {@link Ext.view.BoundList BoundList} within an {@link Ext.window.Window Window}, * or a {@link Ext.menu.Menu Menu}), are managed by a ZIndexManager owned by that floating Container. Therefore * ComboBox dropdowns within Windows will have managed z-indices guaranteed to be correct, relative to the Window. */ Ext.define('Ext.ZIndexManager', { alternateClassName: 'Ext.WindowGroup', statics: { zBase : 9000 }, constructor: function(container) { var me = this; me.list = {}; me.zIndexStack = []; me.front = null; if (container) { // This is the ZIndexManager for an Ext.container.Container, base its zseed on the zIndex of the Container's element if (container.isContainer) { container.on('resize', me._onContainerResize, me); me.zseed = Ext.Number.from(me.rendered ? container.getEl().getStyle('zIndex') : undefined, me.getNextZSeed()); // The containing element we will be dealing with (eg masking) is the content target me.targetEl = container.getTargetEl(); me.container = container; } // This is the ZIndexManager for a DOM element else { Ext.EventManager.onWindowResize(me._onContainerResize, me); me.zseed = me.getNextZSeed(); me.targetEl = Ext.get(container); } } // No container passed means we are the global WindowManager. Our target is the doc body. // DOM must be ready to collect that ref. else { Ext.EventManager.onWindowResize(me._onContainerResize, me); me.zseed = me.getNextZSeed(); Ext.onDocumentReady(function() { me.targetEl = Ext.getBody(); }); } }, getNextZSeed: function() { return (Ext.ZIndexManager.zBase += 10000); }, setBase: function(baseZIndex) { this.zseed = baseZIndex; var result = this.assignZIndices(); this._activateLast(); return result; }, // private assignZIndices: function() { var a = this.zIndexStack, len = a.length, i = 0, zIndex = this.zseed, comp; for (; i < len; i++) { comp = a[i]; if (comp && !comp.hidden) { // Setting the zIndex of a Component returns the topmost zIndex consumed by // that Component. // If it's just a plain floating Component such as a BoundList, then the // return value is the passed value plus 10, ready for the next item. // If a floating *Container* has its zIndex set, it re-orders its managed // floating children, starting from that new base, and returns a value 10000 above // the highest zIndex which it allocates. zIndex = comp.setZIndex(zIndex); } } // Activate new topmost this._activateLast(); return zIndex; }, // private _setActiveChild: function(comp, oldFront) { var front = this.front; if (comp !== front) { if (front && !front.destroying) { front.setActive(false, comp); } this.front = comp; if (comp && comp != oldFront) { comp.setActive(true); if (comp.modal) { this._showModalMask(comp); } } } }, onComponentHide: function(comp){ comp.setActive(false); this._activateLast(); }, // private _activateLast: function() { var me = this, stack = me.zIndexStack, i = stack.length - 1, oldFront = me.front, comp; // There may be no visible floater to activate me.front = undefined; // Go down through the z-index stack. // Activate the next visible one down. // If that was modal, then we're done for (; i >= 0 && stack[i].hidden; --i); if ((comp = stack[i])) { me._setActiveChild(comp, oldFront); if (comp.modal) { return; } } // If the new top one was not modal, keep going down to find the next visible // modal one to shift the modal mask down under for (; i >= 0; --i) { comp = stack[i]; // If we find a visible modal further down the zIndex stack, move the mask to just under it. if (comp.isVisible() && comp.modal) { me._showModalMask(comp); return; } } // No visible modal Component was found in the run down the stack. // So hide the modal mask me._hideModalMask(); }, _showModalMask: function(comp) { var me = this, zIndex = comp.el.getStyle('zIndex') - 4, maskTarget = comp.floatParent ? comp.floatParent.getTargetEl() : comp.container, viewSize = maskTarget.getBox(); if (maskTarget.dom === document.body) { viewSize.height = Math.max(document.body.scrollHeight, Ext.dom.Element.getDocumentHeight()); viewSize.width = Math.max(document.body.scrollWidth, viewSize.width); } if (!me.mask) { me.mask = Ext.getBody().createChild({ cls: Ext.baseCSSPrefix + 'mask' }); me.mask.setVisibilityMode(Ext.Element.DISPLAY); me.mask.on('click', me._onMaskClick, me); } me.mask.maskTarget = maskTarget; maskTarget.addCls(Ext.baseCSSPrefix + 'body-masked'); me.mask.setStyle('zIndex', zIndex); // setting mask box before showing it in an IE7 strict iframe within a quirks page // can cause body scrolling [EXTJSIV-6219] me.mask.show(); me.mask.setBox(viewSize); }, _hideModalMask: function() { var mask = this.mask; if (mask && mask.isVisible()) { mask.maskTarget.removeCls(Ext.baseCSSPrefix + 'body-masked'); mask.maskTarget = undefined; mask.hide(); } }, _onMaskClick: function() { if (this.front) { this.front.focus(); } }, _onContainerResize: function() { var mask = this.mask, maskTarget, viewSize; if (mask && mask.isVisible()) { // At the new container size, the mask might be *causing* the scrollbar, so to find the valid // client size to mask, we must temporarily unmask the parent node. mask.hide(); maskTarget = mask.maskTarget; if (maskTarget.dom === document.body) { viewSize = { height: Math.max(document.body.scrollHeight, Ext.dom.Element.getDocumentHeight()), width: Math.max(document.body.scrollWidth, document.documentElement.clientWidth) }; } else { viewSize = maskTarget.getViewSize(true); } mask.setSize(viewSize); mask.show(); } }, /** * Registers a floating {@link Ext.Component} with this ZIndexManager. This should not * need to be called under normal circumstances. Floating Components (such as Windows, * BoundLists and Menus) are automatically registered with a * {@link Ext.Component#zIndexManager zIndexManager} at render time. * * Where this may be useful is moving Windows between two ZIndexManagers. For example, * to bring the Ext.MessageBox dialog under the same manager as the Desktop's * ZIndexManager in the desktop sample app: * * MyDesktop.getDesktop().getManager().register(Ext.MessageBox); * * @param {Ext.Component} comp The Component to register. */ register : function(comp) { var me = this; if (comp.zIndexManager) { comp.zIndexManager.unregister(comp); } comp.zIndexManager = me; me.list[comp.id] = comp; me.zIndexStack.push(comp); comp.on('hide', me.onComponentHide, me); }, /** * Unregisters a {@link Ext.Component} from this ZIndexManager. This should not * need to be called. Components are automatically unregistered upon destruction. * See {@link #register}. * @param {Ext.Component} comp The Component to unregister. */ unregister : function(comp) { var me = this, list = me.list; delete comp.zIndexManager; if (list && list[comp.id]) { delete list[comp.id]; comp.un('hide', me.onComponentHide); Ext.Array.remove(me.zIndexStack, comp); // Destruction requires that the topmost visible floater be activated. Same as hiding. me._activateLast(); } }, /** * Gets a registered Component by id. * @param {String/Object} id The id of the Component or a {@link Ext.Component} instance * @return {Ext.Component} */ get : function(id) { return id.isComponent ? id : this.list[id]; }, /** * Brings the specified Component to the front of any other active Components in this ZIndexManager. * @param {String/Object} comp The id of the Component or a {@link Ext.Component} instance * @return {Boolean} True if the dialog was brought to the front, else false * if it was already in front */ bringToFront : function(comp) { var me = this, result = false, zIndexStack = me.zIndexStack; comp = me.get(comp); if (comp !== me.front) { Ext.Array.remove(zIndexStack, comp); if (comp.preventBringToFront) { // this takes care of cases where a load mask should be displayed under a floated component zIndexStack.unshift(comp); } else { // the default behavior is to push onto the stack zIndexStack.push(comp); } me.assignZIndices(); result = true; this.front = comp; } if (result && comp.modal) { me._showModalMask(comp); } return result; }, /** * Sends the specified Component to the back of other active Components in this ZIndexManager. * @param {String/Object} comp The id of the Component or a {@link Ext.Component} instance * @return {Ext.Component} The Component */ sendToBack : function(comp) { var me = this; comp = me.get(comp); Ext.Array.remove(me.zIndexStack, comp); me.zIndexStack.unshift(comp); me.assignZIndices(); this._activateLast(); return comp; }, /** * Hides all Components managed by this ZIndexManager. */ hideAll : function() { var list = this.list, item, id; for (id in list) { if (list.hasOwnProperty(id)) { item = list[id]; if (item.isComponent && item.isVisible()) { item.hide(); } } } }, /** * @private * Temporarily hides all currently visible managed Components. This is for when * dragging a Window which may manage a set of floating descendants in its ZIndexManager; * they should all be hidden just for the duration of the drag. */ hide: function() { var me = this, mask = me.mask, i = 0, stack = me.zIndexStack, len = stack.length, comp; me.tempHidden = me.tempHidden||[]; for (; i < len; i++) { comp = stack[i]; if (comp.isVisible()) { me.tempHidden.push(comp); comp.el.hide(); } } // Also hide modal mask during hidden state if (mask) { mask.hide(); } }, /** * @private * Restores temporarily hidden managed Components to visibility. */ show: function() { var me = this, mask = me.mask, i = 0, tempHidden = me.tempHidden, len = tempHidden ? tempHidden.length : 0, comp; for (; i < len; i++) { comp = tempHidden[i]; comp.el.show(); comp.setPosition(comp.x, comp.y); } me.tempHidden.length = 0; // Also restore mask to visibility and ensure it is aligned with its target element if (mask) { mask.show(); mask.alignTo(mask.maskTarget, 'tl-tl'); } }, /** * Gets the currently-active Component in this ZIndexManager. * @return {Ext.Component} The active Component */ getActive : function() { return this.front; }, /** * Returns zero or more Components in this ZIndexManager using the custom search function passed to this method. * The function should accept a single {@link Ext.Component} reference as its only argument and should * return true if the Component matches the search criteria, otherwise it should return false. * @param {Function} fn The search function * @param {Object} [scope] The scope (this reference) in which the function is executed. * Defaults to the Component being tested. That gets passed to the function if not specified. * @return {Array} An array of zero or more matching windows */ getBy : function(fn, scope) { var r = [], i = 0, stack = this.zIndexStack, len = stack.length, comp; for (; i < len; i++) { comp = stack[i]; if (fn.call(scope||comp, comp) !== false) { r.push(comp); } } return r; }, /** * Executes the specified function once for every Component in this ZIndexManager, passing each * Component as the only parameter. Returning false from the function will stop the iteration. * @param {Function} fn The function to execute for each item * @param {Object} [scope] The scope (this reference) in which the function * is executed. Defaults to the current Component in the iteration. */ each : function(fn, scope) { var list = this.list, id, comp; for (id in list) { if (list.hasOwnProperty(id)) { comp = list[id]; if (comp.isComponent && fn.call(scope || comp, comp) === false) { return; } } } }, /** * Executes the specified function once for every Component in this ZIndexManager, passing each * Component as the only parameter. Returning false from the function will stop the iteration. * The components are passed to the function starting at the bottom and proceeding to the top. * @param {Function} fn The function to execute for each item * @param {Object} scope (optional) The scope (this reference) in which the function * is executed. Defaults to the current Component in the iteration. */ eachBottomUp: function (fn, scope) { var stack = this.zIndexStack, i = 0, len = stack.length, comp; for (; i < len; i++) { comp = stack[i]; if (comp.isComponent && fn.call(scope || comp, comp) === false) { return; } } }, /** * Executes the specified function once for every Component in this ZIndexManager, passing each * Component as the only parameter. Returning false from the function will stop the iteration. * The components are passed to the function starting at the top and proceeding to the bottom. * @param {Function} fn The function to execute for each item * @param {Object} [scope] The scope (this reference) in which the function * is executed. Defaults to the current Component in the iteration. */ eachTopDown: function (fn, scope) { var stack = this.zIndexStack, i = stack.length, comp; for (; i-- > 0; ) { comp = stack[i]; if (comp.isComponent && fn.call(scope || comp, comp) === false) { return; } } }, destroy: function() { var me = this, list = me.list, comp, id; for (id in list) { if (list.hasOwnProperty(id)) { comp = list[id]; if (comp.isComponent) { comp.destroy(); } } } delete me.zIndexStack; delete me.list; delete me.container; delete me.targetEl; } }, function() { /** * @class Ext.WindowManager * @extends Ext.ZIndexManager * * The default global floating Component group that is available automatically. * * This manages instances of floating Components which were rendered programatically without * being added to a {@link Ext.container.Container Container}, and for floating Components * which were added into non-floating Containers. * * *Floating* Containers create their own instance of ZIndexManager, and floating Components * added at any depth below there are managed by that ZIndexManager. * * @singleton */ Ext.WindowManager = Ext.WindowMgr = new this(); }); /** * @class Ext.util.AbstractMixedCollection * @private */ Ext.define('Ext.util.AbstractMixedCollection', { requires: ['Ext.util.Filter'], mixins: { observable: 'Ext.util.Observable' }, /** * @property {Boolean} isMixedCollection * `true` in this class to identify an object as an instantiated MixedCollection, or subclass thereof. */ isMixedCollection: true, /** * @private Mutation counter which is incremented upon add and remove. */ generation: 0, constructor: function(allowFunctions, keyFn) { var me = this; me.items = []; me.map = {}; me.keys = []; me.length = 0; /** * @event clear * Fires when the collection is cleared. */ /** * @event add * Fires when an item is added to the collection. * @param {Number} index The index at which the item was added. * @param {Object} o The item added. * @param {String} key The key associated with the added item. */ /** * @event replace * Fires when an item is replaced in the collection. * @param {String} key he key associated with the new added. * @param {Object} old The item being replaced. * @param {Object} new The new item. */ /** * @event remove * Fires when an item is removed from the collection. * @param {Object} o The item being removed. * @param {String} key (optional) The key associated with the removed item. */ me.allowFunctions = allowFunctions === true; if (keyFn) { me.getKey = keyFn; } me.mixins.observable.constructor.call(me); }, /** * @cfg {Boolean} allowFunctions Specify true if the {@link #addAll} * function should add function references to the collection. Defaults to * false. */ allowFunctions : false, /** * Adds an item to the collection. Fires the {@link #event-add} event when complete. * * @param {String/Object} key The key to associate with the item, or the new item. * * If a {@link #getKey} implementation was specified for this MixedCollection, * or if the key of the stored items is in a property called `id`, * the MixedCollection will be able to *derive* the key for the new item. * In this case just pass the new item in this parameter. * * @param {Object} [o] The item to add. * * @return {Object} The item added. */ add : function(key, obj){ var me = this, myObj = obj, myKey = key, old; if (arguments.length == 1) { myObj = myKey; myKey = me.getKey(myObj); } if (typeof myKey != 'undefined' && myKey !== null) { old = me.map[myKey]; if (typeof old != 'undefined') { return me.replace(myKey, myObj); } me.map[myKey] = myObj; } me.generation++; me.length++; me.items.push(myObj); me.keys.push(myKey); if (me.hasListeners.add) { me.fireEvent('add', me.length - 1, myObj, myKey); } return myObj; }, /** * MixedCollection has a generic way to fetch keys if you implement getKey. The default implementation * simply returns item.id but you can provide your own implementation * to return a different value as in the following examples:

// normal way
var mc = new Ext.util.MixedCollection();
mc.add(someEl.dom.id, someEl);
mc.add(otherEl.dom.id, otherEl);
//and so on

// using getKey
var mc = new Ext.util.MixedCollection();
mc.getKey = function(el){
   return el.dom.id;
};
mc.add(someEl);
mc.add(otherEl);

// or via the constructor
var mc = new Ext.util.MixedCollection(false, function(el){
   return el.dom.id;
});
mc.add(someEl);
mc.add(otherEl);
     * 
* @param {Object} item The item for which to find the key. * @return {Object} The key for the passed item. */ getKey : function(o){ return o.id; }, /** * Replaces an item in the collection. Fires the {@link #event-replace} event when complete. * @param {String} key

The key associated with the item to replace, or the replacement item.

*

If you supplied a {@link #getKey} implementation for this MixedCollection, or if the key * of your stored items is in a property called id, then the MixedCollection * will be able to derive the key of the replacement item. If you want to replace an item * with one having the same key value, then just pass the replacement item in this parameter.

* @param o {Object} o (optional) If the first parameter passed was a key, the item to associate * with that key. * @return {Object} The new item. */ replace : function(key, o){ var me = this, old, index; if (arguments.length == 1) { o = arguments[0]; key = me.getKey(o); } old = me.map[key]; if (typeof key == 'undefined' || key === null || typeof old == 'undefined') { return me.add(key, o); } me.generation++; index = me.indexOfKey(key); me.items[index] = o; me.map[key] = o; if (me.hasListeners.replace) { me.fireEvent('replace', key, old, o); } return o; }, /** * Adds all elements of an Array or an Object to the collection. * @param {Object/Array} objs An Object containing properties which will be added * to the collection, or an Array of values, each of which are added to the collection. * Functions references will be added to the collection if {@link #allowFunctions} * has been set to true. */ addAll : function(objs){ var me = this, i = 0, args, len, key; if (arguments.length > 1 || Ext.isArray(objs)) { args = arguments.length > 1 ? arguments : objs; for (len = args.length; i < len; i++) { me.add(args[i]); } } else { for (key in objs) { if (objs.hasOwnProperty(key)) { if (me.allowFunctions || typeof objs[key] != 'function') { me.add(key, objs[key]); } } } } }, /** * Executes the specified function once for every item in the collection. * The function should return a boolean value. * Returning false from the function will stop the iteration. * * @param {Function} fn The function to execute for each item. * @param {Mixed} fn.item The collection item. * @param {Number} fn.index The index of item. * @param {Number} fn.len Total length of collection. * @param {Object} scope (optional) The scope (this reference) * in which the function is executed. Defaults to the current item in the iteration. */ each : function(fn, scope){ var items = [].concat(this.items), // each safe for removal i = 0, len = items.length, item; for (; i < len; i++) { item = items[i]; if (fn.call(scope || item, item, i, len) === false) { break; } } }, /** * Executes the specified function once for every key in the collection, passing each * key, and its associated item as the first two parameters. * @param {Function} fn The function to execute for each item. * @param {String} fn.key The key of collection item. * @param {Mixed} fn.item The collection item. * @param {Number} fn.index The index of item. * @param {Number} fn.len Total length of collection. * @param {Object} scope (optional) The scope (this reference) in which the * function is executed. Defaults to the browser window. */ eachKey : function(fn, scope){ var keys = this.keys, items = this.items, i = 0, len = keys.length; for (; i < len; i++) { fn.call(scope || window, keys[i], items[i], i, len); } }, /** * Returns the first item in the collection which elicits a true return value from the * passed selection function. * @param {Function} fn The selection function to execute for each item. * @param {Mixed} fn.item The collection item. * @param {String} fn.key The key of collection item. * @param {Object} scope (optional) The scope (this reference) in which the * function is executed. Defaults to the browser window. * @return {Object} The first item in the collection which returned true from the selection * function, or null if none was found. */ findBy : function(fn, scope) { var keys = this.keys, items = this.items, i = 0, len = items.length; for (; i < len; i++) { if (fn.call(scope || window, items[i], keys[i])) { return items[i]; } } return null; }, find : function() { if (Ext.isDefined(Ext.global.console)) { Ext.global.console.warn('Ext.util.MixedCollection: find has been deprecated. Use findBy instead.'); } return this.findBy.apply(this, arguments); }, /** * Inserts an item at the specified index in the collection. Fires the {@link #event-add} event when complete. * @param {Number} index The index to insert the item at. * @param {String} key The key to associate with the new item, or the item itself. * @param {Object} o (optional) If the second parameter was a key, the new item. * @return {Object} The item inserted. */ insert : function(index, key, obj){ var me = this, myKey = key, myObj = obj; if (arguments.length == 2) { myObj = myKey; myKey = me.getKey(myObj); } if (me.containsKey(myKey)) { me.suspendEvents(); me.removeAtKey(myKey); me.resumeEvents(); } if (index >= me.length) { return me.add(myKey, myObj); } me.generation++; me.length++; Ext.Array.splice(me.items, index, 0, myObj); if (typeof myKey != 'undefined' && myKey !== null) { me.map[myKey] = myObj; } Ext.Array.splice(me.keys, index, 0, myKey); if (me.hasListeners.add) { me.fireEvent('add', index, myObj, myKey); } return myObj; }, /** * Remove an item from the collection. * @param {Object} o The item to remove. * @return {Object} The item removed or false if no item was removed. */ remove : function(o) { this.generation++; return this.removeAt(this.indexOf(o)); }, /** * Remove all items in the passed array from the collection. * @param {Array} items An array of items to be removed. * @return {Ext.util.MixedCollection} this object */ removeAll : function(items) { items = [].concat(items); var i, iLen = items.length; for (i = 0; i < iLen; i++) { this.remove(items[i]); } return this; }, /** * Remove an item from a specified index in the collection. Fires the {@link #event-remove} event when complete. * @param {Number} index The index within the collection of the item to remove. * @return {Object} The item removed or false if no item was removed. */ removeAt : function(index) { var me = this, o, key; if (index < me.length && index >= 0) { me.length--; o = me.items[index]; Ext.Array.erase(me.items, index, 1); key = me.keys[index]; if (typeof key != 'undefined') { delete me.map[key]; } Ext.Array.erase(me.keys, index, 1); if (me.hasListeners.remove) { me.fireEvent('remove', o, key); } me.generation++; return o; } return false; }, /** * Removed an item associated with the passed key fom the collection. * @param {String} key The key of the item to remove. * @return {Object} The item removed or false if no item was removed. */ removeAtKey : function(key){ return this.removeAt(this.indexOfKey(key)); }, /** * Returns the number of items in the collection. * @return {Number} the number of items in the collection. */ getCount : function(){ return this.length; }, /** * Returns index within the collection of the passed Object. * @param {Object} o The item to find the index of. * @return {Number} index of the item. Returns -1 if not found. */ indexOf : function(o){ return Ext.Array.indexOf(this.items, o); }, /** * Returns index within the collection of the passed key. * @param {String} key The key to find the index of. * @return {Number} index of the key. */ indexOfKey : function(key){ return Ext.Array.indexOf(this.keys, key); }, /** * Returns the item associated with the passed key OR index. * Key has priority over index. This is the equivalent * of calling {@link #getByKey} first, then if nothing matched calling {@link #getAt}. * @param {String/Number} key The key or index of the item. * @return {Object} If the item is found, returns the item. If the item was not found, returns undefined. * If an item was found, but is a Class, returns null. */ get : function(key) { var me = this, mk = me.map[key], item = mk !== undefined ? mk : (typeof key == 'number') ? me.items[key] : undefined; return typeof item != 'function' || me.allowFunctions ? item : null; // for prototype! }, /** * Returns the item at the specified index. * @param {Number} index The index of the item. * @return {Object} The item at the specified index. */ getAt : function(index) { return this.items[index]; }, /** * Returns the item associated with the passed key. * @param {String/Number} key The key of the item. * @return {Object} The item associated with the passed key. */ getByKey : function(key) { return this.map[key]; }, /** * Returns true if the collection contains the passed Object as an item. * @param {Object} o The Object to look for in the collection. * @return {Boolean} True if the collection contains the Object as an item. */ contains : function(o){ return typeof this.map[this.getKey(o)] != 'undefined'; }, /** * Returns true if the collection contains the passed Object as a key. * @param {String} key The key to look for in the collection. * @return {Boolean} True if the collection contains the Object as a key. */ containsKey : function(key){ return typeof this.map[key] != 'undefined'; }, /** * Removes all items from the collection. Fires the {@link #event-clear} event when complete. */ clear : function(){ var me = this; me.length = 0; me.items = []; me.keys = []; me.map = {}; me.generation++; if (me.hasListeners.clear) { me.fireEvent('clear'); } }, /** * Returns the first item in the collection. * @return {Object} the first item in the collection.. */ first : function() { return this.items[0]; }, /** * Returns the last item in the collection. * @return {Object} the last item in the collection.. */ last : function() { return this.items[this.length - 1]; }, /** * Collects all of the values of the given property and returns their sum * @param {String} property The property to sum by * @param {String} [root] 'root' property to extract the first argument from. This is used mainly when * summing fields in records, where the fields are all stored inside the 'data' object * @param {Number} [start=0] The record index to start at * @param {Number} [end=-1] The record index to end at * @return {Number} The total */ sum: function(property, root, start, end) { var values = this.extractValues(property, root), length = values.length, sum = 0, i; start = start || 0; end = (end || end === 0) ? end : length - 1; for (i = start; i <= end; i++) { sum += values[i]; } return sum; }, /** * Collects unique values of a particular property in this MixedCollection * @param {String} property The property to collect on * @param {String} root (optional) 'root' property to extract the first argument from. This is used mainly when * summing fields in records, where the fields are all stored inside the 'data' object * @param {Boolean} allowBlank (optional) Pass true to allow null, undefined or empty string values * @return {Array} The unique values */ collect: function(property, root, allowNull) { var values = this.extractValues(property, root), length = values.length, hits = {}, unique = [], value, strValue, i; for (i = 0; i < length; i++) { value = values[i]; strValue = String(value); if ((allowNull || !Ext.isEmpty(value)) && !hits[strValue]) { hits[strValue] = true; unique.push(value); } } return unique; }, /** * @private * Extracts all of the given property values from the items in the MC. Mainly used as a supporting method for * functions like sum and collect. * @param {String} property The property to extract * @param {String} root (optional) 'root' property to extract the first argument from. This is used mainly when * extracting field data from Model instances, where the fields are stored inside the 'data' object * @return {Array} The extracted values */ extractValues: function(property, root) { var values = this.items; if (root) { values = Ext.Array.pluck(values, root); } return Ext.Array.pluck(values, property); }, /** * Returns a range of items in this collection * @param {Number} startIndex (optional) The starting index. Defaults to 0. * @param {Number} endIndex (optional) The ending index. Defaults to the last item. * @return {Array} An array of items */ getRange : function(start, end){ var me = this, items = me.items, range = [], i; if (items.length < 1) { return range; } start = start || 0; end = Math.min(typeof end == 'undefined' ? me.length - 1 : end, me.length - 1); if (start <= end) { for (i = start; i <= end; i++) { range[range.length] = items[i]; } } else { for (i = start; i >= end; i--) { range[range.length] = items[i]; } } return range; }, /** *

Filters the objects in this collection by a set of {@link Ext.util.Filter Filter}s, or by a single * property/value pair with optional parameters for substring matching and case sensitivity. See * {@link Ext.util.Filter Filter} for an example of using Filter objects (preferred). Alternatively, * MixedCollection can be easily filtered by property like this:


//create a simple store with a few people defined
var people = new Ext.util.MixedCollection();
people.addAll([
    {id: 1, age: 25, name: 'Ed'},
    {id: 2, age: 24, name: 'Tommy'},
    {id: 3, age: 24, name: 'Arne'},
    {id: 4, age: 26, name: 'Aaron'}
]);

//a new MixedCollection containing only the items where age == 24
var middleAged = people.filter('age', 24);
* * * @param {Ext.util.Filter[]/String} property A property on your objects, or an array of {@link Ext.util.Filter Filter} objects * @param {String/RegExp} value Either string that the property values * should start with or a RegExp to test against the property * @param {Boolean} [anyMatch=false] True to match any part of the string, not just the beginning * @param {Boolean} [caseSensitive=false] True for case sensitive comparison. * @return {Ext.util.MixedCollection} The new filtered collection */ filter : function(property, value, anyMatch, caseSensitive) { var filters = [], filterFn; //support for the simple case of filtering by property/value if (Ext.isString(property)) { filters.push(new Ext.util.Filter({ property : property, value : value, anyMatch : anyMatch, caseSensitive: caseSensitive })); } else if (Ext.isArray(property) || property instanceof Ext.util.Filter) { filters = filters.concat(property); } //at this point we have an array of zero or more Ext.util.Filter objects to filter with, //so here we construct a function that combines these filters by ANDing them together filterFn = function(record) { var isMatch = true, length = filters.length, i, filter, fn, scope; for (i = 0; i < length; i++) { filter = filters[i]; fn = filter.filterFn; scope = filter.scope; isMatch = isMatch && fn.call(scope, record); } return isMatch; }; return this.filterBy(filterFn); }, /** * Filter by a function. Returns a new collection that has been filtered. * The passed function will be called with each object in the collection. * If the function returns true, the value is included otherwise it is filtered. * @param {Function} fn The function to be called. * @param {Mixed} fn.item The collection item. * @param {String} fn.key The key of collection item. * @param {Object} scope (optional) The scope (this reference) in * which the function is executed. Defaults to this MixedCollection. * @return {Ext.util.MixedCollection} The new filtered collection */ filterBy : function(fn, scope) { var me = this, newMC = new this.self(), keys = me.keys, items = me.items, length = items.length, i; newMC.getKey = me.getKey; for (i = 0; i < length; i++) { if (fn.call(scope || me, items[i], keys[i])) { newMC.add(keys[i], items[i]); } } return newMC; }, /** * Finds the index of the first matching object in this collection by a specific property/value. * @param {String} property The name of a property on your objects. * @param {String/RegExp} value A string that the property values * should start with or a RegExp to test against the property. * @param {Number} [start=0] The index to start searching at. * @param {Boolean} [anyMatch=false] True to match any part of the string, not just the beginning. * @param {Boolean} [caseSensitive=false] True for case sensitive comparison. * @return {Number} The matched index or -1 */ findIndex : function(property, value, start, anyMatch, caseSensitive){ if(Ext.isEmpty(value, false)){ return -1; } value = this.createValueMatcher(value, anyMatch, caseSensitive); return this.findIndexBy(function(o){ return o && value.test(o[property]); }, null, start); }, /** * Find the index of the first matching object in this collection by a function. * If the function returns true it is considered a match. * @param {Function} fn The function to be called. * @param {Mixed} fn.item The collection item. * @param {String} fn.key The key of collection item. * @param {Object} [scope] The scope (this reference) in which the function is executed. Defaults to this MixedCollection. * @param {Number} [start=0] The index to start searching at. * @return {Number} The matched index or -1 */ findIndexBy : function(fn, scope, start){ var me = this, keys = me.keys, items = me.items, i = start || 0, len = items.length; for (; i < len; i++) { if (fn.call(scope || me, items[i], keys[i])) { return i; } } return -1; }, /** * Returns a regular expression based on the given value and matching options. This is used internally for finding and filtering, * and by Ext.data.Store#filter * @private * @param {String} value The value to create the regex for. This is escaped using Ext.escapeRe * @param {Boolean} anyMatch True to allow any match - no regex start/end line anchors will be added. Defaults to false * @param {Boolean} caseSensitive True to make the regex case sensitive (adds 'i' switch to regex). Defaults to false. * @param {Boolean} exactMatch True to force exact match (^ and $ characters added to the regex). Defaults to false. Ignored if anyMatch is true. */ createValueMatcher : function(value, anyMatch, caseSensitive, exactMatch) { if (!value.exec) { // not a regex var er = Ext.String.escapeRegex; value = String(value); if (anyMatch === true) { value = er(value); } else { value = '^' + er(value); if (exactMatch === true) { value += '$'; } } value = new RegExp(value, caseSensitive ? '' : 'i'); } return value; }, /** * Creates a shallow copy of this collection * @return {Ext.util.MixedCollection} */ clone : function() { var me = this, copy = new this.self(), keys = me.keys, items = me.items, i = 0, len = items.length; for(; i < len; i++){ copy.add(keys[i], items[i]); } copy.getKey = me.getKey; return copy; } }); /** * @private */ Ext.define('Ext.fx.CubicBezier', { /* Begin Definitions */ singleton: true, /* End Definitions */ cubicBezierAtTime: function(t, p1x, p1y, p2x, p2y, duration) { var cx = 3 * p1x, bx = 3 * (p2x - p1x) - cx, ax = 1 - cx - bx, cy = 3 * p1y, by = 3 * (p2y - p1y) - cy, ay = 1 - cy - by; function sampleCurveX(t) { return ((ax * t + bx) * t + cx) * t; } function solve(x, epsilon) { var t = solveCurveX(x, epsilon); return ((ay * t + by) * t + cy) * t; } function solveCurveX(x, epsilon) { var t0, t1, t2, x2, d2, i; for (t2 = x, i = 0; i < 8; i++) { x2 = sampleCurveX(t2) - x; if (Math.abs(x2) < epsilon) { return t2; } d2 = (3 * ax * t2 + 2 * bx) * t2 + cx; if (Math.abs(d2) < 1e-6) { break; } t2 = t2 - x2 / d2; } t0 = 0; t1 = 1; t2 = x; if (t2 < t0) { return t0; } if (t2 > t1) { return t1; } while (t0 < t1) { x2 = sampleCurveX(t2); if (Math.abs(x2 - x) < epsilon) { return t2; } if (x > x2) { t0 = t2; } else { t1 = t2; } t2 = (t1 - t0) / 2 + t0; } return t2; } return solve(t, 1 / (200 * duration)); }, cubicBezier: function(x1, y1, x2, y2) { var fn = function(pos) { return Ext.fx.CubicBezier.cubicBezierAtTime(pos, x1, y1, x2, y2, 1); }; fn.toCSS3 = function() { return 'cubic-bezier(' + [x1, y1, x2, y2].join(',') + ')'; }; fn.reverse = function() { return Ext.fx.CubicBezier.cubicBezier(1 - x2, 1 - y2, 1 - x1, 1 - y1); }; return fn; } }); /** * A custom drag proxy implementation specific to {@link Ext.panel.Panel}s. This class * is primarily used internally for the Panel's drag drop implementation, and * should never need to be created directly. * @private */ Ext.define('Ext.panel.Proxy', { alternateClassName: 'Ext.dd.PanelProxy', /** * @cfg {Boolean} [moveOnDrag=true] * True to move the panel to the dragged position when dropped */ moveOnDrag: true, /** * Creates new panel proxy. * @param {Ext.panel.Panel} panel The {@link Ext.panel.Panel} to proxy for * @param {Object} [config] Config object */ constructor: function(panel, config){ var me = this; /** * @property panel * @type Ext.panel.Panel */ me.panel = panel; me.id = me.panel.id +'-ddproxy'; Ext.apply(me, config); }, /** * @cfg {Boolean} insertProxy * True to insert a placeholder proxy element while dragging the panel, false to drag with no proxy. * Most Panels are not absolute positioned and therefore we need to reserve this space. */ insertProxy: true, // private overrides setStatus: Ext.emptyFn, reset: Ext.emptyFn, update: Ext.emptyFn, stop: Ext.emptyFn, sync: Ext.emptyFn, /** * Gets the proxy's element * @return {Ext.Element} The proxy's element */ getEl: function(){ return this.ghost.el; }, /** * Gets the proxy's ghost Panel * @return {Ext.panel.Panel} The proxy's ghost Panel */ getGhost: function(){ return this.ghost; }, /** * Gets the proxy element. This is the element that represents where the * Panel was before we started the drag operation. * @return {Ext.Element} The proxy's element */ getProxy: function(){ return this.proxy; }, /** * Hides the proxy */ hide : function(){ var me = this; if (me.ghost) { if (me.proxy) { me.proxy.remove(); delete me.proxy; } // Unghost the Panel, do not move the Panel to where the ghost was me.panel.unghost(null, me.moveOnDrag); delete me.ghost; } }, /** * Shows the proxy */ show: function(){ var me = this, panelSize; if (!me.ghost) { panelSize = me.panel.getSize(); me.panel.el.setVisibilityMode(Ext.Element.DISPLAY); me.ghost = me.panel.ghost(); if (me.insertProxy) { // bc Panels aren't absolute positioned we need to take up the space // of where the panel previously was me.proxy = me.panel.el.insertSibling({cls: Ext.baseCSSPrefix + 'panel-dd-spacer'}); me.proxy.setSize(panelSize); } } }, // private repair: function(xy, callback, scope) { this.hide(); Ext.callback(callback, scope || this); }, /** * Moves the proxy to a different position in the DOM. This is typically * called while dragging the Panel to keep the proxy sync'd to the Panel's * location. * @param {HTMLElement} parentNode The proxy's parent DOM node * @param {HTMLElement} [before] The sibling node before which the * proxy should be inserted. Defaults to the parent's last child if not * specified. */ moveProxy : function(parentNode, before){ if (this.proxy) { parentNode.insertBefore(this.proxy.dom, before); } } }); /** * Base Manager class */ 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 all = this.all, key = all.getKey(item); if (all.containsKey(key)) { Ext.Error.raise('Registering duplicate id "' + key + '" with this manager'); } 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.Error.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(); } }); /** * @class Ext.fx.Queue * Animation Queue mixin to handle chaining and queueing by target. * @private */ Ext.define('Ext.fx.Queue', { requires: ['Ext.util.HashMap'], constructor: function() { this.targets = new Ext.util.HashMap(); this.fxQueue = {}; }, // @private getFxDefaults: function(targetId) { var target = this.targets.get(targetId); if (target) { return target.fxDefaults; } return {}; }, // @private setFxDefaults: function(targetId, obj) { var target = this.targets.get(targetId); if (target) { target.fxDefaults = Ext.apply(target.fxDefaults || {}, obj); } }, // @private stopAnimation: function(targetId) { var me = this, queue = me.getFxQueue(targetId), ln = queue.length; while (ln) { queue[ln - 1].end(); ln--; } }, /** * @private * Returns current animation object if the element has any effects actively running or queued, else returns false. */ getActiveAnimation: function(targetId) { var queue = this.getFxQueue(targetId); return (queue && !!queue.length) ? queue[0] : false; }, // @private hasFxBlock: function(targetId) { var queue = this.getFxQueue(targetId); return queue && queue[0] && queue[0].block; }, // @private get fx queue for passed target, create if needed. getFxQueue: function(targetId) { if (!targetId) { return false; } var me = this, queue = me.fxQueue[targetId], target = me.targets.get(targetId); if (!target) { return false; } if (!queue) { me.fxQueue[targetId] = []; // GarbageCollector will need to clean up Elements since they aren't currently observable if (target.type != 'element') { target.target.on('destroy', function() { me.fxQueue[targetId] = []; }); } } return me.fxQueue[targetId]; }, // @private queueFx: function(anim) { var me = this, target = anim.target, queue, ln; if (!target) { return; } queue = me.getFxQueue(target.getId()); ln = queue.length; if (ln) { if (anim.concurrent) { anim.paused = false; } else { queue[ln - 1].on('afteranimate', function() { anim.paused = false; }); } } else { anim.paused = false; } anim.on('afteranimate', function() { Ext.Array.remove(queue, anim); if (anim.remove) { if (target.type == 'element') { var el = Ext.get(target.id); if (el) { el.remove(); } } } }, this); queue.push(anim); } }); /** * A mixin to add floating capability to a Component. */ Ext.define('Ext.util.Floating', { uses: ['Ext.Layer', 'Ext.window.Window'], /** * @cfg {Boolean} focusOnToFront * Specifies whether the floated component should be automatically {@link Ext.Component#method-focus focused} when * it is {@link #toFront brought to the front}. */ focusOnToFront: true, /** * @cfg {String/Boolean} shadow * Specifies whether the floating component should be given a shadow. Set to true to automatically create an * {@link Ext.Shadow}, or a string indicating the shadow's display {@link Ext.Shadow#mode}. Set to false to * disable the shadow. */ shadow: 'sides', /** * @cfg {String/Boolean} shadowOffset * Number of pixels to offset the shadow. */ constructor: function (dom) { var me = this; me.el = new Ext.Layer(Ext.apply({ hideMode : me.hideMode, hidden : me.hidden, shadow : (typeof me.shadow != 'undefined') ? me.shadow : 'sides', shadowOffset : me.shadowOffset, constrain : false, shim : (me.shim === false) ? false : undefined }, me.floating), dom); // release config object (if it was one) me.floating = true; // Register with the configured ownerCt. // With this we acquire a floatParent for relative positioning, and a zIndexParent which is an // ancestor floater which provides zIndex management. me.registerWithOwnerCt(); }, registerWithOwnerCt: function() { var me = this; if (me.zIndexParent) { me.zIndexParent.unregisterFloatingItem(me); } // Acquire a zIndexParent by traversing the ownerCt axis for the nearest floating ancestor me.zIndexParent = me.up('[floating]'); me.setFloatParent(me.ownerCt); delete me.ownerCt; if (me.zIndexParent) { me.zIndexParent.registerFloatingItem(me); } else { Ext.WindowManager.register(me); } }, setFloatParent: function(floatParent) { var me = this; // Remove listeners from previous floatParent if (me.floatParent) { me.mun(me.floatParent, { hide: me.onFloatParentHide, show: me.onFloatParentShow, scope: me }); } me.floatParent = floatParent; // Floating Components as children of Containers must hide when their parent hides. if (floatParent) { me.mon(me.floatParent, { hide: me.onFloatParentHide, show: me.onFloatParentShow, scope: me }); } // If a floating Component is configured to be constrained, but has no configured // constrainTo setting, set its constrainTo to be it's ownerCt before rendering. if ((me.constrain || me.constrainHeader) && !me.constrainTo) { me.constrainTo = floatParent ? floatParent.getTargetEl() : me.container; } }, onAfterFloatLayout: function(){ this.syncShadow(); }, onFloatParentHide: function() { var me = this; if (me.hideOnParentHide !== false && me.isVisible()) { me.hide(); me.showOnParentShow = true; } }, onFloatParentShow: function() { if (this.showOnParentShow) { delete this.showOnParentShow; this.show(); } }, // private // z-index is managed by the zIndexManager and may be overwritten at any time. // Returns the next z-index to be used. // If this is a Container, then it will have rebased any managed floating Components, // and so the next available z-index will be approximately 10000 above that. setZIndex: function(index) { var me = this; me.el.setZIndex(index); // Next item goes 10 above; index += 10; // When a Container with floating descendants has its z-index set, it rebases any floating descendants it is managing. // The returned value is a round number approximately 10000 above the last z-index used. if (me.floatingDescendants) { index = Math.floor(me.floatingDescendants.setBase(index) / 100) * 100 + 10000; } return index; }, /** * Moves this floating Component into a constrain region. * * By default, this Component is constrained to be within the container it was added to, or the element it was * rendered to. * * An alternative constraint may be passed. * @param {String/HTMLElement/Ext.Element/Ext.util.Region} [constrainTo] The Element or {@link Ext.util.Region Region} * into which this Component is to be constrained. Defaults to the element into which this floating Component * was rendered. */ doConstrain: function(constrainTo) { var me = this, // Calculate the constrain vector to coerce our position to within our // constrainTo setting. getConstrainVector will provide a default constraint // region if there is no explicit constrainTo, *and* there is no floatParent owner Component. vector = me.getConstrainVector(constrainTo), xy; if (vector) { xy = me.getPosition(!!me.floatParent); xy[0] += vector[0]; xy[1] += vector[1]; me.setPosition(xy); } }, /** * Gets the x/y offsets to constrain this float * @private * @param {String/HTMLElement/Ext.Element/Ext.util.Region} [constrainTo] The Element or {@link Ext.util.Region Region} * into which this Component is to be constrained. * @return {Number[]} The x/y constraints */ getConstrainVector: function(constrainTo){ var me = this; if (me.constrain || me.constrainHeader) { constrainTo = constrainTo || (me.floatParent && me.floatParent.getTargetEl()) || me.container || me.el.getScopeParent(); return (me.constrainHeader ? me.header.el : me.el).getConstrainVector(constrainTo); } }, /** * Aligns this floating Component to the specified element * * @param {Ext.Component/Ext.Element/HTMLElement/String} element * The element or {@link Ext.Component} to align to. If passing a component, it must be a * component instance. If a string id is passed, it will be used as an element id. * @param {String} [position="tl-bl?"] The position to align to * (see {@link Ext.Element#alignTo} for more details). * @param {Number[]} [offsets] Offset the positioning by [x, y] * @return {Ext.Component} this */ alignTo: function(element, position, offsets) { // element may be a Component, so first attempt to use its el to align to. // When aligning to an Element's X,Y position, we must use setPagePosition which disregards any floatParent this.setPagePosition(this.el.getAlignToXY(element.el || element, position, offsets)); return this; }, /** * Brings this floating Component to the front of any other visible, floating Components managed by the same * {@link Ext.ZIndexManager ZIndexManager} * * If this Component is modal, inserts the modal mask just below this Component in the z-index stack. * * @param {Boolean} [preventFocus=false] Specify `true` to prevent the Component from being focused. * @return {Ext.Component} this */ toFront: function(preventFocus) { var me = this; // Find the floating Component which provides the base for this Component's zIndexing. // That must move to front to then be able to rebase its zIndex stack and move this to the front if (me.zIndexParent && me.bringParentToFront !== false) { me.zIndexParent.toFront(true); } if (!Ext.isDefined(preventFocus)) { preventFocus = !me.focusOnToFront; } if (preventFocus) { me.preventFocusOnActivate = true; } if (me.zIndexManager.bringToFront(me)) { if (!preventFocus) { // Kick off a delayed focus request. // If another floating Component is toFronted before the delay expires // this will not receive focus. me.focus(false, true); } } delete me.preventFocusOnActivate; return me; }, /** * This method is called internally by {@link Ext.ZIndexManager} to signal that a floating Component has either been * moved to the top of its zIndex stack, or pushed from the top of its zIndex stack. * * If a _Window_ is superceded by another Window, deactivating it hides its shadow. * * This method also fires the {@link Ext.Component#activate activate} or * {@link Ext.Component#deactivate deactivate} event depending on which action occurred. * * @param {Boolean} [active=false] True to activate the Component, false to deactivate it. * @param {Ext.Component} [newActive] The newly active Component which is taking over topmost zIndex position. */ setActive: function(active, newActive) { var me = this; if (active) { if (me.el.shadow && !me.maximized) { me.el.enableShadow(true); } if (me.modal && !me.preventFocusOnActivate) { me.focus(false, true); } me.fireEvent('activate', me); } else { // Only the *Windows* in a zIndex stack share a shadow. All other types of floaters // can keep their shadows all the time if (me.isWindow && (newActive && newActive.isWindow)) { me.el.disableShadow(); } me.fireEvent('deactivate', me); } }, /** * Sends this Component to the back of (lower z-index than) any other visible windows * @return {Ext.Component} this */ toBack: function() { this.zIndexManager.sendToBack(this); return this; }, /** * Center this Component in its container. * @return {Ext.Component} this */ center: function() { var me = this, xy; if (me.isVisible()) { xy = me.el.getAlignToXY(me.container, 'c-c'); me.setPagePosition(xy); } else { me.needsCenter = true; } return me; }, onFloatShow: function() { if (this.needsCenter) { this.center(); } delete this.needsCenter; }, // private syncShadow : function() { if (this.floating) { this.el.sync(true); } }, // private fitContainer: function() { var me = this, parent = me.floatParent, container = parent ? parent.getTargetEl() : me.container; me.setSize(container.getViewSize(false)); me.setPosition.apply(me, parent ? [0, 0] : container.getXY()); } }); /** * This class parses the XTemplate syntax and calls abstract methods to process the parts. * @private */ Ext.define('Ext.XTemplateParser', { constructor: function (config) { Ext.apply(this, config); }, /** * @property {Number} level The 'for' loop context level. This is adjusted up by one * prior to calling {@link #doFor} and down by one after calling the corresponding * {@link #doEnd} that closes the loop. This will be 1 on the first {@link #doFor} * call. */ /** * This method is called to process a piece of raw text from the tpl. * @param {String} text * @method doText */ // doText: function (text) /** * This method is called to process expressions (like `{[expr]}`). * @param {String} expr The body of the expression (inside "{[" and "]}"). * @method doExpr */ // doExpr: function (expr) /** * This method is called to process simple tags (like `{tag}`). * @method doTag */ // doTag: function (tag) /** * This method is called to process ``. * @method doElse */ // doElse: function () /** * This method is called to process `{% text %}`. * @param {String} text * @method doEval */ // doEval: function (text) /** * This method is called to process ``. If there are other attributes, * these are passed in the actions object. * @param {String} action * @param {Object} actions Other actions keyed by the attribute name (such as 'exec'). * @method doIf */ // doIf: function (action, actions) /** * This method is called to process ``. If there are other attributes, * these are passed in the actions object. * @param {String} action * @param {Object} actions Other actions keyed by the attribute name (such as 'exec'). * @method doElseIf */ // doElseIf: function (action, actions) /** * This method is called to process ``. If there are other attributes, * these are passed in the actions object. * @param {String} action * @param {Object} actions Other actions keyed by the attribute name (such as 'exec'). * @method doSwitch */ // doSwitch: function (action, actions) /** * This method is called to process ``. If there are other attributes, * these are passed in the actions object. * @param {String} action * @param {Object} actions Other actions keyed by the attribute name (such as 'exec'). * @method doCase */ // doCase: function (action, actions) /** * This method is called to process ``. * @method doDefault */ // doDefault: function () /** * This method is called to process ``. It is given the action type that started * the tpl and the set of additional actions. * @param {String} type The type of action that is being ended. * @param {Object} actions The other actions keyed by the attribute name (such as 'exec'). * @method doEnd */ // doEnd: function (type, actions) /** * This method is called to process ``. If there are other attributes, * these are passed in the actions object. * @param {String} action * @param {Object} actions Other actions keyed by the attribute name (such as 'exec'). * @method doFor */ // doFor: function (action, actions) /** * This method is called to process ``. If there are other attributes, * these are passed in the actions object. * @param {String} action * @param {Object} actions Other actions keyed by the attribute name. * @method doExec */ // doExec: function (action, actions) /** * This method is called to process an empty ``. This is unlikely to need to be * implemented, so a default (do nothing) version is provided. * @method */ doTpl: Ext.emptyFn, parse: function (str) { var me = this, len = str.length, aliases = { elseif: 'elif' }, topRe = me.topRe, actionsRe = me.actionsRe, index, stack, s, m, t, prev, frame, subMatch, begin, end, actions, prop; me.level = 0; me.stack = stack = []; for (index = 0; index < len; index = end) { topRe.lastIndex = index; m = topRe.exec(str); if (!m) { me.doText(str.substring(index, len)); break; } begin = m.index; end = topRe.lastIndex; if (index < begin) { me.doText(str.substring(index, begin)); } if (m[1]) { end = str.indexOf('%}', begin+2); me.doEval(str.substring(begin+2, end)); end += 2; } else if (m[2]) { end = str.indexOf(']}', begin+2); me.doExpr(str.substring(begin+2, end)); end += 2; } else if (m[3]) { // if ('{' token) me.doTag(m[3]); } else if (m[4]) { // content of a tag actions = null; while ((subMatch = actionsRe.exec(m[4])) !== null) { s = subMatch[2] || subMatch[3]; if (s) { s = Ext.String.htmlDecode(s); // decode attr value t = subMatch[1]; t = aliases[t] || t; actions = actions || {}; prev = actions[t]; if (typeof prev == 'string') { actions[t] = [prev, s]; } else if (prev) { actions[t].push(s); } else { actions[t] = s; } } } if (!actions) { if (me.elseRe.test(m[4])) { me.doElse(); } else if (me.defaultRe.test(m[4])) { me.doDefault(); } else { me.doTpl(); stack.push({ type: 'tpl' }); } } else if (actions['if']) { me.doIf(actions['if'], actions); stack.push({ type: 'if' }); } else if (actions['switch']) { me.doSwitch(actions['switch'], actions); stack.push({ type: 'switch' }); } else if (actions['case']) { me.doCase(actions['case'], actions); } else if (actions['elif']) { me.doElseIf(actions['elif'], actions); } else if (actions['for']) { ++me.level; // Extract property name to use from indexed item if (prop = me.propRe.exec(m[4])) { actions.propName = prop[1] || prop[2]; } me.doFor(actions['for'], actions); stack.push({ type: 'for', actions: actions }); } else if (actions.exec) { me.doExec(actions.exec, actions); stack.push({ type: 'exec', actions: actions }); } /* else { // todo - error } */ } else if (m[0].length === 5) { // if the length of m[0] is 5, assume that we're dealing with an opening tpl tag with no attributes (e.g. ...) // in this case no action is needed other than pushing it on to the stack stack.push({ type: 'tpl' }); } else { frame = stack.pop(); me.doEnd(frame.type, frame.actions); if (frame.type == 'for') { --me.level; } } } }, // Internal regexes topRe: /(?:(\{\%)|(\{\[)|\{([^{}]*)\})|(?:]*)\>)|(?:<\/tpl>)/g, actionsRe: /\s*(elif|elseif|if|for|exec|switch|case|eval)\s*\=\s*(?:(?:"([^"]*)")|(?:'([^']*)'))\s*/g, propRe: /prop=(?:(?:"([^"]*)")|(?:'([^']*)'))/, defaultRe: /^\s*default\s*$/, elseRe: /^\s*else\s*$/ }); /** * Represents an RGB color and provides helper functions get * color components in HSL color space. */ Ext.define('Ext.draw.Color', { /* Begin Definitions */ /* End Definitions */ colorToHexRe: /(.*?)rgb\((\d+),\s*(\d+),\s*(\d+)\)/, rgbRe: /\s*rgb\s*\(\s*([0-9]+)\s*,\s*([0-9]+)\s*,\s*([0-9]+)\s*\)\s*/, hexRe: /\s*#([0-9a-fA-F][0-9a-fA-F]?)([0-9a-fA-F][0-9a-fA-F]?)([0-9a-fA-F][0-9a-fA-F]?)\s*/, /** * @cfg {Number} lightnessFactor * * The default factor to compute the lighter or darker color. Defaults to 0.2. */ lightnessFactor: 0.2, /** * Creates new Color. * @param {Number} red Red component (0..255) * @param {Number} green Green component (0..255) * @param {Number} blue Blue component (0..255) */ constructor : function(red, green, blue) { var me = this, clamp = Ext.Number.constrain; me.r = clamp(red, 0, 255); me.g = clamp(green, 0, 255); me.b = clamp(blue, 0, 255); }, /** * Get the red component of the color, in the range 0..255. * @return {Number} */ getRed: function() { return this.r; }, /** * Get the green component of the color, in the range 0..255. * @return {Number} */ getGreen: function() { return this.g; }, /** * Get the blue component of the color, in the range 0..255. * @return {Number} */ getBlue: function() { return this.b; }, /** * Get the RGB values. * @return {Number[]} */ getRGB: function() { var me = this; return [me.r, me.g, me.b]; }, /** * Get the equivalent HSL components of the color. * @return {Number[]} */ getHSL: function() { var me = this, r = me.r / 255, g = me.g / 255, b = me.b / 255, max = Math.max(r, g, b), min = Math.min(r, g, b), delta = max - min, h, s = 0, l = 0.5 * (max + min); // min==max means achromatic (hue is undefined) if (min != max) { s = (l < 0.5) ? delta / (max + min) : delta / (2 - max - min); if (r == max) { h = 60 * (g - b) / delta; } else if (g == max) { h = 120 + 60 * (b - r) / delta; } else { h = 240 + 60 * (r - g) / delta; } if (h < 0) { h += 360; } if (h >= 360) { h -= 360; } } return [h, s, l]; }, /** * Return a new color that is lighter than this color. * @param {Number} factor Lighter factor (0..1), default to 0.2 * @return Ext.draw.Color */ getLighter: function(factor) { var hsl = this.getHSL(); factor = factor || this.lightnessFactor; hsl[2] = Ext.Number.constrain(hsl[2] + factor, 0, 1); return this.fromHSL(hsl[0], hsl[1], hsl[2]); }, /** * Return a new color that is darker than this color. * @param {Number} factor Darker factor (0..1), default to 0.2 * @return Ext.draw.Color */ getDarker: function(factor) { factor = factor || this.lightnessFactor; return this.getLighter(-factor); }, /** * Return the color in the hex format, i.e. '#rrggbb'. * @return {String} */ toString: function() { var me = this, round = Math.round, r = round(me.r).toString(16), g = round(me.g).toString(16), b = round(me.b).toString(16); r = (r.length == 1) ? '0' + r : r; g = (g.length == 1) ? '0' + g : g; b = (b.length == 1) ? '0' + b : b; return ['#', r, g, b].join(''); }, /** * Convert a color to hexadecimal format. * * **Note:** This method is both static and instance. * * @param {String/String[]} color The color value (i.e 'rgb(255, 255, 255)', 'color: #ffffff'). * Can also be an Array, in this case the function handles the first member. * @returns {String} The color in hexadecimal format. * @static */ toHex: function(color) { if (Ext.isArray(color)) { color = color[0]; } if (!Ext.isString(color)) { return ''; } if (color.substr(0, 1) === '#') { return color; } var digits = this.colorToHexRe.exec(color), red, green, blue, rgb; if (Ext.isArray(digits)) { red = parseInt(digits[2], 10); green = parseInt(digits[3], 10); blue = parseInt(digits[4], 10); rgb = blue | (green << 8) | (red << 16); return digits[1] + '#' + ("000000" + rgb.toString(16)).slice(-6); } else { return color; } }, /** * Parse the string and create a new color. * * Supported formats: '#rrggbb', '#rgb', and 'rgb(r,g,b)'. * * If the string is not recognized, an undefined will be returned instead. * * **Note:** This method is both static and instance. * * @param {String} str Color in string. * @returns Ext.draw.Color * @static */ fromString: function(str) { var values, r, g, b, parse = parseInt; if ((str.length == 4 || str.length == 7) && str.substr(0, 1) === '#') { values = str.match(this.hexRe); if (values) { r = parse(values[1], 16) >> 0; g = parse(values[2], 16) >> 0; b = parse(values[3], 16) >> 0; if (str.length == 4) { r += (r * 16); g += (g * 16); b += (b * 16); } } } else { values = str.match(this.rgbRe); if (values) { r = values[1]; g = values[2]; b = values[3]; } } return (typeof r == 'undefined') ? undefined : new Ext.draw.Color(r, g, b); }, /** * Returns the gray value (0 to 255) of the color. * * The gray value is calculated using the formula r*0.3 + g*0.59 + b*0.11. * * @returns {Number} */ getGrayscale: function() { // http://en.wikipedia.org/wiki/Grayscale#Converting_color_to_grayscale return this.r * 0.3 + this.g * 0.59 + this.b * 0.11; }, /** * Create a new color based on the specified HSL values. * * **Note:** This method is both static and instance. * * @param {Number} h Hue component (0..359) * @param {Number} s Saturation component (0..1) * @param {Number} l Lightness component (0..1) * @returns Ext.draw.Color * @static */ fromHSL: function(h, s, l) { var C, X, m, i, rgb = [], abs = Math.abs, floor = Math.floor; if (s == 0 || h == null) { // achromatic rgb = [l, l, l]; } else { // http://en.wikipedia.org/wiki/HSL_and_HSV#From_HSL // C is the chroma // X is the second largest component // m is the lightness adjustment h /= 60; C = s * (1 - abs(2 * l - 1)); X = C * (1 - abs(h - 2 * floor(h / 2) - 1)); m = l - C / 2; switch (floor(h)) { case 0: rgb = [C, X, 0]; break; case 1: rgb = [X, C, 0]; break; case 2: rgb = [0, C, X]; break; case 3: rgb = [0, X, C]; break; case 4: rgb = [X, 0, C]; break; case 5: rgb = [C, 0, X]; break; } rgb = [rgb[0] + m, rgb[1] + m, rgb[2] + m]; } return new Ext.draw.Color(rgb[0] * 255, rgb[1] * 255, rgb[2] * 255); } }, function() { var prototype = this.prototype; //These functions are both static and instance. TODO: find a more elegant way of copying them this.addStatics({ fromHSL: function() { return prototype.fromHSL.apply(prototype, arguments); }, fromString: function() { return prototype.fromString.apply(prototype, arguments); }, toHex: function() { return prototype.toHex.apply(prototype, arguments); } }); }); /** * @class Ext.fx.target.Target This class specifies a generic target for an animation. It provides a wrapper around a series of different types of objects to allow for a generic animation API. A target can be a single object or a Composite object containing other objects that are to be animated. This class and it's subclasses are generally not created directly, the underlying animation will create the appropriate Ext.fx.target.Target object by passing the instance to be animated. The following types of objects can be animated: - {@link Ext.fx.target.Component Components} - {@link Ext.fx.target.Element Elements} - {@link Ext.fx.target.Sprite Sprites} * @markdown * @abstract */ Ext.define('Ext.fx.target.Target', { isAnimTarget: true, /** * Creates new Target. * @param {Ext.Component/Ext.Element/Ext.draw.Sprite} target The object to be animated */ constructor: function(target) { this.target = target; this.id = this.getId(); }, getId: function() { return this.target.id; } }); /** * This animation class is a mixin. * * Ext.util.Animate provides an API for the creation of animated transitions of properties and styles. * This class is used as a mixin and currently applied to {@link Ext.Element}, {@link Ext.CompositeElement}, * {@link Ext.draw.Sprite}, {@link Ext.draw.CompositeSprite}, and {@link Ext.Component}. Note that Components * have a limited subset of what attributes can be animated such as top, left, x, y, height, width, and * opacity (color, paddings, and margins can not be animated). * * ## Animation Basics * * All animations require three things - `easing`, `duration`, and `to` (the final end value for each property) * you wish to animate. Easing and duration are defaulted values specified below. * Easing describes how the intermediate values used during a transition will be calculated. * {@link Ext.fx.Anim#easing Easing} allows for a transition to change speed over its duration. * You may use the defaults for easing and duration, but you must always set a * {@link Ext.fx.Anim#to to} property which is the end value for all animations. * * Popular element 'to' configurations are: * * - opacity * - x * - y * - color * - height * - width * * Popular sprite 'to' configurations are: * * - translation * - path * - scale * - stroke * - rotation * * The default duration for animations is 250 (which is a 1/4 of a second). Duration is denoted in * milliseconds. Therefore 1 second is 1000, 1 minute would be 60000, and so on. The default easing curve * used for all animations is 'ease'. Popular easing functions are included and can be found in {@link Ext.fx.Anim#easing Easing}. * * For example, a simple animation to fade out an element with a default easing and duration: * * var p1 = Ext.get('myElementId'); * * p1.animate({ * to: { * opacity: 0 * } * }); * * To make this animation fade out in a tenth of a second: * * var p1 = Ext.get('myElementId'); * * p1.animate({ * duration: 100, * to: { * opacity: 0 * } * }); * * ## Animation Queues * * By default all animations are added to a queue which allows for animation via a chain-style API. * For example, the following code will queue 4 animations which occur sequentially (one right after the other): * * p1.animate({ * to: { * x: 500 * } * }).animate({ * to: { * y: 150 * } * }).animate({ * to: { * backgroundColor: '#f00' //red * } * }).animate({ * to: { * opacity: 0 * } * }); * * You can change this behavior by calling the {@link Ext.util.Animate#syncFx syncFx} method and all * subsequent animations for the specified target will be run concurrently (at the same time). * * p1.syncFx(); //this will make all animations run at the same time * * p1.animate({ * to: { * x: 500 * } * }).animate({ * to: { * y: 150 * } * }).animate({ * to: { * backgroundColor: '#f00' //red * } * }).animate({ * to: { * opacity: 0 * } * }); * * This works the same as: * * p1.animate({ * to: { * x: 500, * y: 150, * backgroundColor: '#f00' //red * opacity: 0 * } * }); * * The {@link Ext.util.Animate#stopAnimation stopAnimation} method can be used to stop any * currently running animations and clear any queued animations. * * ## Animation Keyframes * * You can also set up complex animations with {@link Ext.fx.Anim#keyframes keyframes} which follow the * CSS3 Animation configuration pattern. Note rotation, translation, and scaling can only be done for sprites. * The previous example can be written with the following syntax: * * p1.animate({ * duration: 1000, //one second total * keyframes: { * 25: { //from 0 to 250ms (25%) * x: 0 * }, * 50: { //from 250ms to 500ms (50%) * y: 0 * }, * 75: { //from 500ms to 750ms (75%) * backgroundColor: '#f00' //red * }, * 100: { //from 750ms to 1sec * opacity: 0 * } * } * }); * * ## Animation Events * * Each animation you create has events for {@link Ext.fx.Anim#beforeanimate beforeanimate}, * {@link Ext.fx.Anim#afteranimate afteranimate}, and {@link Ext.fx.Anim#lastframe lastframe}. * Keyframed animations adds an additional {@link Ext.fx.Animator#keyframe keyframe} event which * fires for each keyframe in your animation. * * All animations support the {@link Ext.util.Observable#listeners listeners} configuration to attact functions to these events. * * startAnimate: function() { * var p1 = Ext.get('myElementId'); * p1.animate({ * duration: 100, * to: { * opacity: 0 * }, * listeners: { * beforeanimate: function() { * // Execute my custom method before the animation * this.myBeforeAnimateFn(); * }, * afteranimate: function() { * // Execute my custom method after the animation * this.myAfterAnimateFn(); * }, * scope: this * }); * }, * myBeforeAnimateFn: function() { * // My custom logic * }, * myAfterAnimateFn: function() { * // My custom logic * } * * Due to the fact that animations run asynchronously, you can determine if an animation is currently * running on any target by using the {@link Ext.util.Animate#getActiveAnimation getActiveAnimation} * method. This method will return false if there are no active animations or return the currently * running {@link Ext.fx.Anim} instance. * * In this example, we're going to wait for the current animation to finish, then stop any other * queued animations before we fade our element's opacity to 0: * * var curAnim = p1.getActiveAnimation(); * if (curAnim) { * curAnim.on('afteranimate', function() { * p1.stopAnimation(); * p1.animate({ * to: { * opacity: 0 * } * }); * }); * } */ Ext.define('Ext.util.Animate', { uses: ['Ext.fx.Manager', 'Ext.fx.Anim'], /** * Performs custom animation on this object. * * This method is applicable to both the {@link Ext.Component Component} class and the {@link Ext.draw.Sprite Sprite} * class. It performs animated transitions of certain properties of this object over a specified timeline. * * ### Animating a {@link Ext.Component Component} * * When animating a Component, the following properties may be specified in `from`, `to`, and `keyframe` objects: * * - `x` - The Component's page X position in pixels. * * - `y` - The Component's page Y position in pixels * * - `left` - The Component's `left` value in pixels. * * - `top` - The Component's `top` value in pixels. * * - `width` - The Component's `width` value in pixels. * * - `width` - The Component's `width` value in pixels. * * - `dynamic` - Specify as true to update the Component's layout (if it is a Container) at every frame of the animation. * *Use sparingly as laying out on every intermediate size change is an expensive operation.* * * For example, to animate a Window to a new size, ensuring that its internal layout and any shadow is correct: * * myWindow = Ext.create('Ext.window.Window', { * title: 'Test Component animation', * width: 500, * height: 300, * layout: { * type: 'hbox', * align: 'stretch' * }, * items: [{ * title: 'Left: 33%', * margins: '5 0 5 5', * flex: 1 * }, { * title: 'Left: 66%', * margins: '5 5 5 5', * flex: 2 * }] * }); * myWindow.show(); * myWindow.header.el.on('click', function() { * myWindow.animate({ * to: { * width: (myWindow.getWidth() == 500) ? 700 : 500, * height: (myWindow.getHeight() == 300) ? 400 : 300 * } * }); * }); * * For performance reasons, by default, the internal layout is only updated when the Window reaches its final `"to"` * size. If dynamic updating of the Window's child Components is required, then configure the animation with * `dynamic: true` and the two child items will maintain their proportions during the animation. * * @param {Object} config Configuration for {@link Ext.fx.Anim}. * Note that the {@link Ext.fx.Anim#to to} config is required. * @return {Object} this */ animate: function(animObj) { var me = this; if (Ext.fx.Manager.hasFxBlock(me.id)) { return me; } Ext.fx.Manager.queueFx(new Ext.fx.Anim(me.anim(animObj))); return this; }, // @private - process the passed fx configuration. anim: function(config) { if (!Ext.isObject(config)) { return (config) ? {} : false; } var me = this; if (config.stopAnimation) { me.stopAnimation(); } Ext.applyIf(config, Ext.fx.Manager.getFxDefaults(me.id)); return Ext.apply({ target: me, paused: true }, config); }, /** * Stops any running effects and clears this object's internal effects queue if it contains any additional effects * that haven't started yet. * @deprecated 4.0 Replaced by {@link #stopAnimation} * @return {Ext.Element} The Element * @method */ stopFx: Ext.Function.alias(Ext.util.Animate, 'stopAnimation'), /** * Stops any running effects and clears this object's internal effects queue if it contains any additional effects * that haven't started yet. * @return {Ext.Element} The Element */ stopAnimation: function() { Ext.fx.Manager.stopAnimation(this.id); return this; }, /** * Ensures that all effects queued after syncFx is called on this object are run concurrently. This is the opposite * of {@link #sequenceFx}. * @return {Object} this */ syncFx: function() { Ext.fx.Manager.setFxDefaults(this.id, { concurrent: true }); return this; }, /** * Ensures that all effects queued after sequenceFx is called on this object are run in sequence. This is the * opposite of {@link #syncFx}. * @return {Object} this */ sequenceFx: function() { Ext.fx.Manager.setFxDefaults(this.id, { concurrent: false }); return this; }, /** * @deprecated 4.0 Replaced by {@link #getActiveAnimation} * @inheritdoc Ext.util.Animate#getActiveAnimation * @method */ hasActiveFx: Ext.Function.alias(Ext.util.Animate, 'getActiveAnimation'), /** * Returns the current animation if this object has any effects actively running or queued, else returns false. * @return {Ext.fx.Anim/Boolean} Anim if element has active effects, else false */ getActiveAnimation: function() { return Ext.fx.Manager.getActiveAnimation(this.id); } }, function(){ // Apply Animate mixin manually until Element is defined in the proper 4.x way Ext.applyIf(Ext.Element.prototype, this.prototype); // We need to call this again so the animation methods get copied over to CE Ext.CompositeElementLite.importElementMethods(); }); /** * Given a component hierarchy of this: * * { * xtype: 'panel', * id: 'ContainerA', * layout: 'hbox', * renderTo: Ext.getBody(), * items: [ * { * id: 'ContainerB', * xtype: 'container', * items: [ * { id: 'ComponentA' } * ] * } * ] * } * * The rendering of the above proceeds roughly like this: * * - ContainerA's initComponent calls #render passing the `renderTo` property as the * container argument. * - `render` calls the `getRenderTree` method to get a complete {@link Ext.DomHelper} spec. * - `getRenderTree` fires the "beforerender" event and calls the #beforeRender * method. Its result is obtained by calling #getElConfig. * - The #getElConfig method uses the `renderTpl` and its render data as the content * of the `autoEl` described element. * - The result of `getRenderTree` is passed to {@link Ext.DomHelper#append}. * - The `renderTpl` contains calls to render things like docked items, container items * and raw markup (such as the `html` or `tpl` config properties). These calls are to * methods added to the {@link Ext.XTemplate} instance by #setupRenderTpl. * - The #setupRenderTpl method adds methods such as `renderItems`, `renderContent`, etc. * to the template. These are directed to "doRenderItems", "doRenderContent" etc.. * - The #setupRenderTpl calls traverse from components to their {@link Ext.layout.Layout} * object. * - When a container is rendered, it also has a `renderTpl`. This is processed when the * `renderContainer` method is called in the component's `renderTpl`. This call goes to * Ext.layout.container.Container#doRenderContainer. This method repeats this * process for all components in the container. * - After the top-most component's markup is generated and placed in to the DOM, the next * step is to link elements to their components and finish calling the component methods * `onRender` and `afterRender` as well as fire the corresponding events. * - The first step in this is to call #finishRender. This method descends the * component hierarchy and calls `onRender` and fires the `render` event. These calls * are delivered top-down to approximate the timing of these calls/events from previous * versions. * - During the pass, the component's `el` is set. Likewise, the `renderSelectors` and * `childEls` are applied to capture references to the component's elements. * - These calls are also made on the {@link Ext.layout.container.Container} layout to * capture its elements. Both of these classes use {@link Ext.util.ElementContainer} to * handle `childEls` processing. * - Once this is complete, a similar pass is made by calling #finishAfterRender. * This call also descends the component hierarchy, but this time the calls are made in * a bottom-up order to `afterRender`. * * @private */ Ext.define('Ext.util.Renderable', { requires: [ 'Ext.dom.Element' ], frameCls: Ext.baseCSSPrefix + 'frame', frameIdRegex: /[\-]frame\d+[TMB][LCR]$/, frameElementCls: { tl: [], tc: [], tr: [], ml: [], mc: [], mr: [], bl: [], bc: [], br: [] }, frameElNames: ['TL','TC','TR','ML','MC','MR','BL','BC','BR'], frameTpl: [ '{%this.renderDockedItems(out,values,0);%}', '', '
{parent.baseCls}-{parent.ui}-{.}-tl" style="background-position: {tl}; padding-left: {frameWidth}px" role="presentation">', '
{parent.baseCls}-{parent.ui}-{.}-tr" style="background-position: {tr}; padding-right: {frameWidth}px" role="presentation">', '
{parent.baseCls}-{parent.ui}-{.}-tc" style="background-position: {tc}; height: {frameWidth}px" role="presentation">
', '
', '
', '
', '
{parent.baseCls}-{parent.ui}-{.}-ml" style="background-position: {ml}; padding-left: {frameWidth}px" role="presentation">', '
{parent.baseCls}-{parent.ui}-{.}-mr" style="background-position: {mr}; padding-right: {frameWidth}px" role="presentation">', '
{parent.baseCls}-{parent.ui}-{.}-mc" role="presentation">', '{%this.applyRenderTpl(out, values)%}', '
', '
', '
', '', '
{parent.baseCls}-{parent.ui}-{.}-bl" style="background-position: {bl}; padding-left: {frameWidth}px" role="presentation">', '
{parent.baseCls}-{parent.ui}-{.}-br" style="background-position: {br}; padding-right: {frameWidth}px" role="presentation">', '
{parent.baseCls}-{parent.ui}-{.}-bc" style="background-position: {bc}; height: {frameWidth}px" role="presentation">
', '
', '
', '
', '{%this.renderDockedItems(out,values,1);%}' ], frameTableTpl: [ '{%this.renderDockedItems(out,values,0);%}', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '', '
{parent.baseCls}-{parent.ui}-{.}-tl" style="background-position: {tl}; padding-left:{frameWidth}px" role="presentation"> {parent.baseCls}-{parent.ui}-{.}-tc" style="background-position: {tc}; height: {frameWidth}px" role="presentation"> {parent.baseCls}-{parent.ui}-{.}-tr" style="background-position: {tr}; padding-left: {frameWidth}px" role="presentation">
{parent.baseCls}-{parent.ui}-{.}-ml" style="background-position: {ml}; padding-left: {frameWidth}px" role="presentation"> {parent.baseCls}-{parent.ui}-{.}-mc" style="background-position: 0 0;" role="presentation">', '{%this.applyRenderTpl(out, values)%}', ' {parent.baseCls}-{parent.ui}-{.}-mr" style="background-position: {mr}; padding-left: {frameWidth}px" role="presentation">
{parent.baseCls}-{parent.ui}-{.}-bl" style="background-position: {bl}; padding-left: {frameWidth}px" role="presentation"> {parent.baseCls}-{parent.ui}-{.}-bc" style="background-position: {bc}; height: {frameWidth}px" role="presentation"> {parent.baseCls}-{parent.ui}-{.}-br" style="background-position: {br}; padding-left: {frameWidth}px" role="presentation">
', '{%this.renderDockedItems(out,values,1);%}' ], /** * Allows addition of behavior after rendering is complete. At this stage the Component’s Element * will have been styled according to the configuration, will have had any configured CSS class * names added, and will be in the configured visibility and the configured enable state. * * @template * @protected */ afterRender : function() { var me = this, data = {}, protoEl = me.protoEl, target = me.getTargetEl(), item; me.finishRenderChildren(); if (me.styleHtmlContent) { target.addCls(me.styleHtmlCls); } protoEl.writeTo(data); // Here we apply any styles that were set on the protoEl during the rendering phase // A majority of times this will not happen, but we still need to handle it item = data.removed; if (item) { target.removeCls(item); } item = data.cls; if (item.length) { target.addCls(item); } item = data.style; if (data.style) { target.setStyle(item); } me.protoEl = null; // If this is the outermost Container, lay it out as soon as it is rendered. if (!me.ownerCt) { me.updateLayout(); } }, afterFirstLayout : function(width, height) { var me = this, hasX = Ext.isDefined(me.x), hasY = Ext.isDefined(me.y), pos, xy; // For floaters, calculate x and y if they aren't defined by aligning // the sized element to the center of either the container or the ownerCt if (me.floating && (!hasX || !hasY)) { if (me.floatParent) { pos = me.floatParent.getTargetEl().getViewRegion(); xy = me.el.getAlignToXY(me.floatParent.getTargetEl(), 'c-c'); pos.left = xy[0] - pos.left; pos.top = xy[1] - pos.top; } else { xy = me.el.getAlignToXY(me.container, 'c-c'); pos = me.container.translatePoints(xy[0], xy[1]); } me.x = hasX ? me.x : pos.left; me.y = hasY ? me.y : pos.top; hasX = hasY = true; } if (hasX || hasY) { me.setPosition(me.x, me.y); } me.onBoxReady(width, height); if (me.hasListeners.boxready) { me.fireEvent('boxready', me, width, height); } }, onBoxReady: Ext.emptyFn, /** * Sets references to elements inside the component. This applies {@link Ext.AbstractComponent#cfg-renderSelectors renderSelectors} * as well as {@link Ext.AbstractComponent#cfg-childEls childEls}. * @private */ applyRenderSelectors: function() { var me = this, selectors = me.renderSelectors, el = me.el, dom = el.dom, selector; me.applyChildEls(el); // We still support renderSelectors. There are a few places in the framework that // need them and they are a documented part of the API. In fact, we support mixing // childEls and renderSelectors (no reason not to). if (selectors) { for (selector in selectors) { if (selectors.hasOwnProperty(selector) && selectors[selector]) { me[selector] = Ext.get(Ext.DomQuery.selectNode(selectors[selector], dom)); } } } }, beforeRender: function () { var me = this, target = me.getTargetEl(), layout = me.getComponentLayout(); // Just before rendering, set the frame flag if we are an always-framed component like Window or Tip. me.frame = me.frame || me.alwaysFramed; if (!layout.initialized) { layout.initLayout(); } // Attempt to set overflow style prior to render if the targetEl can be accessed. // If the targetEl does not exist yet, this will take place in finishRender if (target) { target.setStyle(me.getOverflowStyle()); me.overflowStyleSet = true; } me.setUI(me.ui); if (me.disabled) { // pass silent so the event doesn't fire the first time. me.disable(true); } }, /** * @private * Called from the selected frame generation template to insert this Component's inner structure inside the framing structure. * * When framing is used, a selected frame generation template is used as the primary template of the #getElConfig instead * of the configured {@link Ext.AbstractComponent#renderTpl renderTpl}. The renderTpl is invoked by this method which is injected into the framing template. */ doApplyRenderTpl: function(out, values) { // Careful! This method is bolted on to the frameTpl so all we get for context is // the renderData! The "this" pointer is the frameTpl instance! var me = values.$comp, tpl; // Don't do this if the component is already rendered: if (!me.rendered) { tpl = me.initRenderTpl(); tpl.applyOut(values.renderData, out); } }, /** * Handles autoRender. * Floating Components may have an ownerCt. If they are asking to be constrained, constrain them within that * ownerCt, and have their z-index managed locally. Floating Components are always rendered to document.body */ doAutoRender: function() { var me = this; if (!me.rendered) { if (me.floating) { me.render(document.body); } else { me.render(Ext.isBoolean(me.autoRender) ? Ext.getBody() : me.autoRender); } } }, doRenderContent: function (out, renderData) { // Careful! This method is bolted on to the renderTpl so all we get for context is // the renderData! The "this" pointer is the renderTpl instance! var me = renderData.$comp; if (me.html) { Ext.DomHelper.generateMarkup(me.html, out); delete me.html; } if (me.tpl) { // Make sure this.tpl is an instantiated XTemplate if (!me.tpl.isTemplate) { me.tpl = new Ext.XTemplate(me.tpl); } if (me.data) { //me.tpl[me.tplWriteMode](target, me.data); me.tpl.applyOut(me.data, out); delete me.data; } } }, doRenderFramingDockedItems: function (out, renderData, after) { // Careful! This method is bolted on to the frameTpl so all we get for context is // the renderData! The "this" pointer is the frameTpl instance! var me = renderData.$comp; // Most components don't have dockedItems, so check for doRenderDockedItems on the // component (also, don't do this if the component is already rendered): if (!me.rendered && me.doRenderDockedItems) { // The "renderData" property is placed in scope for the renderTpl, but we don't // want to render docked items at that level in addition to the framing level: renderData.renderData.$skipDockedItems = true; // doRenderDockedItems requires the $comp property on renderData, but this is // set on the frameTpl's renderData as well: me.doRenderDockedItems.call(this, out, renderData, after); } }, /** * This method visits the rendered component tree in a "top-down" order. That is, this * code runs on a parent component before running on a child. This method calls the * {@link #onRender} method of each component. * @param {Number} containerIdx The index into the Container items of this Component. * * @private */ finishRender: function(containerIdx) { var me = this, tpl, data, contentEl, el, pre, hide; // We are typically called w/me.el==null as a child of some ownerCt that is being // rendered. We are also called by render for a normal component (w/o a configured // me.el). In this case, render sets me.el and me.rendering (indirectly). Lastly // we are also called on a component (like a Viewport) that has a configured me.el // (body for a Viewport) when render is called. In this case, it is not flagged as // "me.rendering" yet becasue it does not produce a renderTree. We use this to know // not to regen the renderTpl. if (!me.el || me.$pid) { if (me.container) { el = me.container.getById(me.id, true); } else { el = Ext.getDom(me.id); } if (!me.el) { // Typical case: we produced the el during render me.wrapPrimaryEl(el); } else { // We were configured with an el and created a proxy, so now we can swap // the proxy for me.el: delete me.$pid; if (!me.el.dom) { // make sure me.el is an Element me.wrapPrimaryEl(me.el); } el.parentNode.insertBefore(me.el.dom, el); Ext.removeNode(el); // remove placeholder el // TODO - what about class/style? } } else if (!me.rendering) { // We were configured with an el and then told to render (e.g., Viewport). We // need to generate the proper DOM. Insert first because the layout system // insists that child Component elements indices match the Component indices. tpl = me.initRenderTpl(); if (tpl) { data = me.initRenderData(); tpl.insertFirst(me.getTargetEl(), data); } } // else we are rendering if (!me.container) { // top-level rendered components will already have me.container set up me.container = Ext.get(me.el.dom.parentNode); } if (me.ctCls) { me.container.addCls(me.ctCls); } // Sets the rendered flag and clears the redering flag me.onRender(me.container, containerIdx); // If we could not access a target protoEl in bewforeRender, we have to set the overflow styles here. if (!me.overflowStyleSet) { me.getTargetEl().setStyle(me.getOverflowStyle()); } // Tell the encapsulating element to hide itself in the way the Component is configured to hide // This means DISPLAY, VISIBILITY or OFFSETS. me.el.setVisibilityMode(Ext.Element[me.hideMode.toUpperCase()]); if (me.overCls) { me.el.hover(me.addOverCls, me.removeOverCls, me); } if (me.hasListeners.render) { me.fireEvent('render', me); } if (me.contentEl) { pre = Ext.baseCSSPrefix; hide = pre + 'hide-'; contentEl = Ext.get(me.contentEl); contentEl.removeCls([pre+'hidden', hide+'display', hide+'offsets', hide+'nosize']); me.getTargetEl().appendChild(contentEl.dom); } me.afterRender(); // this can cause a layout if (me.hasListeners.afterrender) { me.fireEvent('afterrender', me); } me.initEvents(); if (me.hidden) { // Hiding during the render process should not perform any ancillary // actions that the full hide process does; It is not hiding, it begins in a hidden state.' // So just make the element hidden according to the configured hideMode me.el.hide(); } }, finishRenderChildren: function () { var layout = this.getComponentLayout(); layout.finishRender(); }, getElConfig : function() { var me = this, autoEl = me.autoEl, frameInfo = me.getFrameInfo(), config = { tag: 'div', tpl: frameInfo ? me.initFramingTpl(frameInfo.table) : me.initRenderTpl() }, i, frameElNames, len, suffix, frameGenId; me.initStyles(me.protoEl); me.protoEl.writeTo(config); me.protoEl.flush(); if (Ext.isString(autoEl)) { config.tag = autoEl; } else { Ext.apply(config, autoEl); // harmless if !autoEl } // It's important to assign the id here as an autoEl.id could have been (wrongly) applied and this would get things out of sync config.id = me.id; if (config.tpl) { // Use the framingTpl as the main content creating template. It will call out to this.applyRenderTpl(out, values) if (frameInfo) { frameElNames = me.frameElNames; len = frameElNames.length; frameGenId = me.id + '-frame1'; me.frameGenId = 1; config.tplData = Ext.apply({}, { $comp: me, fgid: frameGenId, ui: me.ui, uiCls: me.uiCls, frameCls: me.frameCls, baseCls: me.baseCls, frameWidth: frameInfo.maxWidth, top: !!frameInfo.top, left: !!frameInfo.left, right: !!frameInfo.right, bottom: !!frameInfo.bottom, renderData: me.initRenderData() }, me.getFramePositions(frameInfo)); // Add the childEls for each of the frame elements for (i = 0; i < len; i++) { suffix = frameElNames[i]; me.addChildEls({ name: 'frame' + suffix, id: frameGenId + suffix }); } // Panel must have a frameBody me.addChildEls({ name: 'frameBody', id: frameGenId + 'MC' }); } else { config.tplData = me.initRenderData(); } } return config; }, // Create the framingTpl from the string. // Poke in a reference to applyRenderTpl(frameInfo, out) initFramingTpl: function(table) { var tpl = table ? this.getTpl('frameTableTpl') : this.getTpl('frameTpl'); if (tpl && !tpl.applyRenderTpl) { this.setupFramingTpl(tpl); } return tpl; }, /** * @private * Inject a reference to the function which applies the render template into the framing template. The framing template * wraps the content. */ setupFramingTpl: function(frameTpl) { frameTpl.applyRenderTpl = this.doApplyRenderTpl; frameTpl.renderDockedItems = this.doRenderFramingDockedItems; }, /** * This function takes the position argument passed to onRender and returns a * DOM element that you can use in the insertBefore. * @param {String/Number/Ext.dom.Element/HTMLElement} position Index, element id or element you want * to put this component before. * @return {HTMLElement} DOM element that you can use in the insertBefore */ getInsertPosition: function(position) { // Convert the position to an element to insert before if (position !== undefined) { if (Ext.isNumber(position)) { position = this.container.dom.childNodes[position]; } else { position = Ext.getDom(position); } } return position; }, getRenderTree: function() { var me = this; if (!me.hasListeners.beforerender || me.fireEvent('beforerender', me) !== false) { me.beforeRender(); // Flag to let the layout's finishRenderItems and afterFinishRenderItems // know which items to process me.rendering = true; if (me.el) { // Since we are producing a render tree, we produce a "proxy el" that will // sit in the rendered DOM precisely where me.el belongs. We replace the // proxy el in the finishRender phase. return { tag: 'div', id: (me.$pid = Ext.id()) }; } return me.getElConfig(); } return null; }, initContainer: function(container) { var me = this; // If you render a component specifying the el, we get the container // of the el, and make sure we dont move the el around in the dom // during the render if (!container && me.el) { container = me.el.dom.parentNode; me.allowDomMove = false; } me.container = container.dom ? container : Ext.get(container); return me.container; }, /** * Initialized the renderData to be used when rendering the renderTpl. * @return {Object} Object with keys and values that are going to be applied to the renderTpl * @private */ initRenderData: function() { var me = this; return Ext.apply({ $comp: me, id: me.id, ui: me.ui, uiCls: me.uiCls, baseCls: me.baseCls, componentCls: me.componentCls, frame: me.frame }, me.renderData); }, /** * Initializes the renderTpl. * @return {Ext.XTemplate} The renderTpl XTemplate instance. * @private */ initRenderTpl: function() { var tpl = this.getTpl('renderTpl'); if (tpl && !tpl.renderContent) { this.setupRenderTpl(tpl); } return tpl; }, /** * Template method called when this Component's DOM structure is created. * * At this point, this Component's (and all descendants') DOM structure *exists* but it has not * been layed out (positioned and sized). * * Subclasses which override this to gain access to the structure at render time should * call the parent class's method before attempting to access any child elements of the Component. * * @param {Ext.core.Element} parentNode The parent Element in which this Component's encapsulating element is contained. * @param {Number} containerIdx The index within the parent Container's child collection of this Component. * * @template * @protected */ onRender: function(parentNode, containerIdx) { var me = this, x = me.x, y = me.y, lastBox, width, height, el = me.el, body = Ext.getBody().dom; // Wrap this Component in a reset wraper if necessary if (Ext.scopeResetCSS && !me.ownerCt) { // If this component's el is the body element, we add the reset class to the html tag if (el.dom === body) { el.parent().addCls(Ext.resetCls); } // Otherwise, we ensure that there is a wrapper which has the reset class else { // Floaters rendered into the body can all be bumped into the common reset element if (me.floating && me.el.dom.parentNode === body) { Ext.resetElement.appendChild(me.el); } // Else we wrap this element in an element that adds the reset class. else { // Wrap this Component's DOM with a reset structure as determined in EventManager's initExtCss closure. me.resetEl = el.wrap(Ext.resetElementSpec, false, Ext.supports.CSS3LinearGradient ? undefined : '*'); } } } me.applyRenderSelectors(); // Flag set on getRenderTree to flag to the layout's postprocessing routine that // the Component is in the process of being rendered and needs postprocessing. delete me.rendering; me.rendered = true; // We need to remember these to avoid writing them during the initial layout: lastBox = null; if (x !== undefined) { lastBox = lastBox || {}; lastBox.x = x; } if (y !== undefined) { lastBox = lastBox || {}; lastBox.y = y; } // Framed components need their width/height to apply to the frame, which is // best handled in layout at present. // If we're using the content box model, we also cannot assign initial sizes since we do not know the border widths to subtract if (!me.getFrameInfo() && Ext.isBorderBox) { width = me.width; height = me.height; if (typeof width == 'number') { lastBox = lastBox || {}; lastBox.width = width; } if (typeof height == 'number') { lastBox = lastBox || {}; lastBox.height = height; } } me.lastBox = me.el.lastBox = lastBox; }, /** * Renders the Component into the passed HTML element. * * **If you are using a {@link Ext.container.Container Container} object to house this * Component, then do not use the render method.** * * A Container's child Components are rendered by that Container's * {@link Ext.container.Container#layout layout} manager when the Container is first rendered. * * If the Container is already rendered when a new child Component is added, you may need to call * the Container's {@link Ext.container.Container#doLayout doLayout} to refresh the view which * causes any unrendered child Components to be rendered. This is required so that you can add * multiple child components if needed while only refreshing the layout once. * * When creating complex UIs, it is important to remember that sizing and positioning * of child items is the responsibility of the Container's {@link Ext.container.Container#layout layout} * manager. If you expect child items to be sized in response to user interactions, you must * configure the Container with a layout manager which creates and manages the type of layout you * have in mind. * * **Omitting the Container's {@link Ext.Container#layout layout} config means that a basic * layout manager is used which does nothing but render child components sequentially into the * Container. No sizing or positioning will be performed in this situation.** * * @param {Ext.Element/HTMLElement/String} [container] The element this Component should be * rendered into. If it is being created from existing markup, this should be omitted. * @param {String/Number} [position] The element ID or DOM node index within the container **before** * which this component will be inserted (defaults to appending to the end of the container) */ render: function(container, position) { var me = this, el = me.el && (me.el = Ext.get(me.el)), // ensure me.el is wrapped vetoed, tree, nextSibling; Ext.suspendLayouts(); container = me.initContainer(container); nextSibling = me.getInsertPosition(position); if (!el) { tree = me.getRenderTree(); if (me.ownerLayout && me.ownerLayout.transformItemRenderTree) { tree = me.ownerLayout.transformItemRenderTree(tree); } // tree will be null if a beforerender listener returns false if (tree) { if (nextSibling) { el = Ext.DomHelper.insertBefore(nextSibling, tree); } else { el = Ext.DomHelper.append(container, tree); } me.wrapPrimaryEl(el); } } else { if (!me.hasListeners.beforerender || me.fireEvent('beforerender', me) !== false) { // Set configured styles on pre-rendered Component's element me.initStyles(el); if (me.allowDomMove !== false) { //debugger; // TODO if (nextSibling) { container.dom.insertBefore(el.dom, nextSibling); } else { container.dom.appendChild(el.dom); } } } else { vetoed = true; } } if (el && !vetoed) { me.finishRender(position); } Ext.resumeLayouts(!container.isDetachedBody); }, /** * Ensures that this component is attached to `document.body`. If the component was * rendered to {@link Ext#getDetachedBody}, then it will be appended to `document.body`. * Any configured position is also restored. * @param {Boolean} [runLayout=false] True to run the component's layout. */ ensureAttachedToBody: function (runLayout) { var comp = this, body; while (comp.ownerCt) { comp = comp.ownerCt; } if (comp.container.isDetachedBody) { comp.container = body = Ext.resetElement; body.appendChild(comp.el.dom); if (runLayout) { comp.updateLayout(); } if (typeof comp.x == 'number' || typeof comp.y == 'number') { comp.setPosition(comp.x, comp.y); } } }, setupRenderTpl: function (renderTpl) { renderTpl.renderBody = renderTpl.renderContent = this.doRenderContent; }, wrapPrimaryEl: function (dom) { this.el = Ext.get(dom, true); }, /** * @private */ initFrame : function() { if (Ext.supports.CSS3BorderRadius || !this.frame) { return; } var me = this, frameInfo = me.getFrameInfo(), frameWidth, frameTpl, frameGenId, i, frameElNames = me.frameElNames, len = frameElNames.length, suffix; if (frameInfo) { frameWidth = frameInfo.maxWidth; frameTpl = me.getFrameTpl(frameInfo.table); // since we render id's into the markup and id's NEED to be unique, we have a // simple strategy for numbering their generations. me.frameGenId = frameGenId = (me.frameGenId || 0) + 1; frameGenId = me.id + '-frame' + frameGenId; // Here we render the frameTpl to this component. This inserts the 9point div or the table framing. frameTpl.insertFirst(me.el, Ext.apply({ $comp: me, fgid: frameGenId, ui: me.ui, uiCls: me.uiCls, frameCls: me.frameCls, baseCls: me.baseCls, frameWidth: frameWidth, top: !!frameInfo.top, left: !!frameInfo.left, right: !!frameInfo.right, bottom: !!frameInfo.bottom }, me.getFramePositions(frameInfo))); // The frameBody is returned in getTargetEl, so that layouts render items to the correct target. me.frameBody = me.el.down('.' + me.frameCls + '-mc'); // Clean out the childEls for the old frame elements (the majority of the els) me.removeChildEls(function (c) { return c.id && me.frameIdRegex.test(c.id); }); // Grab references to the childEls for each of the new frame elements for (i = 0; i < len; i++) { suffix = frameElNames[i]; me['frame' + suffix] = me.el.getById(frameGenId + suffix); } } }, updateFrame: function() { if (Ext.supports.CSS3BorderRadius || !this.frame) { return; } var me = this, wasTable = this.frameSize && this.frameSize.table, oldFrameTL = this.frameTL, oldFrameBL = this.frameBL, oldFrameML = this.frameML, oldFrameMC = this.frameMC, newMCClassName; this.initFrame(); if (oldFrameMC) { if (me.frame) { // Store the class names set on the new MC newMCClassName = this.frameMC.dom.className; // Framing elements have been selected in initFrame, no need to run applyRenderSelectors // Replace the new mc with the old mc oldFrameMC.insertAfter(this.frameMC); this.frameMC.remove(); // Restore the reference to the old frame mc as the framebody this.frameBody = this.frameMC = oldFrameMC; // Apply the new mc classes to the old mc element oldFrameMC.dom.className = newMCClassName; // Remove the old framing if (wasTable) { me.el.query('> table')[1].remove(); } else { if (oldFrameTL) { oldFrameTL.remove(); } if (oldFrameBL) { oldFrameBL.remove(); } if (oldFrameML) { oldFrameML.remove(); } } } } else if (me.frame) { this.applyRenderSelectors(); } }, /** * @private * On render, reads an encoded style attribute, "background-position" from the style of this Component's element. * This information is memoized based upon the CSS class name of this Component's element. * Because child Components are rendered as textual HTML as part of the topmost Container, a dummy div is inserted * into the document to receive the document element's CSS class name, and therefore style attributes. */ getFrameInfo: function() { // If native framing can be used, or this component is not going to be framed, then do not attempt to read CSS framing info. if (Ext.supports.CSS3BorderRadius || !this.frame) { return false; } var me = this, frameInfoCache = me.frameInfoCache, el = me.el || me.protoEl, cls = el.dom ? el.dom.className : el.classList.join(' '), frameInfo = frameInfoCache[cls], styleEl, left, top, info; if (frameInfo == null) { // Get the singleton frame style proxy with our el class name stamped into it. styleEl = Ext.fly(me.getStyleProxy(cls), 'frame-style-el'); left = styleEl.getStyle('background-position-x'); top = styleEl.getStyle('background-position-y'); // Some browsers don't support background-position-x and y, so for those // browsers let's split background-position into two parts. if (!left && !top) { info = styleEl.getStyle('background-position').split(' '); left = info[0]; top = info[1]; } frameInfo = me.calculateFrame(left, top); if (frameInfo) { // Just to be sure we set the background image of the el to none. el.setStyle('background-image', 'none'); } // This happens when you set frame: true explicitly without using the x-frame mixin in sass. // This way IE can't figure out what sizes to use and thus framing can't work. if (me.frame === true && !frameInfo) { Ext.log.error('You have set frame: true explicity on this component (' + me.getXType() + ') and it ' + 'does not have any framing defined in the CSS template. In this case IE cannot figure out ' + 'what sizes to use and thus framing on this component will be disabled.'); } frameInfoCache[cls] = frameInfo; } me.frame = !!frameInfo; me.frameSize = frameInfo; return frameInfo; }, calculateFrame: function(left, top){ // We actually pass a string in the form of '[type][tl][tr]px [direction][br][bl]px' as // the background position of this.el from the CSS to indicate to IE that this component needs // framing. We parse it here. if (!(parseInt(left, 10) >= 1000000 && parseInt(top, 10) >= 1000000)) { return false; } var max = Math.max, tl = parseInt(left.substr(3, 2), 10), tr = parseInt(left.substr(5, 2), 10), br = parseInt(top.substr(3, 2), 10), bl = parseInt(top.substr(5, 2), 10), frameInfo = { // Table markup starts with 110, div markup with 100. table: left.substr(0, 3) == '110', // Determine if we are dealing with a horizontal or vertical component vertical: top.substr(0, 3) == '110', // Get and parse the different border radius sizes top: max(tl, tr), right: max(tr, br), bottom: max(bl, br), left: max(tl, bl) }; frameInfo.maxWidth = max(frameInfo.top, frameInfo.right, frameInfo.bottom, frameInfo.left); frameInfo.width = frameInfo.left + frameInfo.right; frameInfo.height = frameInfo.top + frameInfo.bottom; return frameInfo; }, /** * @private * Returns an offscreen div with the same class name as the element this is being rendered. * This is because child item rendering takes place in a detached div which, being not part of the document, has no styling. */ getStyleProxy: function(cls) { var result = this.styleProxyEl || (Ext.AbstractComponent.prototype.styleProxyEl = Ext.resetElement.createChild({ style: { position: 'absolute', top: '-10000px' } }, null, true)); result.className = cls; return result; }, getFramePositions: function(frameInfo) { var me = this, frameWidth = frameInfo.maxWidth, dock = me.dock, positions, tc, bc, ml, mr; if (frameInfo.vertical) { tc = '0 -' + (frameWidth * 0) + 'px'; bc = '0 -' + (frameWidth * 1) + 'px'; if (dock && dock == "right") { tc = 'right -' + (frameWidth * 0) + 'px'; bc = 'right -' + (frameWidth * 1) + 'px'; } positions = { tl: '0 -' + (frameWidth * 0) + 'px', tr: '0 -' + (frameWidth * 1) + 'px', bl: '0 -' + (frameWidth * 2) + 'px', br: '0 -' + (frameWidth * 3) + 'px', ml: '-' + (frameWidth * 1) + 'px 0', mr: 'right 0', tc: tc, bc: bc }; } else { ml = '-' + (frameWidth * 0) + 'px 0'; mr = 'right 0'; if (dock && dock == "bottom") { ml = 'left bottom'; mr = 'right bottom'; } positions = { tl: '0 -' + (frameWidth * 2) + 'px', tr: 'right -' + (frameWidth * 3) + 'px', bl: '0 -' + (frameWidth * 4) + 'px', br: 'right -' + (frameWidth * 5) + 'px', ml: ml, mr: mr, tc: '0 -' + (frameWidth * 0) + 'px', bc: '0 -' + (frameWidth * 1) + 'px' }; } return positions; }, /** * @private */ getFrameTpl : function(table) { return this.getTpl(table ? 'frameTableTpl' : 'frameTpl'); }, // Cache the frame information object so as not to cause style recalculations frameInfoCache: {} }); /** * Provides searching of Components within Ext.ComponentManager (globally) or a specific * Ext.container.Container on the document with a similar syntax to a CSS selector. * * Components can be retrieved by using their {@link Ext.Component xtype} * * - `component` * - `gridpanel` * * Matching by xtype matches inherited types, so in the following code, the previous field * *of any type which inherits from `TextField`* will be found: * * prevField = myField.previousNode('textfield'); * * To match only the exact type, pass the "shallow" flag (See {@link Ext.AbstractComponent#isXType AbstractComponent's isXType method}) * * prevTextField = myField.previousNode('textfield(true)'); * * An itemId or id must be prefixed with a # * * - `#myContainer` * * Attributes must be wrapped in brackets * * - `component[autoScroll]` * - `panel[title="Test"]` * * Member expressions from candidate Components may be tested. If the expression returns a *truthy* value, * the candidate Component will be included in the query: * * var disabledFields = myFormPanel.query("{isDisabled()}"); * * Pseudo classes may be used to filter results in the same way as in {@link Ext.DomQuery DomQuery}: * * // Function receives array and returns a filtered array. * Ext.ComponentQuery.pseudos.invalid = function(items) { * var i = 0, l = items.length, c, result = []; * for (; i < l; i++) { * if (!(c = items[i]).isValid()) { * result.push(c); * } * } * return result; * }; * * var invalidFields = myFormPanel.query('field:invalid'); * if (invalidFields.length) { * invalidFields[0].getEl().scrollIntoView(myFormPanel.body); * for (var i = 0, l = invalidFields.length; i < l; i++) { * invalidFields[i].getEl().frame("red"); * } * } * * Default pseudos include: * * - not * - first * - last * * Queries return an array of components. * Here are some example queries. * * // retrieve all Ext.Panels in the document by xtype * var panelsArray = Ext.ComponentQuery.query('panel'); * * // retrieve all Ext.Panels within the container with an id myCt * var panelsWithinmyCt = Ext.ComponentQuery.query('#myCt panel'); * * // retrieve all direct children which are Ext.Panels within myCt * var directChildPanel = Ext.ComponentQuery.query('#myCt > panel'); * * // retrieve all grids and trees * var gridsAndTrees = Ext.ComponentQuery.query('gridpanel, treepanel'); * * For easy access to queries based from a particular Container see the {@link Ext.container.Container#query}, * {@link Ext.container.Container#down} and {@link Ext.container.Container#child} methods. Also see * {@link Ext.Component#up}. */ Ext.define('Ext.ComponentQuery', { singleton: true, uses: ['Ext.ComponentManager'] }, function() { var cq = this, // A function source code pattern with a placeholder which accepts an expression which yields a truth value when applied // as a member on each item in the passed array. filterFnPattern = [ 'var r = [],', 'i = 0,', 'it = items,', 'l = it.length,', 'c;', 'for (; i < l; i++) {', 'c = it[i];', 'if (c.{0}) {', 'r.push(c);', '}', '}', 'return r;' ].join(''), filterItems = function(items, operation) { // Argument list for the operation is [ itemsArray, operationArg1, operationArg2...] // The operation's method loops over each item in the candidate array and // returns an array of items which match its criteria return operation.method.apply(this, [ items ].concat(operation.args)); }, getItems = function(items, mode) { var result = [], i = 0, length = items.length, candidate, deep = mode !== '>'; for (; i < length; i++) { candidate = items[i]; if (candidate.getRefItems) { result = result.concat(candidate.getRefItems(deep)); } } return result; }, getAncestors = function(items) { var result = [], i = 0, length = items.length, candidate; for (; i < length; i++) { candidate = items[i]; while (!!(candidate = (candidate.ownerCt || candidate.floatParent))) { result.push(candidate); } } return result; }, // Filters the passed candidate array and returns only items which match the passed xtype filterByXType = function(items, xtype, shallow) { if (xtype === '*') { return items.slice(); } else { var result = [], i = 0, length = items.length, candidate; for (; i < length; i++) { candidate = items[i]; if (candidate.isXType(xtype, shallow)) { result.push(candidate); } } return result; } }, // Filters the passed candidate array and returns only items which have the passed className filterByClassName = function(items, className) { var EA = Ext.Array, result = [], i = 0, length = items.length, candidate; for (; i < length; i++) { candidate = items[i]; if (candidate.hasCls(className)) { result.push(candidate); } } return result; }, // Filters the passed candidate array and returns only items which have the specified property match filterByAttribute = function(items, property, operator, value) { var result = [], i = 0, length = items.length, candidate; for (; i < length; i++) { candidate = items[i]; if (!value ? !!candidate[property] : (String(candidate[property]) === value)) { result.push(candidate); } } return result; }, // Filters the passed candidate array and returns only items which have the specified itemId or id filterById = function(items, id) { var result = [], i = 0, length = items.length, candidate; for (; i < length; i++) { candidate = items[i]; if (candidate.getItemId() === id) { result.push(candidate); } } return result; }, // Filters the passed candidate array and returns only items which the named pseudo class matcher filters in filterByPseudo = function(items, name, value) { return cq.pseudos[name](items, value); }, // Determines leading mode // > for direct child, and ^ to switch to ownerCt axis modeRe = /^(\s?([>\^])\s?|\s|$)/, // Matches a token with possibly (true|false) appended for the "shallow" parameter tokenRe = /^(#)?([\w\-]+|\*)(?:\((true|false)\))?/, matchers = [{ // Checks for .xtype with possibly (true|false) appended for the "shallow" parameter re: /^\.([\w\-]+)(?:\((true|false)\))?/, method: filterByXType },{ // checks for [attribute=value] re: /^(?:[\[](?:@)?([\w\-]+)\s?(?:(=|.=)\s?['"]?(.*?)["']?)?[\]])/, method: filterByAttribute }, { // checks for #cmpItemId re: /^#([\w\-]+)/, method: filterById }, { // checks for :() re: /^\:([\w\-]+)(?:\(((?:\{[^\}]+\})|(?:(?!\{)[^\s>\/]*?(?!\})))\))?/, method: filterByPseudo }, { // checks for {} re: /^(?:\{([^\}]+)\})/, method: filterFnPattern }]; // Internal class Ext.ComponentQuery.Query cq.Query = Ext.extend(Object, { constructor: function(cfg) { cfg = cfg || {}; Ext.apply(this, cfg); }, // Executes this Query upon the selected root. // The root provides the initial source of candidate Component matches which are progressively // filtered by iterating through this Query's operations cache. // If no root is provided, all registered Components are searched via the ComponentManager. // root may be a Container who's descendant Components are filtered // root may be a Component with an implementation of getRefItems which provides some nested Components such as the // docked items within a Panel. // root may be an array of candidate Components to filter using this Query. execute : function(root) { var operations = this.operations, i = 0, length = operations.length, operation, workingItems; // no root, use all Components in the document if (!root) { workingItems = Ext.ComponentManager.all.getArray(); } // Root is a candidate Array else if (Ext.isArray(root)) { workingItems = root; } // Root is a MixedCollection else if (root.isMixedCollection) { workingItems = root.items; } // We are going to loop over our operations and take care of them // one by one. for (; i < length; i++) { operation = operations[i]; // The mode operation requires some custom handling. // All other operations essentially filter down our current // working items, while mode replaces our current working // items by getting children from each one of our current // working items. The type of mode determines the type of // children we get. (e.g. > only gets direct children) if (operation.mode === '^') { workingItems = getAncestors(workingItems || [root]); } else if (operation.mode) { workingItems = getItems(workingItems || [root], operation.mode); } else { workingItems = filterItems(workingItems || getItems([root]), operation); } // If this is the last operation, it means our current working // items are the final matched items. Thus return them! if (i === length -1) { return workingItems; } } return []; }, is: function(component) { var operations = this.operations, components = Ext.isArray(component) ? component : [component], originalLength = components.length, lastOperation = operations[operations.length-1], ln, i; components = filterItems(components, lastOperation); if (components.length === originalLength) { if (operations.length > 1) { for (i = 0, ln = components.length; i < ln; i++) { if (Ext.Array.indexOf(this.execute(), components[i]) === -1) { return false; } } } return true; } return false; } }); Ext.apply(this, { // private cache of selectors and matching ComponentQuery.Query objects cache: {}, // private cache of pseudo class filter functions pseudos: { not: function(components, selector){ var CQ = Ext.ComponentQuery, i = 0, length = components.length, results = [], index = -1, component; for(; i < length; ++i) { component = components[i]; if (!CQ.is(component, selector)) { results[++index] = component; } } return results; }, first: function(components) { var ret = []; if (components.length > 0) { ret.push(components[0]); } return ret; }, last: function(components) { var len = components.length, ret = []; if (len > 0) { ret.push(components[len - 1]); } return ret; } }, /** * Returns an array of matched Components from within the passed root object. * * This method filters returned Components in a similar way to how CSS selector based DOM * queries work using a textual selector string. * * See class summary for details. * * @param {String} selector The selector string to filter returned Components * @param {Ext.container.Container} root The Container within which to perform the query. * If omitted, all Components within the document are included in the search. * * This parameter may also be an array of Components to filter according to the selector.

* @returns {Ext.Component[]} The matched Components. * * @member Ext.ComponentQuery */ query: function(selector, root) { var selectors = selector.split(','), length = selectors.length, i = 0, results = [], noDupResults = [], dupMatcher = {}, query, resultsLn, cmp; for (; i < length; i++) { selector = Ext.String.trim(selectors[i]); query = this.cache[selector] || (this.cache[selector] = this.parse(selector)); results = results.concat(query.execute(root)); } // multiple selectors, potential to find duplicates // lets filter them out. if (length > 1) { resultsLn = results.length; for (i = 0; i < resultsLn; i++) { cmp = results[i]; if (!dupMatcher[cmp.id]) { noDupResults.push(cmp); dupMatcher[cmp.id] = true; } } results = noDupResults; } return results; }, /** * Tests whether the passed Component matches the selector string. * @param {Ext.Component} component The Component to test * @param {String} selector The selector string to test against. * @return {Boolean} True if the Component matches the selector. * @member Ext.ComponentQuery */ is: function(component, selector) { if (!selector) { return true; } var selectors = selector.split(','), length = selectors.length, i = 0, query; for (; i < length; i++) { selector = Ext.String.trim(selectors[i]); query = this.cache[selector] || (this.cache[selector] = this.parse(selector)); if (query.is(component)) { return true; } } return false; }, parse: function(selector) { var operations = [], length = matchers.length, lastSelector, tokenMatch, matchedChar, modeMatch, selectorMatch, i, matcher, method; // We are going to parse the beginning of the selector over and // over again, slicing off the selector any portions we converted into an // operation, until it is an empty string. while (selector && lastSelector !== selector) { lastSelector = selector; // First we check if we are dealing with a token like #, * or an xtype tokenMatch = selector.match(tokenRe); if (tokenMatch) { matchedChar = tokenMatch[1]; // If the token is prefixed with a # we push a filterById operation to our stack if (matchedChar === '#') { operations.push({ method: filterById, args: [Ext.String.trim(tokenMatch[2])] }); } // If the token is prefixed with a . we push a filterByClassName operation to our stack // FIXME: Not enabled yet. just needs \. adding to the tokenRe prefix else if (matchedChar === '.') { operations.push({ method: filterByClassName, args: [Ext.String.trim(tokenMatch[2])] }); } // If the token is a * or an xtype string, we push a filterByXType // operation to the stack. else { operations.push({ method: filterByXType, args: [Ext.String.trim(tokenMatch[2]), Boolean(tokenMatch[3])] }); } // Now we slice of the part we just converted into an operation selector = selector.replace(tokenMatch[0], ''); } // If the next part of the query is not a space or > or ^, it means we // are going to check for more things that our current selection // has to comply to. while (!(modeMatch = selector.match(modeRe))) { // Lets loop over each type of matcher and execute it // on our current selector. for (i = 0; selector && i < length; i++) { matcher = matchers[i]; selectorMatch = selector.match(matcher.re); method = matcher.method; // If we have a match, add an operation with the method // associated with this matcher, and pass the regular // expression matches are arguments to the operation. if (selectorMatch) { operations.push({ method: Ext.isString(matcher.method) // Turn a string method into a function by formatting the string with our selector matche expression // A new method is created for different match expressions, eg {id=='textfield-1024'} // Every expression may be different in different selectors. ? Ext.functionFactory('items', Ext.String.format.apply(Ext.String, [method].concat(selectorMatch.slice(1)))) : matcher.method, args: selectorMatch.slice(1) }); selector = selector.replace(selectorMatch[0], ''); break; // Break on match } // Exhausted all matches: It's an error if (i === (length - 1)) { Ext.Error.raise('Invalid ComponentQuery selector: "' + arguments[0] + '"'); } } } // Now we are going to check for a mode change. This means a space // or a > to determine if we are going to select all the children // of the currently matched items, or a ^ if we are going to use the // ownerCt axis as the candidate source. if (modeMatch[1]) { // Assignment, and test for truthiness! operations.push({ mode: modeMatch[2]||modeMatch[1] }); selector = selector.replace(modeMatch[0], ''); } } // Now that we have all our operations in an array, we are going // to create a new Query using these operations. return new cq.Query({ operations: operations }); } }); }); /** * @class Ext.ComponentManager *

Provides a registry of all Components (instances of {@link Ext.Component} or any subclass * thereof) on a page so that they can be easily accessed by {@link Ext.Component component} * {@link Ext.Component#id id} (see {@link #get}, or the convenience method {@link Ext#getCmp Ext.getCmp}).

*

This object also provides a registry of available Component classes * indexed by a mnemonic code known as the Component's {@link Ext.Component#xtype xtype}. * The xtype provides a way to avoid instantiating child Components * when creating a full, nested config object for a complete Ext page.

*

A child Component may be specified simply as a config object * as long as the correct {@link Ext.Component#xtype xtype} is specified so that if and when the Component * needs rendering, the correct type can be looked up for lazy instantiation.

*

For a list of all available {@link Ext.Component#xtype xtypes}, see {@link Ext.Component}.

* @singleton */ Ext.define('Ext.ComponentManager', { extend: 'Ext.AbstractManager', alternateClassName: 'Ext.ComponentMgr', singleton: true, typeName: 'xtype', /** * Creates a new Component from the specified config object using the * config object's xtype to determine the class to instantiate. * @param {Object} config A configuration object for the Component you wish to create. * @param {String} defaultType (optional) The xtype to use if the config object does not * contain a xtype. (Optional if the config contains a xtype). * @return {Ext.Component} The newly instantiated Component. */ create: function(component, defaultType){ if (typeof component == 'string') { return Ext.widget(component); } if (component.isComponent) { return component; } return Ext.widget(component.xtype || defaultType, component); }, registerType: function(type, cls) { this.types[type] = cls; cls[this.typeName] = type; cls.prototype[this.typeName] = type; } }); /* * The dirty implementation in this class is quite naive. The reasoning for this is that the dirty state * will only be used in very specific circumstances, specifically, after the render process has begun but * the component is not yet rendered to the DOM. As such, we want it to perform as quickly as possible * so it's not as fully featured as you may expect. */ /** * Manages certain element-like data prior to rendering. These values are passed * on to the render process. This is currently used to manage the "class" and "style" attributes * of a component's primary el as well as the bodyEl of panels. This allows things like * addBodyCls in Panel to share logic with addCls in AbstractComponent. * @private */ Ext.define('Ext.util.ProtoElement', (function () { var splitWords = Ext.String.splitWords, toMap = Ext.Array.toMap; return { isProtoEl: true, /** * The property name for the className on the data object passed to {@link #writeTo}. */ clsProp: 'cls', /** * The property name for the style on the data object passed to {@link #writeTo}. */ styleProp: 'style', /** * The property name for the removed classes on the data object passed to {@link #writeTo}. */ removedProp: 'removed', /** * True if the style must be converted to text during {@link #writeTo}. When used to * populate tpl data, this will be true. When used to populate {@link Ext.DomHelper} * specs, this will be false (the default). */ styleIsText: false, constructor: function (config) { var me = this; Ext.apply(me, config); me.classList = splitWords(me.cls); me.classMap = toMap(me.classList); delete me.cls; if (Ext.isFunction(me.style)) { me.styleFn = me.style; delete me.style; } else if (typeof me.style == 'string') { me.style = Ext.Element.parseStyles(me.style); } else if (me.style) { me.style = Ext.apply({}, me.style); // don't edit the given object } }, /** * Indicates that the current state of the object has been flushed to the DOM, so we need * to track any subsequent changes */ flush: function(){ this.flushClassList = []; this.removedClasses = {}; // clear the style, it will be recreated if we add anything new delete this.style; }, /** * Adds class to the element. * @param {String} cls One or more classnames separated with spaces. * @return {Ext.util.ProtoElement} this */ addCls: function (cls) { var me = this, add = splitWords(cls), length = add.length, list = me.classList, map = me.classMap, flushList = me.flushClassList, i = 0, c; for (; i < length; ++i) { c = add[i]; if (!map[c]) { map[c] = true; list.push(c); if (flushList) { flushList.push(c); delete me.removedClasses[c]; } } } return me; }, /** * True if the element has given class. * @param {String} cls * @return {Boolean} */ hasCls: function (cls) { return cls in this.classMap; }, /** * Removes class from the element. * @param {String} cls One or more classnames separated with spaces. * @return {Ext.util.ProtoElement} this */ removeCls: function (cls) { var me = this, list = me.classList, newList = (me.classList = []), remove = toMap(splitWords(cls)), length = list.length, map = me.classMap, removedClasses = me.removedClasses, i, c; for (i = 0; i < length; ++i) { c = list[i]; if (remove[c]) { if (removedClasses) { if (map[c]) { removedClasses[c] = true; Ext.Array.remove(me.flushClassList, c); } } delete map[c]; } else { newList.push(c); } } return me; }, /** * Adds styles to the element. * @param {String/Object} prop The style property to be set, or an object of multiple styles. * @param {String} [value] The value to apply to the given property. * @return {Ext.util.ProtoElement} this */ setStyle: function (prop, value) { var me = this, style = me.style || (me.style = {}); if (typeof prop == 'string') { if (arguments.length === 1) { me.setStyle(Ext.Element.parseStyles(prop)); } else { style[prop] = value; } } else { Ext.apply(style, prop); } return me; }, /** * Writes style and class properties to given object. * Styles will be written to {@link #styleProp} and class names to {@link #clsProp}. * @param {Object} to * @return {Object} to */ writeTo: function (to) { var me = this, classList = me.flushClassList || me.classList, removedClasses = me.removedClasses, style; if (me.styleFn) { style = Ext.apply({}, me.styleFn()); Ext.apply(style, me.style); } else { style = me.style; } to[me.clsProp] = classList.join(' '); if (style) { to[me.styleProp] = me.styleIsText ? Ext.DomHelper.generateStyles(style) : style; } if (removedClasses) { removedClasses = Ext.Object.getKeys(removedClasses); if (removedClasses.length) { to[me.removedProp] = removedClasses.join(' '); } } return to; } }; }())); /** * This class is used as a mixin. * * This class is to be used to provide basic methods for binding/unbinding stores to other * classes. In general it will not be used directly. */ Ext.define('Ext.util.Bindable', { /** * Binds a store to this instance. * @param {Ext.data.AbstractStore/String} [store] The store to bind or ID of the store. * When no store given (or when `null` or `undefined` passed), unbinds the existing store. * @param {Boolean} [initial=false] True to not remove listeners from existing store. */ bindStore: function(store, initial){ var me = this, oldStore = me.store; if (!initial && me.store) { // Perform implementation-specific unbinding operations *before* possible Store destruction. me.onUnbindStore(oldStore, initial); if (store !== oldStore && oldStore.autoDestroy) { oldStore.destroyStore(); } else { me.unbindStoreListeners(oldStore); } } if (store) { store = Ext.data.StoreManager.lookup(store); me.bindStoreListeners(store); me.onBindStore(store, initial); } me.store = store || null; return me; }, /** * Gets the current store instance. * @return {Ext.data.AbstractStore} The store, null if one does not exist. */ getStore: function(){ return this.store; }, /** * Unbinds listeners from this component to the store. By default it will remove * anything bound by the bindStoreListeners method, however it can be overridden * in a subclass to provide any more complicated handling. * @protected * @param {Ext.data.AbstractStore} store The store to unbind from */ unbindStoreListeners: function(store) { // Can be overridden in the subclass for more complex removal var listeners = this.storeListeners; if (listeners) { store.un(listeners); } }, /** * Binds listeners for this component to the store. By default it will add * anything bound by the getStoreListeners method, however it can be overridden * in a subclass to provide any more complicated handling. * @protected * @param {Ext.data.AbstractStore} store The store to bind to */ bindStoreListeners: function(store) { // Can be overridden in the subclass for more complex binding var me = this, listeners = Ext.apply({}, me.getStoreListeners()); if (!listeners.scope) { listeners.scope = me; } me.storeListeners = listeners; store.on(listeners); }, /** * Gets the listeners to bind to a new store. * @protected * @return {Object} The listeners to be bound to the store in object literal form. The scope * may be omitted, it is assumed to be the current instance. */ getStoreListeners: Ext.emptyFn, /** * Template method, it is called when an existing store is unbound * from the current instance. * @protected * @param {Ext.data.AbstractStore} store The store being unbound * @param {Boolean} initial True if this store is being bound as initialization of the instance. */ onUnbindStore: Ext.emptyFn, /** * Template method, it is called when a new store is bound * to the current instance. * @protected * @param {Ext.data.AbstractStore} store The store being bound * @param {Boolean} initial True if this store is being bound as initialization of the instance. */ onBindStore: Ext.emptyFn }); /** * The subclasses of this class provide actions to perform upon {@link Ext.form.Basic Form}s. * * Instances of this class are only created by a {@link Ext.form.Basic Form} when the Form needs to perform an action * such as submit or load. The Configuration options listed for this class are set through the Form's action methods: * {@link Ext.form.Basic#submit submit}, {@link Ext.form.Basic#load load} and {@link Ext.form.Basic#doAction doAction} * * The instance of Action which performed the action is passed to the success and failure callbacks of the Form's action * methods ({@link Ext.form.Basic#submit submit}, {@link Ext.form.Basic#load load} and * {@link Ext.form.Basic#doAction doAction}), and to the {@link Ext.form.Basic#actioncomplete actioncomplete} and * {@link Ext.form.Basic#actionfailed actionfailed} event handlers. */ Ext.define('Ext.form.action.Action', { alternateClassName: 'Ext.form.Action', /** * @cfg {Ext.form.Basic} form * The {@link Ext.form.Basic BasicForm} instance that is invoking this Action. Required. */ /** * @cfg {String} url * The URL that the Action is to invoke. Will default to the {@link Ext.form.Basic#url url} configured on the * {@link #form}. */ /** * @cfg {Boolean} reset * When set to **true**, causes the Form to be {@link Ext.form.Basic#reset reset} on Action success. If specified, * this happens before the {@link #success} callback is called and before the Form's * {@link Ext.form.Basic#actioncomplete actioncomplete} event fires. */ /** * @cfg {String} method * The HTTP method to use to access the requested URL. * Defaults to the {@link Ext.form.Basic#method BasicForm's method}, or 'POST' if not specified. */ /** * @cfg {Object/String} params * Extra parameter values to pass. These are added to the Form's {@link Ext.form.Basic#baseParams} and passed to the * specified URL along with the Form's input fields. * * Parameters are encoded as standard HTTP parameters using {@link Ext#urlEncode Ext.Object.toQueryString}. */ /** * @cfg {Object} headers * Extra headers to be sent in the AJAX request for submit and load actions. * See {@link Ext.data.proxy.Ajax#headers}. */ /** * @cfg {Number} timeout * The number of seconds to wait for a server response before failing with the {@link #failureType} as * {@link Ext.form.action.Action#CONNECT_FAILURE}. If not specified, defaults to the configured * {@link Ext.form.Basic#timeout timeout} of the {@link #form}. */ /** * @cfg {Function} success * The function to call when a valid success return packet is received. * @cfg {Ext.form.Basic} success.form The form that requested the action * @cfg {Ext.form.action.Action} success.action The Action class. The {@link #result} property of this object may * be examined to perform custom postprocessing. */ /** * @cfg {Function} failure * The function to call when a failure packet was received, or when an error ocurred in the Ajax communication. * @cfg {Ext.form.Basic} failure.form The form that requested the action * @cfg {Ext.form.action.Action} failure.action The Action class. If an Ajax error ocurred, the failure type will * be in {@link #failureType}. The {@link #result} property of this object may be examined to perform custom * postprocessing. */ /** * @cfg {Object} scope * The scope in which to call the configured #success and #failure callback functions * (the `this` reference for the callback functions). */ /** * @cfg {String} waitMsg * The message to be displayed by a call to {@link Ext.window.MessageBox#wait} during the time the action is being * processed. */ /** * @cfg {String} waitTitle * The title to be displayed by a call to {@link Ext.window.MessageBox#wait} during the time the action is being * processed. */ /** * @cfg {Boolean} submitEmptyText * If set to true, the emptyText value will be sent with the form when it is submitted. */ submitEmptyText : true, /** * @property {String} type * The type of action this Action instance performs. Currently only "submit" and "load" are supported. */ /** * @property {String} failureType * The type of failure detected will be one of these: * {@link #CLIENT_INVALID}, {@link #SERVER_INVALID}, {@link #CONNECT_FAILURE}, or {@link #LOAD_FAILURE}. * * Usage: * * var fp = new Ext.form.Panel({ * ... * buttons: [{ * text: 'Save', * formBind: true, * handler: function(){ * if(fp.getForm().isValid()){ * fp.getForm().submit({ * url: 'form-submit.php', * waitMsg: 'Submitting your data...', * success: function(form, action){ * // server responded with success = true * var result = action.{@link #result}; * }, * failure: function(form, action){ * if (action.{@link #failureType} === Ext.form.action.Action.CONNECT_FAILURE) { * Ext.Msg.alert('Error', * 'Status:'+action.{@link #response}.status+': '+ * action.{@link #response}.statusText); * } * if (action.failureType === Ext.form.action.Action.SERVER_INVALID){ * // server responded with success = false * Ext.Msg.alert('Invalid', action.{@link #result}.errormsg); * } * } * }); * } * } * },{ * text: 'Reset', * handler: function(){ * fp.getForm().reset(); * } * }] */ /** * @property {Object} response * The raw XMLHttpRequest object used to perform the action. */ /** * @property {Object} result * The decoded response object containing a boolean `success` property and other, action-specific properties. */ /** * Creates new Action. * @param {Object} [config] Config object. */ constructor: function(config) { if (config) { Ext.apply(this, config); } // Normalize the params option to an Object var params = config.params; if (Ext.isString(params)) { this.params = Ext.Object.fromQueryString(params); } }, /** * @method * Invokes this action using the current configuration. */ run: Ext.emptyFn, /** * @private * @method onSuccess * Callback method that gets invoked when the action completes successfully. Must be implemented by subclasses. * @param {Object} response */ /** * @private * @method handleResponse * Handles the raw response and builds a result object from it. Must be implemented by subclasses. * @param {Object} response */ /** * @private * Handles a failure response. * @param {Object} response */ onFailure : function(response){ this.response = response; this.failureType = Ext.form.action.Action.CONNECT_FAILURE; this.form.afterAction(this, false); }, /** * @private * Validates that a response contains either responseText or responseXML and invokes * {@link #handleResponse} to build the result object. * @param {Object} response The raw response object. * @return {Object/Boolean} The result object as built by handleResponse, or `true` if * the response had empty responseText and responseXML. */ processResponse : function(response){ this.response = response; if (!response.responseText && !response.responseXML) { return true; } return (this.result = this.handleResponse(response)); }, /** * @private * Build the URL for the AJAX request. Used by the standard AJAX submit and load actions. * @return {String} The URL. */ getUrl: function() { return this.url || this.form.url; }, /** * @private * Determine the HTTP method to be used for the request. * @return {String} The HTTP method */ getMethod: function() { return (this.method || this.form.method || 'POST').toUpperCase(); }, /** * @private * Get the set of parameters specified in the BasicForm's baseParams and/or the params option. * Items in params override items of the same name in baseParams. * @return {Object} the full set of parameters */ getParams: function() { return Ext.apply({}, this.params, this.form.baseParams); }, /** * @private * Creates a callback object. */ createCallback: function() { var me = this, undef, form = me.form; return { success: me.onSuccess, failure: me.onFailure, scope: me, timeout: (this.timeout * 1000) || (form.timeout * 1000), upload: form.fileUpload ? me.onSuccess : undef }; }, statics: { /** * @property * Failure type returned when client side validation of the Form fails thus aborting a submit action. Client * side validation is performed unless {@link Ext.form.action.Submit#clientValidation} is explicitly set to * false. * @static */ CLIENT_INVALID: 'client', /** * @property * Failure type returned when server side processing fails and the {@link #result}'s `success` property is set to * false. * * In the case of a form submission, field-specific error messages may be returned in the {@link #result}'s * errors property. * @static */ SERVER_INVALID: 'server', /** * @property * Failure type returned when a communication error happens when attempting to send a request to the remote * server. The {@link #response} may be examined to provide further information. * @static */ CONNECT_FAILURE: 'connect', /** * @property * Failure type returned when the response's `success` property is set to false, or no field values are returned * in the response's data property. * @static */ LOAD_FAILURE: 'load' } }); /** * A subclass of Ext.dd.DragTracker which handles dragging any Component. * * This is configured with a Component to be made draggable, and a config object for the {@link Ext.dd.DragTracker} * class. * * A {@link #delegate} may be provided which may be either the element to use as the mousedown target or a {@link * Ext.DomQuery} selector to activate multiple mousedown targets. * * When the Component begins to be dragged, its `beginDrag` method will be called if implemented. * * When the drag ends, its `endDrag` method will be called if implemented. */ Ext.define('Ext.util.ComponentDragger', { extend: 'Ext.dd.DragTracker', /** * @cfg {Boolean} constrain * Specify as `true` to constrain the Component to within the bounds of the {@link #constrainTo} region. */ /** * @cfg {String/Ext.Element} delegate * A {@link Ext.DomQuery DomQuery} selector which identifies child elements within the Component's encapsulating * Element which are the drag handles. This limits dragging to only begin when the matching elements are * mousedowned. * * This may also be a specific child element within the Component's encapsulating element to use as the drag handle. */ /** * @cfg {Boolean} constrainDelegate * Specify as `true` to constrain the drag handles within the {@link #constrainTo} region. */ autoStart: 500, /** * Creates new ComponentDragger. * @param {Object} comp The Component to provide dragging for. * @param {Object} [config] Config object */ constructor: function(comp, config) { this.comp = comp; this.initialConstrainTo = config.constrainTo; this.callParent([ config ]); }, onStart: function(e) { var me = this, comp = me.comp; // Cache the start [X, Y] array this.startPosition = comp.el.getXY(); // If client Component has a ghost method to show a lightweight version of itself // then use that as a drag proxy unless configured to liveDrag. if (comp.ghost && !comp.liveDrag) { me.proxy = comp.ghost(); me.dragTarget = me.proxy.header.el; } // Set the constrainTo Region before we start dragging. if (me.constrain || me.constrainDelegate) { me.constrainTo = me.calculateConstrainRegion(); } if (comp.beginDrag) { comp.beginDrag(); } }, calculateConstrainRegion: function() { var me = this, comp = me.comp, c = me.initialConstrainTo, delegateRegion, elRegion, dragEl = me.proxy ? me.proxy.el : comp.el, shadowSize = (!me.constrainDelegate && dragEl.shadow && !dragEl.shadowDisabled) ? dragEl.shadow.getShadowSize() : 0; // The configured constrainTo might be a Region or an element if (!(c instanceof Ext.util.Region)) { c = Ext.fly(c).getViewRegion(); } // Reduce the constrain region to allow for shadow if (shadowSize) { c.adjust(shadowSize[0], -shadowSize[1], -shadowSize[2], shadowSize[3]); } // If they only want to constrain the *delegate* to within the constrain region, // adjust the region to be larger based on the insets of the delegate from the outer // edges of the Component. if (!me.constrainDelegate) { delegateRegion = Ext.fly(me.dragTarget).getRegion(); elRegion = dragEl.getRegion(); c.adjust( delegateRegion.top - elRegion.top, delegateRegion.right - elRegion.right, delegateRegion.bottom - elRegion.bottom, delegateRegion.left - elRegion.left ); } return c; }, // Move either the ghost Component or the target Component to its new position on drag onDrag: function(e) { var me = this, comp = (me.proxy && !me.comp.liveDrag) ? me.proxy : me.comp, offset = me.getOffset(me.constrain || me.constrainDelegate ? 'dragTarget' : null); comp.setPagePosition(me.startPosition[0] + offset[0], me.startPosition[1] + offset[1]); }, onEnd: function(e) { var comp = this.comp; if (this.proxy && !comp.liveDrag) { comp.unghost(); } if (comp.endDrag) { comp.endDrag(); } } }); /** * @singleton * @alternateClassName Ext.form.VTypes * * This is a singleton object which contains a set of commonly used field validation functions * and provides a mechanism for creating reusable custom field validations. * The following field validation functions are provided out of the box: * * - {@link #alpha} * - {@link #alphanum} * - {@link #email} * - {@link #url} * * VTypes can be applied to a {@link Ext.form.field.Text Text Field} using the `{@link Ext.form.field.Text#vtype vtype}` configuration: * * Ext.create('Ext.form.field.Text', { * fieldLabel: 'Email Address', * name: 'email', * vtype: 'email' // applies email validation rules to this field * }); * * To create custom VTypes: * * // custom Vtype for vtype:'time' * var timeTest = /^([1-9]|1[0-9]):([0-5][0-9])(\s[a|p]m)$/i; * Ext.apply(Ext.form.field.VTypes, { * // vtype validation function * time: function(val, field) { * return timeTest.test(val); * }, * // vtype Text property: The error text to display when the validation function returns false * timeText: 'Not a valid time. Must be in the format "12:34 PM".', * // vtype Mask property: The keystroke filter mask * timeMask: /[\d\s:amp]/i * }); * * In the above example the `time` function is the validator that will run when field validation occurs, * `timeText` is the error message, and `timeMask` limits what characters can be typed into the field. * Note that the `Text` and `Mask` functions must begin with the same name as the validator function. * * Using a custom validator is the same as using one of the build-in validators - just use the name of the validator function * as the `{@link Ext.form.field.Text#vtype vtype}` configuration on a {@link Ext.form.field.Text Text Field}: * * Ext.create('Ext.form.field.Text', { * fieldLabel: 'Departure Time', * name: 'departureTime', * vtype: 'time' // applies custom time validation rules to this field * }); * * Another example of a custom validator: * * // custom Vtype for vtype:'IPAddress' * Ext.apply(Ext.form.field.VTypes, { * IPAddress: function(v) { * return /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(v); * }, * IPAddressText: 'Must be a numeric IP address', * IPAddressMask: /[\d\.]/i * }); * * It's important to note that using {@link Ext#apply Ext.apply()} means that the custom validator function * as well as `Text` and `Mask` fields are added as properties of the `Ext.form.field.VTypes` singleton. */ Ext.define('Ext.form.field.VTypes', (function(){ // closure these in so they are only created once. var alpha = /^[a-zA-Z_]+$/, alphanum = /^[a-zA-Z0-9_]+$/, email = /^(\w+)([\-+.][\w]+)*@(\w[\-\w]*\.){1,5}([A-Za-z]){2,6}$/, url = /(((^https?)|(^ftp)):\/\/([\-\w]+\.)+\w{2,3}(\/[%\-\w]+(\.\w{2,})?)*(([\w\-\.\?\\\/+@&#;`~=%!]*)(\.\w{2,})?)*\/?)/i; // All these messages and functions are configurable return { singleton: true, alternateClassName: 'Ext.form.VTypes', /** * The function used to validate email addresses. Note that this is a very basic validation - complete * validation per the email RFC specifications is very complex and beyond the scope of this class, although this * function can be overridden if a more comprehensive validation scheme is desired. See the validation section * of the [Wikipedia article on email addresses][1] for additional information. This implementation is intended * to validate the following emails: * * - `barney@example.de` * - `barney.rubble@example.com` * - `barney-rubble@example.coop` * - `barney+rubble@example.com` * * [1]: http://en.wikipedia.org/wiki/E-mail_address * * @param {String} value The email address * @return {Boolean} true if the RegExp test passed, and false if not. */ 'email' : function(v){ return email.test(v); }, // /** * @property {String} emailText * The error text to display when the email validation function returns false. * Defaults to: 'This field should be an e-mail address in the format "user@example.com"' */ 'emailText' : 'This field should be an e-mail address in the format "user@example.com"', // /** * @property {RegExp} emailMask * The keystroke filter mask to be applied on email input. See the {@link #email} method for information about * more complex email validation. Defaults to: /[a-z0-9_\.\-@]/i */ 'emailMask' : /[a-z0-9_\.\-@\+]/i, /** * The function used to validate URLs * @param {String} value The URL * @return {Boolean} true if the RegExp test passed, and false if not. */ 'url' : function(v){ return url.test(v); }, // /** * @property {String} urlText * The error text to display when the url validation function returns false. * Defaults to: 'This field should be a URL in the format "http:/'+'/www.example.com"' */ 'urlText' : 'This field should be a URL in the format "http:/'+'/www.example.com"', // /** * The function used to validate alpha values * @param {String} value The value * @return {Boolean} true if the RegExp test passed, and false if not. */ 'alpha' : function(v){ return alpha.test(v); }, // /** * @property {String} alphaText * The error text to display when the alpha validation function returns false. * Defaults to: 'This field should only contain letters and _' */ 'alphaText' : 'This field should only contain letters and _', // /** * @property {RegExp} alphaMask * The keystroke filter mask to be applied on alpha input. Defaults to: /[a-z_]/i */ 'alphaMask' : /[a-z_]/i, /** * The function used to validate alphanumeric values * @param {String} value The value * @return {Boolean} true if the RegExp test passed, and false if not. */ 'alphanum' : function(v){ return alphanum.test(v); }, // /** * @property {String} alphanumText * The error text to display when the alphanumeric validation function returns false. * Defaults to: 'This field should only contain letters, numbers and _' */ 'alphanumText' : 'This field should only contain letters, numbers and _', // /** * @property {RegExp} alphanumMask * The keystroke filter mask to be applied on alphanumeric input. Defaults to: /[a-z0-9_]/i */ 'alphanumMask' : /[a-z0-9_]/i }; }())); /** * @class Ext.chart.Navigation * * Handles panning and zooming capabilities. * * Used as mixin by Ext.chart.Chart. */ Ext.define('Ext.chart.Navigation', { constructor: function() { this.originalStore = this.store; }, /** * Zooms the chart to the specified selection range. * Can be used with a selection mask. For example: * * items: { * xtype: 'chart', * animate: true, * store: store1, * mask: 'horizontal', * listeners: { * select: { * fn: function(me, selection) { * me.setZoom(selection); * me.mask.hide(); * } * } * } * } */ setZoom: function(zoomConfig) { var me = this, axes = me.axes, axesItems = axes.items, i, ln, axis, bbox = me.chartBBox, xScale = 1 / bbox.width, yScale = 1 / bbox.height, zoomer = { x : zoomConfig.x * xScale, y : zoomConfig.y * yScale, width : zoomConfig.width * xScale, height : zoomConfig.height * yScale }, ends, from, to; for (i = 0, ln = axesItems.length; i < ln; i++) { axis = axesItems[i]; ends = axis.calcEnds(); if (axis.position == 'bottom' || axis.position == 'top') { from = (ends.to - ends.from) * zoomer.x + ends.from; to = (ends.to - ends.from) * zoomer.width + from; axis.minimum = from; axis.maximum = to; } else { to = (ends.to - ends.from) * (1 - zoomer.y) + ends.from; from = to - (ends.to - ends.from) * zoomer.height; axis.minimum = from; axis.maximum = to; } } me.redraw(false); }, /** * Restores the zoom to the original value. This can be used to reset * the previous zoom state set by `setZoom`. For example: * * myChart.restoreZoom(); */ restoreZoom: function() { if (this.originalStore) { this.store = this.substore = this.originalStore; this.redraw(true); } } }); /** * @class Ext.state.Provider *

Abstract base class for state provider implementations. The provider is responsible * for setting values and extracting values to/from the underlying storage source. The * storage source can vary and the details should be implemented in a subclass. For example * a provider could use a server side database or the browser localstorage where supported.

* *

This class provides methods for encoding and decoding typed variables including * dates and defines the Provider interface. By default these methods put the value and the * type information into a delimited string that can be stored. These should be overridden in * a subclass if you want to change the format of the encoded value and subsequent decoding.

*/ Ext.define('Ext.state.Provider', { mixins: { observable: 'Ext.util.Observable' }, /** * @cfg {String} prefix A string to prefix to items stored in the underlying state store. * Defaults to 'ext-' */ prefix: 'ext-', constructor : function(config){ config = config || {}; var me = this; Ext.apply(me, config); /** * @event statechange * Fires when a state change occurs. * @param {Ext.state.Provider} this This state provider * @param {String} key The state key which was changed * @param {String} value The encoded value for the state */ me.addEvents("statechange"); me.state = {}; me.mixins.observable.constructor.call(me); }, /** * Returns the current value for a key * @param {String} name The key name * @param {Object} defaultValue A default value to return if the key's value is not found * @return {Object} The state data */ get : function(name, defaultValue){ return typeof this.state[name] == "undefined" ? defaultValue : this.state[name]; }, /** * Clears a value from the state * @param {String} name The key name */ clear : function(name){ var me = this; delete me.state[name]; me.fireEvent("statechange", me, name, null); }, /** * Sets the value for a key * @param {String} name The key name * @param {Object} value The value to set */ set : function(name, value){ var me = this; me.state[name] = value; me.fireEvent("statechange", me, name, value); }, /** * Decodes a string previously encoded with {@link #encodeValue}. * @param {String} value The value to decode * @return {Object} The decoded value */ decodeValue : function(value){ // a -> Array // n -> Number // d -> Date // b -> Boolean // s -> String // o -> Object // -> Empty (null) var me = this, re = /^(a|n|d|b|s|o|e)\:(.*)$/, matches = re.exec(unescape(value)), all, type, keyValue, values, vLen, v; if(!matches || !matches[1]){ return; // non state } type = matches[1]; value = matches[2]; switch (type) { case 'e': return null; case 'n': return parseFloat(value); case 'd': return new Date(Date.parse(value)); case 'b': return (value == '1'); case 'a': all = []; if(value != ''){ values = value.split('^'); vLen = values.length; for (v = 0; v < vLen; v++) { value = values[v]; all.push(me.decodeValue(value)); } } return all; case 'o': all = {}; if(value != ''){ values = value.split('^'); vLen = values.length; for (v = 0; v < vLen; v++) { value = values[v]; keyValue = value.split('='); all[keyValue[0]] = me.decodeValue(keyValue[1]); } } return all; default: return value; } }, /** * Encodes a value including type information. Decode with {@link #decodeValue}. * @param {Object} value The value to encode * @return {String} The encoded value */ encodeValue : function(value){ var flat = '', i = 0, enc, len, key; if (value == null) { return 'e:1'; } else if(typeof value == 'number') { enc = 'n:' + value; } else if(typeof value == 'boolean') { enc = 'b:' + (value ? '1' : '0'); } else if(Ext.isDate(value)) { enc = 'd:' + value.toGMTString(); } else if(Ext.isArray(value)) { for (len = value.length; i < len; i++) { flat += this.encodeValue(value[i]); if (i != len - 1) { flat += '^'; } } enc = 'a:' + flat; } else if (typeof value == 'object') { for (key in value) { if (typeof value[key] != 'function' && value[key] !== undefined) { flat += key + '=' + this.encodeValue(value[key]) + '^'; } } enc = 'o:' + flat.substring(0, flat.length-1); } else { enc = 's:' + value; } return escape(enc); } }); /** * @docauthor Jason Johnston * * This mixin provides a common interface for the logical behavior and state of form fields, including: * * - Getter and setter methods for field values * - Events and methods for tracking value and validity changes * - Methods for triggering validation * * **NOTE**: When implementing custom fields, it is most likely that you will want to extend the {@link Ext.form.field.Base} * component class rather than using this mixin directly, as BaseField contains additional logic for generating an * actual DOM complete with {@link Ext.form.Labelable label and error message} display and a form input field, * plus methods that bind the Field value getters and setters to the input field's value. * * If you do want to implement this mixin directly and don't want to extend {@link Ext.form.field.Base}, then * you will most likely want to override the following methods with custom implementations: {@link #getValue}, * {@link #setValue}, and {@link #getErrors}. Other methods may be overridden as needed but their base * implementations should be sufficient for common cases. You will also need to make sure that {@link #initField} * is called during the component's initialization. */ Ext.define('Ext.form.field.Field', { /** * @property {Boolean} isFormField * Flag denoting that this component is a Field. Always true. */ isFormField : true, /** * @cfg {Object} value * A value to initialize this field with. */ /** * @cfg {String} name * The name of the field. By default this is used as the parameter name when including the * {@link #getSubmitData field value} in a {@link Ext.form.Basic#submit form submit()}. To prevent the field from * being included in the form submit, set {@link #submitValue} to false. */ /** * @cfg {Boolean} disabled * True to disable the field. Disabled Fields will not be {@link Ext.form.Basic#submit submitted}. */ disabled : false, /** * @cfg {Boolean} submitValue * Setting this to false will prevent the field from being {@link Ext.form.Basic#submit submitted} even when it is * not disabled. */ submitValue: true, /** * @cfg {Boolean} validateOnChange * Specifies whether this field should be validated immediately whenever a change in its value is detected. * If the validation results in a change in the field's validity, a {@link #validitychange} event will be * fired. This allows the field to show feedback about the validity of its contents immediately as the user is * typing. * * When set to false, feedback will not be immediate. However the form will still be validated before submitting if * the clientValidation option to {@link Ext.form.Basic#doAction} is enabled, or if the field or form are validated * manually. * * See also {@link Ext.form.field.Base#checkChangeEvents} for controlling how changes to the field's value are * detected. */ validateOnChange: true, /** * @private */ suspendCheckChange: 0, /** * Initializes this Field mixin on the current instance. Components using this mixin should call this method during * their own initialization process. */ initField: function() { this.addEvents( /** * @event change * Fires when the value of a field is changed via the {@link #setValue} method. * @param {Ext.form.field.Field} this * @param {Object} newValue The new value * @param {Object} oldValue The original value */ 'change', /** * @event validitychange * Fires when a change in the field's validity is detected. * @param {Ext.form.field.Field} this * @param {Boolean} isValid Whether or not the field is now valid */ 'validitychange', /** * @event dirtychange * Fires when a change in the field's {@link #isDirty} state is detected. * @param {Ext.form.field.Field} this * @param {Boolean} isDirty Whether or not the field is now dirty */ 'dirtychange' ); this.initValue(); }, /** * Initializes the field's value based on the initial config. */ initValue: function() { var me = this; me.value = me.transformOriginalValue(me.value); /** * @property {Object} originalValue * The original value of the field as configured in the {@link #value} configuration, or as loaded by the last * form load operation if the form's {@link Ext.form.Basic#trackResetOnLoad trackResetOnLoad} setting is `true`. */ me.originalValue = me.lastValue = me.value; // Set the initial value - prevent validation on initial set me.suspendCheckChange++; me.setValue(me.value); me.suspendCheckChange--; }, /** * Allows for any necessary modifications before the original * value is set * @protected * @param {Object} value The initial value * @return {Object} The modified initial value */ transformOriginalValue: function(value){ return value; }, /** * Returns the {@link Ext.form.field.Field#name name} attribute of the field. This is used as the parameter name * when including the field value in a {@link Ext.form.Basic#submit form submit()}. * @return {String} name The field {@link Ext.form.field.Field#name name} */ getName: function() { return this.name; }, /** * Returns the current data value of the field. The type of value returned is particular to the type of the * particular field (e.g. a Date object for {@link Ext.form.field.Date}). * @return {Object} value The field value */ getValue: function() { return this.value; }, /** * Sets a data value into the field and runs the change detection and validation. * @param {Object} value The value to set * @return {Ext.form.field.Field} this */ setValue: function(value) { var me = this; me.value = value; me.checkChange(); return me; }, /** * Returns whether two field {@link #getValue values} are logically equal. Field implementations may override this * to provide custom comparison logic appropriate for the particular field's data type. * @param {Object} value1 The first value to compare * @param {Object} value2 The second value to compare * @return {Boolean} True if the values are equal, false if inequal. */ isEqual: function(value1, value2) { return String(value1) === String(value2); }, /** * Returns whether two values are logically equal. * Similar to {@link #isEqual}, however null or undefined values will be treated as empty strings. * @private * @param {Object} value1 The first value to compare * @param {Object} value2 The second value to compare * @return {Boolean} True if the values are equal, false if inequal. */ isEqualAsString: function(value1, value2){ return String(Ext.value(value1, '')) === String(Ext.value(value2, '')); }, /** * Returns the parameter(s) that would be included in a standard form submit for this field. Typically this will be * an object with a single name-value pair, the name being this field's {@link #getName name} and the value being * its current stringified value. More advanced field implementations may return more than one name-value pair. * * Note that the values returned from this method are not guaranteed to have been successfully {@link #validate * validated}. * * @return {Object} A mapping of submit parameter names to values; each value should be a string, or an array of * strings if that particular name has multiple values. It can also return null if there are no parameters to be * submitted. */ getSubmitData: function() { var me = this, data = null; if (!me.disabled && me.submitValue && !me.isFileUpload()) { data = {}; data[me.getName()] = '' + me.getValue(); } return data; }, /** * Returns the value(s) that should be saved to the {@link Ext.data.Model} instance for this field, when {@link * Ext.form.Basic#updateRecord} is called. Typically this will be an object with a single name-value pair, the name * being this field's {@link #getName name} and the value being its current data value. More advanced field * implementations may return more than one name-value pair. The returned values will be saved to the corresponding * field names in the Model. * * Note that the values returned from this method are not guaranteed to have been successfully {@link #validate * validated}. * * @return {Object} A mapping of submit parameter names to values; each value should be a string, or an array of * strings if that particular name has multiple values. It can also return null if there are no parameters to be * submitted. */ getModelData: function() { var me = this, data = null; if (!me.disabled && !me.isFileUpload()) { data = {}; data[me.getName()] = me.getValue(); } return data; }, /** * Resets the current field value to the originally loaded value and clears any validation messages. See {@link * Ext.form.Basic}.{@link Ext.form.Basic#trackResetOnLoad trackResetOnLoad} */ reset : function(){ var me = this; me.beforeReset(); me.setValue(me.originalValue); me.clearInvalid(); // delete here so we reset back to the original state delete me.wasValid; }, /** * Template method before a field is reset. * @protected */ beforeReset: Ext.emptyFn, /** * Resets the field's {@link #originalValue} property so it matches the current {@link #getValue value}. This is * called by {@link Ext.form.Basic}.{@link Ext.form.Basic#setValues setValues} if the form's * {@link Ext.form.Basic#trackResetOnLoad trackResetOnLoad} property is set to true. */ resetOriginalValue: function() { this.originalValue = this.getValue(); this.checkDirty(); }, /** * Checks whether the value of the field has changed since the last time it was checked. * If the value has changed, it: * * 1. Fires the {@link #change change event}, * 2. Performs validation if the {@link #validateOnChange} config is enabled, firing the * {@link #validitychange validitychange event} if the validity has changed, and * 3. Checks the {@link #isDirty dirty state} of the field and fires the {@link #dirtychange dirtychange event} * if it has changed. */ checkChange: function() { if (!this.suspendCheckChange) { var me = this, newVal = me.getValue(), oldVal = me.lastValue; if (!me.isEqual(newVal, oldVal) && !me.isDestroyed) { me.lastValue = newVal; me.fireEvent('change', me, newVal, oldVal); me.onChange(newVal, oldVal); } } }, /** * @private * Called when the field's value changes. Performs validation if the {@link #validateOnChange} * config is enabled, and invokes the dirty check. */ onChange: function(newVal, oldVal) { if (this.validateOnChange) { this.validate(); } this.checkDirty(); }, /** * Returns true if the value of this Field has been changed from its {@link #originalValue}. * Will always return false if the field is disabled. * * Note that if the owning {@link Ext.form.Basic form} was configured with * {@link Ext.form.Basic#trackResetOnLoad trackResetOnLoad} then the {@link #originalValue} is updated when * the values are loaded by {@link Ext.form.Basic}.{@link Ext.form.Basic#setValues setValues}. * @return {Boolean} True if this field has been changed from its original value (and is not disabled), * false otherwise. */ isDirty : function() { var me = this; return !me.disabled && !me.isEqual(me.getValue(), me.originalValue); }, /** * Checks the {@link #isDirty} state of the field and if it has changed since the last time it was checked, * fires the {@link #dirtychange} event. */ checkDirty: function() { var me = this, isDirty = me.isDirty(); if (isDirty !== me.wasDirty) { me.fireEvent('dirtychange', me, isDirty); me.onDirtyChange(isDirty); me.wasDirty = isDirty; } }, /** * @private Called when the field's dirty state changes. * @param {Boolean} isDirty */ onDirtyChange: Ext.emptyFn, /** * Runs this field's validators and returns an array of error messages for any validation failures. This is called * internally during validation and would not usually need to be used manually. * * Each subclass should override or augment the return value to provide their own errors. * * @param {Object} value The value to get errors for (defaults to the current field value) * @return {String[]} All error messages for this field; an empty Array if none. */ getErrors: function(value) { return []; }, /** * Returns whether or not the field value is currently valid by {@link #getErrors validating} the field's current * value. The {@link #validitychange} event will not be fired; use {@link #validate} instead if you want the event * to fire. **Note**: {@link #disabled} fields are always treated as valid. * * Implementations are encouraged to ensure that this method does not have side-effects such as triggering error * message display. * * @return {Boolean} True if the value is valid, else false */ isValid : function() { var me = this; return me.disabled || Ext.isEmpty(me.getErrors()); }, /** * Returns whether or not the field value is currently valid by {@link #getErrors validating} the field's current * value, and fires the {@link #validitychange} event if the field's validity has changed since the last validation. * **Note**: {@link #disabled} fields are always treated as valid. * * Custom implementations of this method are allowed to have side-effects such as triggering error message display. * To validate without side-effects, use {@link #isValid}. * * @return {Boolean} True if the value is valid, else false */ validate : function() { var me = this, isValid = me.isValid(); if (isValid !== me.wasValid) { me.wasValid = isValid; me.fireEvent('validitychange', me, isValid); } return isValid; }, /** * A utility for grouping a set of modifications which may trigger value changes into a single transaction, to * prevent excessive firing of {@link #change} events. This is useful for instance if the field has sub-fields which * are being updated as a group; you don't want the container field to check its own changed state for each subfield * change. * @param {Object} fn A function containing the transaction code */ batchChanges: function(fn) { try { this.suspendCheckChange++; fn(); } catch(e){ throw e; } finally { this.suspendCheckChange--; } this.checkChange(); }, /** * Returns whether this Field is a file upload field; if it returns true, forms will use special techniques for * {@link Ext.form.Basic#submit submitting the form} via AJAX. See {@link Ext.form.Basic#hasUpload} for details. If * this returns true, the {@link #extractFileInput} method must also be implemented to return the corresponding file * input element. * @return {Boolean} */ isFileUpload: function() { return false; }, /** * Only relevant if the instance's {@link #isFileUpload} method returns true. Returns a reference to the file input * DOM element holding the user's selected file. The input will be appended into the submission form and will not be * returned, so this method should also create a replacement. * @return {HTMLElement} */ extractFileInput: function() { return null; }, /** * @method markInvalid * Associate one or more error messages with this field. Components using this mixin should implement this method to * update the component's rendering to display the messages. * * **Note**: this method does not cause the Field's {@link #validate} or {@link #isValid} methods to return `false` * if the value does _pass_ validation. So simply marking a Field as invalid will not prevent submission of forms * submitted with the {@link Ext.form.action.Submit#clientValidation} option set. * * @param {String/String[]} errors The error message(s) for the field. */ markInvalid: Ext.emptyFn, /** * @method clearInvalid * Clear any invalid styles/messages for this field. Components using this mixin should implement this method to * update the components rendering to clear any existing messages. * * **Note**: this method does not cause the Field's {@link #validate} or {@link #isValid} methods to return `true` * if the value does not _pass_ validation. So simply clearing a field's errors will not necessarily allow * submission of forms submitted with the {@link Ext.form.action.Submit#clientValidation} option set. */ clearInvalid: Ext.emptyFn }); /** * 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.LockingView', { mixins: { observable: 'Ext.util.Observable' }, eventRelayRe: /^(beforeitem|beforecontainer|item|container|cell)/, constructor: function(config){ var me = this, eventNames = [], eventRe = me.eventRelayRe, locked = config.locked.getView(), normal = config.normal.getView(), events, event; Ext.apply(me, { lockedView: locked, normalView: normal, lockedGrid: config.locked, normalGrid: config.normal, panel: config.panel }); me.mixins.observable.constructor.call(me, config); // relay events events = locked.events; for (event in events) { if (events.hasOwnProperty(event) && eventRe.test(event)) { eventNames.push(event); } } me.relayEvents(locked, eventNames); me.relayEvents(normal, eventNames); normal.on({ scope: me, itemmouseleave: me.onItemMouseLeave, itemmouseenter: me.onItemMouseEnter }); locked.on({ scope: me, itemmouseleave: me.onItemMouseLeave, itemmouseenter: me.onItemMouseEnter }); }, getGridColumns: function() { var cols = this.lockedGrid.headerCt.getGridColumns(); return cols.concat(this.normalGrid.headerCt.getGridColumns()); }, getEl: function(column){ return this.getViewForColumn(column).getEl(); }, 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 view = this.lockedView; view[name].apply(view, args || []); view = this.normalView; view[name].apply(view, args || []); }, getSelectionModel: function(){ return this.panel.getSelectionModel(); }, getStore: function(){ return this.panel.store; }, getNode: function(nodeInfo){ // default to the normal view return this.normalView.getNode(nodeInfo); }, getCell: function(record, column){ var view = this.getViewForColumn(column), row; row = view.getNode(record); return Ext.fly(row).down(column.getCellSelector()); }, getRecord: function(node){ var result = this.lockedView.getRecord(node); if (!node) { result = this.normalView.getRecord(node); } return result; }, addElListener: function(eventName, fn, scope){ this.relayFn('addElListener', arguments); }, refreshNode: function(){ this.relayFn('refreshNode', arguments); }, refresh: function(){ this.relayFn('refresh', arguments); }, bindStore: function(){ this.relayFn('bindStore', arguments); }, addRowCls: function(){ this.relayFn('addRowCls', arguments); }, removeRowCls: function(){ this.relayFn('removeRowCls', arguments); } }); /** * 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] * }); * * @abstract */ Ext.define('Ext.grid.feature.Feature', { extend: 'Ext.util.Observable', alias: 'feature.feature', /* * @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, /** * Most features will not modify the data returned to the view. * This is limited to one feature that manipulates the data per grid view. */ collectData: false, constructor: function(config) { this.initialConfig = config; this.callParent(arguments); }, clone: function() { return new this.self(this.initialConfig); }, init: Ext.emptyFn, getFeatureTpl: function() { return ''; }, /** * 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]; }, /** * Approriate place to attach events to the view, selectionmodel, headerCt, etc * @template */ attachEvents: function() { }, getFragmentTpl: Ext.emptyFn, /** * Allows a feature to mutate the metaRowTpl. * The array received as a single argument can be manipulated to add things * on the end/begining of a particular row. * @param {Array} metaRowTplArray A String array to be used constructing an {@link Ext.XTemplate XTemplate} * to render the rows. This Array may be changed to provide extra DOM structure. * @template */ mutateMetaRowTpl: Ext.emptyFn, /** * Allows a feature to inject member methods into the metaRowTpl. This is * important for embedding functionality which will become part of the proper * row tpl. * @template */ getMetaRowTplFragments: function() { return {}; }, getTableFragments: function() { return {}; }, /** * Provide additional data to the prepareData call within the grid view. * @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. * @template */ getAdditionalData: function(data, idx, record, orig) { return {}; }, /** * 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', { /* Begin Definitions */ extend: 'Ext.grid.feature.Feature', alias: 'feature.abstractsummary', /* End Definitions */ /** * @cfg * True to show the summary row. */ showSummaryRow: true, // @private nestedIdRe: /\{\{id\}([\w\-]*)\}/g, // Listen for store updates. Eg, from an Editor. init: function() { var me = this; // Summary rows must be kept in column order, so view must be refreshed on column move me.grid.optimizedColumnMove = false; me.view.mon(me.view.store, { update: me.onStoreUpdate, scope: me }); }, // Refresh the whole view on edit so that the Summary gets updated onStoreUpdate: function() { var v = this.view; if (this.showSummaryRow) { v.saveScrollState(); v.refresh(); v.restoreScrollState(); } }, /** * Toggle whether or not to show the summary row. * @param {Boolean} visible True to show the summary row */ toggleSummaryRow: function(visible){ this.showSummaryRow = !!visible; }, /** * Gets any fragments to be used in the tpl * @private * @return {Object} The fragments */ getSummaryFragments: function(){ var fragments = {}; if (this.showSummaryRow) { Ext.apply(fragments, { printSummaryRow: Ext.bind(this.printSummaryRow, this) }); } return fragments; }, /** * Prints a summary row * @private * @param {Object} index The index in the template * @return {String} The value of the summary row */ printSummaryRow: function(index){ var inner = this.view.getTableChunker().metaRowTpl.join(''), prefix = Ext.baseCSSPrefix; inner = inner.replace(prefix + 'grid-row', prefix + 'grid-row-summary'); inner = inner.replace('{{id}}', '{gridSummaryValue}'); inner = inner.replace(this.nestedIdRe, '{id$1}'); inner = inner.replace('{[this.embedRowCls()]}', '{rowCls}'); inner = inner.replace('{[this.embedRowAttr()]}', '{rowAttr}'); inner = new Ext.XTemplate(inner, { firstOrLastCls: Ext.view.TableChunker.firstOrLastCls }); return inner.applyTemplate({ columns: this.getPrintData(index) }); }, /** * Gets the value for the column from the attached data. * @param {Ext.grid.column.Column} column The header * @param {Object} data The current data * @return {String} The value to be rendered */ getColumnValue: function(column, summaryData){ var comp = Ext.getCmp(column.id), value = summaryData[column.id], renderer = comp.summaryRenderer; if (!value && value !== 0) { value = '\u00a0'; } if (renderer) { value = renderer.call( comp.scope || this, value, summaryData, column.dataIndex ); } return value; }, /** * 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. */ getSummary: function(store, type, field, group){ if (type) { if (Ext.isFunction(type)) { return store.aggregate(type, null, group); } switch (type) { case 'count': return store.count(group); case 'min': return store.min(field, group); case 'max': return store.max(field, group); case 'sum': return store.sum(field, group); case 'average': return store.average(field, group); default: return group ? {} : ''; } } } }); /** * */ Ext.define('Ext.grid.feature.Chunking', { extend: 'Ext.grid.feature.Feature', alias: 'feature.chunking', chunkSize: 20, rowHeight: Ext.isIE ? 27 : 26, visibleChunk: 0, hasFeatureEvent: false, attachEvents: function() { this.view.el.on('scroll', this.onBodyScroll, this, {buffer: 300}); }, onBodyScroll: function(e, t) { var view = this.view, top = t.scrollTop, nextChunk = Math.floor(top / this.rowHeight / this.chunkSize); if (nextChunk !== this.visibleChunk) { this.visibleChunk = nextChunk; view.refresh(); view.el.dom.scrollTop = top; //BrowserBug: IE6,7,8 quirks mode takes setting scrollTop 2x. view.el.dom.scrollTop = top; } }, collectData: function(records, preppedRecords, startIndex, fullWidth, o) { //headerCt = this.view.headerCt, //colums = headerCt.getColumnsForTpl(), var me = this, recordCount = o.rows.length, start = 0, i = 0, visibleChunk = me.visibleChunk, rows, chunkLength, origRows = o.rows; delete o.rows; o.chunks = []; for (; start < recordCount; start += me.chunkSize, i++) { if (start + me.chunkSize > recordCount) { chunkLength = recordCount - start; } else { chunkLength = me.chunkSize; } if (i >= visibleChunk - 1 && i <= visibleChunk + 1) { rows = origRows.slice(start, start + me.chunkSize); } else { rows = []; } o.chunks.push({ rows: rows, fullWidth: fullWidth, chunkHeight: chunkLength * me.rowHeight }); } return o; }, getTableFragments: function() { return { openTableWrap: function() { return '
'; }, closeTableWrap: function() { return '
'; } }; } }); /** * This feature allows to display the grid rows aggregated into groups as specified by the {@link Ext.data.Store#groupers} * specified on the Store. The group will show the title for the group name and then the appropriate records for the group * 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. * * ## Example Usage * * @example * var store = Ext.create('Ext.data.Store', { * storeId:'employeeStore', * fields:['name', 'seniority', 'department'], * groupField: 'department', * data: {'employees':[ * { "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" } * ]}, * proxy: { * type: 'memory', * reader: { * type: 'json', * root: 'employees' * } * } * }); * * Ext.create('Ext.grid.Panel', { * title: 'Employees', * store: Ext.data.StoreManager.lookup('employeeStore'), * 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. * * @author Nicolas Ferrero */ Ext.define('Ext.grid.feature.Grouping', { extend: 'Ext.grid.feature.Feature', alias: 'feature.grouping', eventPrefix: 'group', eventSelector: '.' + Ext.baseCSSPrefix + 'grid-group-hd', bodySelector: '.' + Ext.baseCSSPrefix + 'grid-group-body', constructor: function() { var me = this; me.collapsedState = {}; me.callParent(arguments); }, /** * @event groupclick * @param {Ext.view.Table} view * @param {HTMLElement} node * @param {String} group The name of the group * @param {Ext.EventObject} e */ /** * @event groupdblclick * @param {Ext.view.Table} view * @param {HTMLElement} node * @param {String} group The name of the group * @param {Ext.EventObject} e */ /** * @event groupcontextmenu * @param {Ext.view.Table} view * @param {HTMLElement} node * @param {String} group The name of the group * @param {Ext.EventObject} 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 {Object[]} groupHeaderTpl.rows An array of child row data objects as returned by the View's {@link Ext.view.AbstractView#prepareData prepareData} method. * @cfg {Ext.data.Model[]} groupHeaderTpl.children An array containing the child records for the group being rendered. */ 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', hdCollapsibleCls: Ext.baseCSSPrefix + 'grid-group-hd-collapsible', // /** * @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 `falsee` to disable collapsing groups from the UI. * * This is set to `false` when the associated {@link Ext.data.Store store} is * {@link Ext.data.Store#buffered buffered}. */ collapsible: true, enable: function() { var me = this, view = me.view, store = view.store, groupToggleMenuItem; me.lastGroupField = me.getGroupField(); if (me.lastGroupIndex) { me.block(); store.group(me.lastGroupIndex); me.unblock(); } me.callParent(); groupToggleMenuItem = me.view.headerCt.getMenu().down('#groupToggleMenuItem'); groupToggleMenuItem.setChecked(true, true); me.refreshIf(); }, disable: function() { var me = this, view = me.view, store = view.store, remote = store.remoteGroup, groupToggleMenuItem, lastGroup; lastGroup = store.groupers.first(); if (lastGroup) { me.lastGroupIndex = lastGroup.property; me.block(); store.clearGrouping(); me.unblock(); } me.callParent(); groupToggleMenuItem = me.view.headerCt.getMenu().down('#groupToggleMenuItem'); groupToggleMenuItem.setChecked(true, true); groupToggleMenuItem.setChecked(false, true); me.refreshIf(); }, refreshIf: function() { var ownerCt = this.grid.ownerCt, view = this.view; if (!view.store.remoteGroup && !this.blockRefresh) { // We are one side of a lockable grid, so refresh the locking view if (ownerCt && ownerCt.lockable) { ownerCt.view.refresh(); } else { view.refresh(); } } }, getFeatureTpl: function(values, parent, x, xcount) { return [ '', // group row tpl '
{collapsed}{[this.renderGroupHeaderTpl(values, parent)]}
', // this is the rowbody '{[this.recurse(values)]}', '
' ].join(''); }, getFragmentTpl: function() { var me = this; return { indentByDepth: me.indentByDepth, depthToIndent: me.depthToIndent, renderGroupHeaderTpl: function(values, parent) { return Ext.XTemplate.getTpl(me, 'groupHeaderTpl').apply(values, parent); } }; }, indentByDepth: function(values) { return 'style="padding-left:'+ ((values.depth || 0) * this.depthToIndent) + 'px;"'; }, // Containers holding these components are responsible for // destroying them, we are just deleting references. destroy: function() { delete this.view; delete this.prunedHeader; }, // perhaps rename to afterViewRender attachEvents: function() { var me = this, view = me.view; view.on({ scope: me, groupclick: me.onGroupClick, rowfocus: me.onRowFocus }); view.mon(view.store, { scope: me, groupchange: me.onGroupChange, remove: me.onRemove, add: me.onAdd, update: me.onUpdate }); if (me.enableGroupingMenu) { me.injectGroupingMenu(); } me.pruneGroupedHeader(); me.lastGroupField = me.getGroupField(); me.block(); me.onGroupChange(); me.unblock(); }, // If we add a new item that doesn't belong to a rendered group, refresh the view onAdd: function(store, records){ var me = this, view = me.view, groupField = me.getGroupField(), i = 0, len = records.length, activeGroups, addedGroups, groups, needsRefresh, group; if (view.rendered) { addedGroups = {}; activeGroups = {}; for (; i < len; ++i) { group = records[i].get(groupField); if (addedGroups[group] === undefined) { addedGroups[group] = 0; } addedGroups[group] += 1; } groups = store.getGroups(); for (i = 0, len = groups.length; i < len; ++i) { group = groups[i]; activeGroups[group.name] = group.children.length; } for (group in addedGroups) { if (addedGroups[group] === activeGroups[group]) { needsRefresh = true; break; } } if (needsRefresh) { view.refresh(); } } }, onUpdate: function(store, record, type, changedFields){ var view = this.view; if (view.rendered && !changedFields || Ext.Array.contains(changedFields, this.getGroupField())) { view.refresh(); } }, onRemove: function(store, record) { var me = this, groupField = me.getGroupField(), removedGroup = record.get(groupField), view = me.view; if (view.rendered) { // If that was the last one in the group, force a refresh if (store.findExact(groupField, removedGroup) === -1) { me.view.refresh(); } } }, injectGroupingMenu: function() { var me = this, headerCt = me.view.headerCt; headerCt.showMenuBy = me.showMenuBy; headerCt.getMenuItems = me.getMenuItems(); }, showMenuBy: function(t, header) { var menu = this.getMenu(), groupMenuItem = menu.down('#groupMenuItem'), groupableMth = header.groupable === false ? 'disable' : 'enable'; groupMenuItem[groupableMth](); Ext.grid.header.Container.prototype.showMenuBy.apply(this, 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 = view.store; delete me.lastGroupIndex; me.block(); me.enable(); store.group(hdr.dataIndex); me.pruneGroupedHeader(); me.unblock(); me.refreshIf(); }, block: function(){ this.blockRefresh = this.view.blockRefresh = true; }, unblock: function(){ this.blockRefresh = this.view.blockRefresh = false; }, /** * 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) { if (me.prunedHeader) { me.prunedHeader.show(); } me.prunedHeader = header; header.hide(); } }, getGroupedHeader: function(){ var groupField = this.getGroupField(), headerCt = this.view.headerCt; return groupField ? headerCt.down('[dataIndex=' + groupField + ']') : null; }, getGroupField: function(){ var group = this.view.store.groupers.first(); if (group) { return group.property; } return ''; }, /** * When a row gains focus, expand the groups above it * @private */ onRowFocus: function(rowIdx) { var node = this.view.getNode(rowIdx), groupBd = Ext.fly(node).up('.' + this.collapsedCls); if (groupBd) { // for multiple level groups, should expand every groupBd // above this.expand(groupBd); } }, /** * Returns `true` if the named group is expanded. * @param {String} groupName The group name as returned from {@link Ext.data.Store#getGroupString getGroupString}. This is usually 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.collapsedState[groupName] === false); }, /** * Expand a group * @param {String/Ext.Element} groupName The group name, or the element that contains the group body * @param {Boolean} focus Pass `true` to focus the group after expand. */ expand: function(groupName, focus, /*private*/ preventSizeCalculation) { var me = this, view = me.view, groupHeader, groupBody, lockingPartner = me.lockingPartner; // We've been passed the group name if (Ext.isString(groupName)) { groupBody = Ext.fly(me.getGroupBodyId(groupName), '_grouping'); } // We've been passed an element else { groupBody = Ext.fly(groupName, '_grouping') groupName = me.getGroupName(groupBody); } groupHeader = Ext.get(me.getGroupHeaderId(groupName)); // If we are collapsed... if (me.collapsedState[groupName]) { groupBody.removeCls(me.collapsedCls); groupBody.prev().removeCls(me.hdCollapsedCls); if (preventSizeCalculation !== true) { view.refreshSize(); } view.fireEvent('groupexpand', view, groupHeader, groupName); me.collapsedState[groupName] = false; // If we are one side of a locking view, the other side has to stay in sync if (lockingPartner) { lockingPartner.expand(groupName, focus, preventSizeCalculation); } if (focus) { groupBody.scrollIntoView(view.el, null, true); } } }, /** * Expand all groups */ expandAll: function(){ var me = this, view = me.view, els = view.el.select(me.eventSelector).elements, e, eLen = els.length; for (e = 0; e < eLen; e++) { me.expand(Ext.fly(els[e]).next(), false, true); } view.refreshSize(); }, /** * Collapse a group * @param {String/Ext.Element} groupName The group name, or the element that contains group body * @param {Boolean} focus Pass `true` to focus the group after expand. */ collapse: function(groupName, focus, /*private*/ preventSizeCalculation) { var me = this, view = me.view, groupHeader, groupBody, lockingPartner = me.lockingPartner; // We've been passed the group name if (Ext.isString(groupName)) { groupBody = Ext.fly(me.getGroupBodyId(groupName), '_grouping'); } // We've been passed an element else { groupBody = Ext.fly(groupName, '_grouping') groupName = me.getGroupName(groupBody); } groupHeader = Ext.get(me.getGroupHeaderId(groupName)); // If we are not collapsed... if (!me.collapsedState[groupName]) { groupBody.addCls(me.collapsedCls); groupBody.prev().addCls(me.hdCollapsedCls); if (preventSizeCalculation !== true) { view.refreshSize(); } view.fireEvent('groupcollapse', view, groupHeader, groupName); me.collapsedState[groupName] = true; // If we are one side of a locking view, the other side has to stay in sync if (lockingPartner) { lockingPartner.collapse(groupName, focus, preventSizeCalculation); } if (focus) { groupHeader.scrollIntoView(view.el, null, true); } } }, /** * Collapse all groups */ collapseAll: function() { var me = this, view = me.view, els = view.el.select(me.eventSelector).elements, e, eLen = els.length; for (e = 0; e < eLen; e++) { me.collapse(Ext.fly(els[e]).next(), false, true); } view.refreshSize(); }, onGroupChange: function(){ var me = this, field = me.getGroupField(), menuItem, visibleGridColumns, groupingByLastVisibleColumn; if (me.hideGroupedHeader) { if (me.lastGroupField) { menuItem = me.getMenuItem(me.lastGroupField); if (menuItem) { menuItem.setChecked(true); } } if (field) { visibleGridColumns = me.view.headerCt.getVisibleGridColumns(); // See if we are being asked to group by the sole remaining visible column. // If so, then do not hide that column. groupingByLastVisibleColumn = ((visibleGridColumns.length === 1) && (visibleGridColumns[0].dataIndex == field)); menuItem = me.getMenuItem(field); if (menuItem && !groupingByLastVisibleColumn) { menuItem.setChecked(false); } } } me.refreshIf(); me.lastGroupField = field; }, /** * 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; }, /** * Toggle between expanded/collapsed state when clicking on * the group. * @private */ onGroupClick: function(view, rowElement, groupName, e) { var me = this; if (me.collapsible) { if (me.collapsedState[groupName]) { me.expand(groupName); } else { me.collapse(groupName); } } }, // Injects isRow and closeRow into the metaRowTpl. getMetaRowTplFragments: function() { return { isRow: this.isRow, closeRow: this.closeRow }; }, // injected into rowtpl and wrapped around metaRowTpl // becomes part of the standard tpl isRow: function() { return ''; }, // injected into rowtpl and wrapped around metaRowTpl // becomes part of the standard tpl closeRow: function() { return ''; }, // isRow and closeRow are injected via getMetaRowTplFragments mutateMetaRowTpl: function(metaRowTpl) { metaRowTpl.unshift('{[this.isRow()]}'); metaRowTpl.push('{[this.closeRow()]}'); }, // injects an additional style attribute via tdAttrKey with the proper // amount of padding getAdditionalData: function(data, idx, record, orig) { var view = this.view, hCt = view.headerCt, col = hCt.items.getAt(0), o = {}, tdAttrKey; // If there *are* any columne in this grid (possible empty side of a locking grid)... // Add the padding-left style to indent the row according to grouping depth. // Preserve any current tdAttr that a user may have set. if (col) { tdAttrKey = col.id + '-tdAttr'; o[tdAttrKey] = this.indentByDepth(data) + " " + (orig[tdAttrKey] ? orig[tdAttrKey] : ''); o.collapsed = 'true'; o.data = record.getData(); } return o; }, // return matching preppedRecords getGroupRows: function(group, records, preppedRecords, fullWidth) { var me = this, children = group.children, rows = group.rows = [], view = me.view, header = me.getGroupedHeader(), groupField = me.getGroupField(), index = -1, r, rLen = records.length, record; // Buffered rendering implies that user collapsing is disabled. if (view.store.buffered) { me.collapsible = false; } group.viewId = view.id; for (r = 0; r < rLen; r++) { record = records[r]; if (record.get(groupField) == group.name) { index = r; } if (Ext.Array.indexOf(children, record) != -1) { rows.push(Ext.apply(preppedRecords[r], { depth : 1 })); } } group.groupField = groupField, group.groupHeaderId = me.getGroupHeaderId(group.name); group.groupBodyId = me.getGroupBodyId(group.name); group.fullWidth = fullWidth; group.columnName = header ? header.text : groupField; group.groupValue = group.name; // Here we attempt to overwrite the group name value from the Store with // the get the rendered value of the column from the *prepped* record if (header && index > -1) { group.name = group.renderedValue = preppedRecords[index][header.id]; } if (me.collapsedState[group.name]) { group.collapsedCls = me.collapsedCls; group.hdCollapsedCls = me.hdCollapsedCls; } else { group.collapsedCls = group.hdCollapsedCls = ''; } // Collapsibility of groups may be disabled. if (me.collapsible) { group.collapsibleClass = me.hdCollapsibleCls; } else { group.collapsibleClass = ''; } return group; }, // Create an associated DOM id for the group's header element given the group name getGroupHeaderId: function(groupName) { return this.view.id + '-hd-' + groupName; }, // Create an associated DOM id for the group's body element given the group name getGroupBodyId: function(groupName) { return this.view.id + '-bd-' + groupName; }, // Get the group name from an associated element whether it's within a header or a body getGroupName: function(element) { var me = this, targetEl; // See if element is, or is within a group header. If so, we can extract its name targetEl = Ext.fly(element).findParent(me.eventSelector); if (targetEl) { return targetEl.id.split(this.view.id + '-hd-')[1]; } // See if element is, or is within a group body. If so, we can extract its name targetEl = Ext.fly(element).findParent(me.bodySelector); if (targetEl) { return targetEl.id.split(this.view.id + '-bd-')[1]; } }, // return the data in a grouped format. collectData: function(records, preppedRecords, startIndex, fullWidth, o) { var me = this, store = me.view.store, collapsedState = me.collapsedState, collapseGroups, g, groups, gLen, group; if (me.startCollapsed) { // If we start collapse, we'll set the state of the groups here // and unset the flag so any subsequent expand/collapse is // managed by the feature me.startCollapsed = false; collapseGroups = true; } if (!me.disabled && store.isGrouped()) { o.rows = groups = store.getGroups(); gLen = groups.length; for (g = 0; g < gLen; g++) { group = groups[g]; if (collapseGroups) { collapsedState[group.name] = true; } me.getGroupRows(group, records, preppedRecords, fullWidth); } } return o; }, // adds the groupName to the groupclick, groupdblclick, groupcontextmenu // events that are fired on the view. Chose not to return the actual // group itself because of its expense and because developers can simply // grab the group via store.getGroups(groupName) getFireEventArgs: function(type, view, targetEl, e) { return [type, view, targetEl, this.getGroupName(targetEl), e]; } }); /** * 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 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 * * ## 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', { /* Begin Definitions */ extend: 'Ext.grid.feature.Grouping', alias: 'feature.groupingsummary', mixins: { summary: 'Ext.grid.feature.AbstractSummary' }, /* End Definitions */ init: function() { this.mixins.summary.init.call(this); }, /** * Modifies the row template to include the summary row. * @private * @return {String} The modified template */ getFeatureTpl: function() { var tpl = this.callParent(arguments); if (this.showSummaryRow) { // lop off the end
so we can attach it tpl = tpl.replace('
', ''); tpl += '{[this.printSummaryRow(xindex)]}
'; } return tpl; }, /** * Gets any fragments needed for the template. * @private * @return {Object} The fragments */ getFragmentTpl: function() { var me = this, fragments = me.callParent(); Ext.apply(fragments, me.getSummaryFragments()); if (me.showSummaryRow) { // this gets called before render, so we'll setup the data here. me.summaryGroups = me.view.store.getGroups(); me.summaryData = me.generateSummaryData(); } return fragments; }, /** * Gets the data for printing a template row * @private * @param {Number} index The index in the template * @return {Array} The template values */ getPrintData: function(index){ var me = this, columns = me.view.headerCt.getColumnsForTpl(), i = 0, length = columns.length, data = [], name = me.summaryGroups[index - 1].name, active = me.summaryData[name], column; for (; i < length; ++i) { column = columns[i]; column.gridSummaryValue = this.getColumnValue(column, active); data.push(column); } return data; }, /** * Generates all of the summary data to be used when processing the template * @private * @return {Object} The summary data */ generateSummaryData: function(){ var me = this, data = {}, remoteData = {}, store = me.view.store, groupField = this.getGroupField(), reader = store.proxy.reader, groups = me.summaryGroups, columns = me.view.headerCt.getColumnsForTpl(), remote, i, length, fieldData, root, key, comp, summaryRows, s, sLen, convertedSummaryRow; for (i = 0, length = groups.length; i < length; ++i) { data[groups[i].name] = {}; } /** * @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 && reader.rawData) { // reset reader root and rebuild extractors to extract summaries data root = reader.root; reader.root = me.remoteRoot; reader.buildExtractors(true); summaryRows = reader.getRoot(reader.rawData); sLen = summaryRows.length; // Ensure the Reader has a data conversion function to convert a raw data row into a Record data hash if (!reader.convertRecordData) { reader.buildExtractors(); } for (s = 0; s < sLen; s++) { convertedSummaryRow = {}; // Convert a raw data row into a Record's hash object using the Reader reader.convertRecordData(convertedSummaryRow, summaryRows[s]); remoteData[convertedSummaryRow[groupField]] = convertedSummaryRow; } // restore initial reader configuration reader.root = root; reader.buildExtractors(true); } for (i = 0, length = columns.length; i < length; ++i) { comp = Ext.getCmp(columns[i].id); fieldData = me.getSummary(store, comp.summaryType, comp.dataIndex, true); for (key in fieldData) { if (fieldData.hasOwnProperty(key)) { data[key][comp.id] = fieldData[key]; } } for (key in remoteData) { if (remoteData.hasOwnProperty(key)) { remote = remoteData[key][comp.dataIndex]; if (remote !== undefined && data[key] !== undefined) { data[key][comp.id] = remote; } } } } return data; } }); /** * 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 a grid. * * Rowbodies are initially hidden unless you override getAdditionalData. * * Will expose additional events on the gridview with the prefix of 'rowbody'. * For example: 'rowbodyclick', 'rowbodydblclick', 'rowbodycontextmenu'. * * # Example * * @example * Ext.define('Animal', { * extend: 'Ext.data.Model', * fields: ['name', 'latin', 'desc'] * }); * * 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).'}, * {name: 'Roman snail', latin: 'Helix pomatia', * desc: 'A species of large, edible, air-breathing land snail.'}, * {name: 'Yellow-winged darter', latin: 'Sympetrum flaveolum', * desc: 'A dragonfly found in Europe and mid and Northern China.'}, * {name: 'Superb Fairy-wren', latin: 'Malurus cyaneus', * desc: 'Common and familiar across south-eastern Australia.'} * ] * }, * columns: [{ * dataIndex: 'name', * text: 'Common name', * width: 125 * }, { * dataIndex: 'latin', * text: 'Scientific name', * flex: 1 * }], * features: [{ * ftype: 'rowbody', * getAdditionalData: function(data, rowIndex, record, orig) { * var headerCt = this.view.headerCt, * colspan = headerCt.getColumnCount(); * // Usually you would style the my-body-class in CSS file * return { * rowBody: '
'+record.get("desc")+'
', * rowBodyCls: "my-body-class", * rowBodyColspan: colspan * }; * } * }] * }); */ Ext.define('Ext.grid.feature.RowBody', { extend: 'Ext.grid.feature.Feature', alias: 'feature.rowbody', rowBodyHiddenCls: Ext.baseCSSPrefix + 'grid-row-body-hidden', rowBodyTrCls: Ext.baseCSSPrefix + 'grid-rowbody-tr', rowBodyTdCls: Ext.baseCSSPrefix + 'grid-cell-rowbody', rowBodyDivCls: Ext.baseCSSPrefix + 'grid-rowbody', eventPrefix: 'rowbody', eventSelector: '.' + Ext.baseCSSPrefix + 'grid-rowbody-tr', getRowBody: function(values) { return [ '', '', '
{rowBody}
', '', '' ].join(''); }, // injects getRowBody into the metaRowTpl. getMetaRowTplFragments: function() { return { getRowBody: this.getRowBody, rowBodyTrCls: this.rowBodyTrCls, rowBodyTdCls: this.rowBodyTdCls, rowBodyDivCls: this.rowBodyDivCls }; }, mutateMetaRowTpl: function(metaRowTpl) { metaRowTpl.push('{[this.getRowBody(values)]}'); }, /** * 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 rowBodyCls, rowBodyColspan, and rowBody. * @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. */ getAdditionalData: function(data, idx, record, orig) { var headerCt = this.view.headerCt, colspan = headerCt.getColumnCount(); return { rowBody: "", rowBodyCls: this.rowBodyCls, rowBodyColspan: colspan }; } }); /** * @private */ Ext.define('Ext.grid.feature.RowWrap', { extend: 'Ext.grid.feature.Feature', alias: 'feature.rowwrap', // turn off feature events. hasFeatureEvent: false, init: function() { if (!this.disabled) { this.enable(); } }, getRowSelector: function(){ return 'tr:has(> ' + this.view.cellSelector + ')'; }, enable: function(){ var me = this, view = me.view; me.callParent(); // we need to mutate the rowSelector since the template changes the ordering me.savedRowSelector = view.rowSelector; view.rowSelector = me.getRowSelector(); // Extra functionality needed on header resize when row is wrapped: // Every individual cell in a column needs its width syncing. // So we produce a different column selector which includes al TDs in a column view.getComponentLayout().getColumnSelector = me.getColumnSelector; }, disable: function(){ var me = this, view = me.view, saved = me.savedRowSelector; me.callParent(); if (saved) { view.rowSelector = saved; } delete me.savedRowSelector; }, mutateMetaRowTpl: function(metaRowTpl) { var prefix = Ext.baseCSSPrefix; // Remove "x-grid-row" from the first row, note this could be wrong // if some other feature unshifted things in front. metaRowTpl[0] = metaRowTpl[0].replace(prefix + 'grid-row', ''); metaRowTpl[0] = metaRowTpl[0].replace("{[this.embedRowCls()]}", ""); // 2 metaRowTpl.unshift(''); // 1 metaRowTpl.unshift('
'); // 3 metaRowTpl.push('
'); // 4 metaRowTpl.push(''); }, embedColSpan: function() { return '{colspan}'; }, embedFullWidth: function() { return '{fullWidth}'; }, getAdditionalData: function(data, idx, record, orig) { var headerCt = this.view.headerCt, colspan = headerCt.getColumnCount(), fullWidth = headerCt.getFullWidth(), items = headerCt.query('gridcolumn'), itemsLn = items.length, i = 0, o = { colspan: colspan, fullWidth: fullWidth }, id, tdClsKey, colResizerCls; for (; i < itemsLn; i++) { id = items[i].id; tdClsKey = id + '-tdCls'; colResizerCls = Ext.baseCSSPrefix + 'grid-col-resizer-'+id; // give the inner td's the resizer class // while maintaining anything a user may have injected via a custom // renderer o[tdClsKey] = colResizerCls + " " + (orig[tdClsKey] ? orig[tdClsKey] : ''); // TODO: Unhackify the initial rendering width's o[id+'-tdAttr'] = " style=\"width: " + (items[i].hidden ? 0 : items[i].getDesiredWidth()) + "px;\" "/* + (i === 0 ? " rowspan=\"2\"" : "")*/; if (orig[id+'-tdAttr']) { o[id+'-tdAttr'] += orig[id+'-tdAttr']; } } return o; }, getMetaRowTplFragments: function() { return { embedFullWidth: this.embedFullWidth, embedColSpan: this.embedColSpan }; }, getColumnSelector: function(header) { var s = Ext.baseCSSPrefix + 'grid-col-resizer-' + header.id; return 'th.' + s + ',td.' + s; } }); /** * 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 * * ## Example Usage * * @example * Ext.define('TestResult', { * extend: 'Ext.data.Model', * fields: ['student', { * name: 'mark', * type: 'int' * }] * }); * * Ext.create('Ext.grid.Panel', { * width: 200, * height: 140, * 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', /* End Definitions */ /** * Gets any fragments needed for the template. * @private * @return {Object} The fragments */ getFragmentTpl: function() { // this gets called before render, so we'll setup the data here. this.summaryData = this.generateSummaryData(); return this.getSummaryFragments(); }, /** * Overrides the closeRows method on the template so we can include our own custom * footer. * @private * @return {Object} The custom fragments */ getTableFragments: function(){ if (this.showSummaryRow) { return { closeRows: this.closeRows }; } }, /** * Provide our own custom footer for the grid. * @private * @return {String} The custom footer */ closeRows: function() { return '
{[this.printSummaryRow()]}'; }, /** * Gets the data for printing a template row * @private * @param {Number} index The index in the template * @return {Array} The template values */ getPrintData: function(index){ var me = this, columns = me.view.headerCt.getColumnsForTpl(), i = 0, length = columns.length, data = [], active = me.summaryData, column; for (; i < length; ++i) { column = columns[i]; column.gridSummaryValue = this.getColumnValue(column, active); data.push(column); } return data; }, /** * Generates all of the summary data to be used when processing the template * @private * @return {Object} The summary data */ generateSummaryData: function(){ var me = this, data = {}, store = me.view.store, columns = me.view.headerCt.getColumnsForTpl(), i = 0, length = columns.length, fieldData, key, comp; for (i = 0, length = columns.length; i < length; ++i) { comp = Ext.getCmp(columns[i].id); data[comp.id] = me.getSummary(store, comp.summaryType, comp.dataIndex, false); } return data; } }); /** * This plugin provides drag and/or drop functionality for a GridView. * * It creates a specialized instance of {@link Ext.dd.DragZone DragZone} which knows how to drag out of a {@link * Ext.grid.View GridView} and loads the data object which is passed to a cooperating {@link Ext.dd.DragZone DragZone}'s * methods with the following properties: * * - `copy` : Boolean * * The value of the GridView's `copy` property, or `true` if the GridView was configured with `allowCopy: true` _and_ * the control key was pressed when the drag operation was begun. * * - `view` : GridView * * The source GridView from which the drag originated. * * - `ddel` : HtmlElement * * The drag proxy element which moves with the mouse * * - `item` : HtmlElement * * The GridView node upon which the mousedown event was registered. * * - `records` : Array * * An Array of {@link Ext.data.Model Model}s representing the selected data being dragged from the source GridView. * * It also creates a specialized instance of {@link Ext.dd.DropZone} which cooperates with other DropZones which are * members of the same ddGroup which processes such data objects. * * Adding this plugin to a view means that two new events may be fired from the client GridView, `{@link #beforedrop * beforedrop}` and `{@link #drop drop}` * * @example * Ext.create('Ext.data.Store', { * storeId:'simpsonsStore', * fields:['name'], * data: [["Lisa"], ["Bart"], ["Homer"], ["Marge"]], * proxy: { * type: 'memory', * reader: 'array' * } * }); * * Ext.create('Ext.grid.Panel', { * store: 'simpsonsStore', * 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.AbstractPlugin', alias: 'plugin.gridviewdragdrop', uses: [ 'Ext.view.DragZone', 'Ext.grid.ViewDropZone' ], /** * @event beforedrop * **This event is fired through the GridView. Add listeners to the GridView object** * * Fired when a drop gesture has been triggered by a mouseup event in a valid drop position in the GridView. * * @param {HTMLElement} node The GridView node **if any** over which the mouse was positioned. * * Returning `false` to this event signals that the drop gesture was invalid, and if the drag proxy will animate * back to the point from which the drag began. * * Returning `0` To this event signals that the data transfer operation should not take place, but that the gesture * was valid, and that the repair operation should not take place. * * Any other return value continues with the data transfer operation. * * @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: * * - copy : Boolean * * The value of the GridView's `copy` property, or `true` if the GridView was configured with `allowCopy: true` and * the control key was pressed when the drag operation was begun * * - view : GridView * * The source GridView from which the drag originated. * * - ddel : HtmlElement * * The drag proxy element which moves with the mouse * * - item : HtmlElement * * The GridView node upon which the mousedown event was registered. * * - records : Array * * An Array of {@link Ext.data.Model Model}s representing the selected data being dragged from the source GridView. * * @param {Ext.data.Model} overModel The Model over which the drop gesture took place. * * @param {String} dropPosition `"before"` or `"after"` depending on whether the mouse is above or below the midline * of the node. * * @param {Function} dropFunction * * A function to call to complete the data transfer operation and either move or copy Model instances from the * source View's Store to the destination View's Store. * * This is useful when you want to perform some kind of asynchronous processing before confirming the drop, such as * an {@link Ext.window.MessageBox#confirm confirm} call, or an Ajax request. * * Return `0` from this event handler, and call the `dropFunction` at any time to perform the data transfer. */ /** * @event drop * **This event is fired through the GridView. Add listeners to the GridView object** Fired when a drop operation * has been completed and the data has been moved or copied. * * @param {HTMLElement} node The GridView node **if any** over which the mouse 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: * * - copy : Boolean * * The value of the GridView's `copy` property, or `true` if the GridView was configured with `allowCopy: true` and * the control key was pressed when the drag operation was begun * * - view : GridView * * The source GridView from which the drag originated. * * - ddel : HtmlElement * * The drag proxy element which moves with the mouse * * - item : HtmlElement * * The GridView node upon which the mousedown event was registered. * * - records : Array * * An Array of {@link Ext.data.Model Model}s representing the selected data being dragged from the source GridView. * * @param {Ext.data.Model} overModel The Model over which the drop gesture took place. * * @param {String} dropPosition `"before"` or `"after"` depending on whether the mouse is above or below the midline * of the node. */ // /** * @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 * 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 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 ddGroup. */ /** * @cfg {String} dropGroup * The 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 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, init : function(view) { view.on('render', this.onViewRender, this, {single: true}); }, /** * @private * AbstractComponent calls destroy on all its plugins at destroy time. */ destroy: function() { Ext.destroy(this.dragZone, this.dropZone); }, 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; if (me.enableDrag) { me.dragZone = new Ext.view.DragZone({ view: view, ddGroup: me.dragGroup || me.ddGroup, dragText: me.dragText }); } if (me.enableDrop) { me.dropZone = new Ext.grid.ViewDropZone({ view: view, ddGroup: me.dropGroup || me.ddGroup }); } } }); /** * @class Ext.data.writer.Json This class is used to write {@link Ext.data.Model} data to the server in a JSON format. The {@link #allowSingle} configuration can be set to false to force the records to always be encoded in an array, even if there is only a single record being sent. * @markdown */ Ext.define('Ext.data.writer.Json', { extend: 'Ext.data.writer.Writer', alternateClassName: 'Ext.data.JsonWriter', alias: 'writer.json', /** * @cfg {String} root The key under which the records in this Writer will be placed. Defaults to undefined. * Example generated request, using root: 'records':

{'records': [{name: 'my record'}, {name: 'another record'}]}
*/ root: undefined, /** * @cfg {Boolean} encode True to use Ext.encode() on the data before sending. Defaults to false. * The encode option should only be set to true when a {@link #root} is defined, because the values will be * sent as part of the request parameters as opposed to a raw post. The root will be the name of the parameter * sent to the server. */ encode: false, /** * @cfg {Boolean} allowSingle False to ensure that records are always wrapped in an array, even if there is only * one record being sent. When there is more than one record, they will always be encoded into an array. * Defaults to true. Example: *

// with allowSingle: true
"root": {
    "first": "Mark",
    "last": "Corrigan"
}

// with allowSingle: false
"root": [{
    "first": "Mark",
    "last": "Corrigan"
}]
     * 
*/ allowSingle: true, //inherit docs writeRecords: function(request, data) { var root = this.root; if (this.allowSingle && data.length == 1) { // convert to single object format data = data[0]; } if (this.encode) { if (root) { // sending as a param, need to encode request.params[root] = Ext.encode(data); } else { Ext.Error.raise('Must specify a root when using encode'); } } else { // send as jsonData request.jsonData = request.jsonData || {}; if (root) { request.jsonData[root] = data; } else { request.jsonData = data; } } return request; } }); /** * @author Ed Spencer * @class Ext.data.Batch * *

Provides a mechanism to run one or more {@link Ext.data.Operation operations} in a given order. Fires the 'operationcomplete' event * after the completion of each Operation, and the 'complete' event when all Operations have been successfully executed. Fires an 'exception' * event if any of the Operations encounter an exception.

* *

Usually these are only used internally by {@link Ext.data.proxy.Proxy} classes

* */ Ext.define('Ext.data.Batch', { mixins: { observable: 'Ext.util.Observable' }, /** * @cfg {Boolean} autoStart * True to immediately start processing the batch as soon as it is constructed (defaults to false) */ autoStart: false, /** * @cfg {Boolean} pauseOnException * True to pause the execution of the batch if any operation encounters an exception * (defaults to false). If you set this to true you are responsible for implementing the appropriate * handling logic and restarting or discarding the batch as needed. There are different ways you could * do this, e.g. by handling the batch's {@link #exception} event directly, or perhaps by overriding * {@link Ext.data.AbstractStore#onBatchException onBatchException} at the store level. If you do pause * and attempt to handle the exception you can call {@link #retry} to process the same operation again. * * Note that {@link Ext.data.Operation operations} are atomic, so any operations that may have succeeded * prior to an exception (and up until pausing the batch) will be finalized at the server level and will * not be automatically reversible. Any transactional / rollback behavior that might be desired would have * to be implemented at the application level. Pausing on exception will likely be most beneficial when * used in coordination with such a scheme, where an exception might actually affect subsequent operations * in the same batch and so should be handled before continuing with the next operation. * * If you have not implemented transactional operation handling then this option should typically be left * to the default of false (e.g. process as many operations as possible, and handle any exceptions * asynchronously without holding up the rest of the batch). */ pauseOnException: false, /** * @property {Number} current * The index of the current operation being executed. Read only */ current: -1, /** * @property {Number} total * The total number of operations in this batch. Read only */ total: 0, /** * @property {Boolean} isRunning * True if the batch is currently running. Read only */ isRunning: false, /** * @property {Boolean} isComplete * True if this batch has been executed completely. Read only */ isComplete: false, /** * @property {Boolean} hasException * True if this batch has encountered an exception. This is cleared at the start of each operation. Read only */ hasException: false, /** * Creates new Batch object. * @param {Object} [config] Config object */ constructor: function(config) { var me = this; /** * @event complete * Fired when all operations of this batch have been completed * @param {Ext.data.Batch} batch The batch object * @param {Object} operation The last operation that was executed */ /** * @event exception * Fired when a operation encountered an exception * @param {Ext.data.Batch} batch The batch object * @param {Object} operation The operation that encountered the exception */ /** * @event operationcomplete * Fired when each operation of the batch completes * @param {Ext.data.Batch} batch The batch object * @param {Object} operation The operation that just completed */ me.mixins.observable.constructor.call(me, config); /** * Ordered array of operations that will be executed by this batch * @property {Ext.data.Operation[]} operations */ me.operations = []; /** * Ordered array of operations that raised an exception during the most recent * batch execution and did not successfully complete * @property {Ext.data.Operation[]} exceptions */ me.exceptions = []; }, /** * Adds a new operation to this batch at the end of the {@link #operations} array * @param {Object} operation The {@link Ext.data.Operation Operation} object * @return {Ext.data.Batch} this */ add: function(operation) { this.total++; operation.setBatch(this); this.operations.push(operation); return this; }, /** * Kicks off execution of the batch, continuing from the next operation if the previous * operation encountered an exception, or if execution was paused. Use this method to start * the batch for the first time or to restart a paused batch by skipping the current * unsuccessful operation. * * To retry processing the current operation before continuing to the rest of the batch (e.g. * because you explicitly handled the operation's exception), call {@link #retry} instead. * * Note that if the batch is already running any call to start will be ignored. * * @return {Ext.data.Batch} this */ start: function(/* private */ index) { var me = this; if (me.isRunning) { return me; } me.exceptions.length = 0; me.hasException = false; me.isRunning = true; return me.runOperation(Ext.isDefined(index) ? index : me.current + 1); }, /** * Kicks off execution of the batch, continuing from the current operation. This is intended * for restarting a {@link #pause paused} batch after an exception, and the operation that raised * the exception will now be retried. The batch will then continue with its normal processing until * all operations are complete or another exception is encountered. * * Note that if the batch is already running any call to retry will be ignored. * * @return {Ext.data.Batch} this */ retry: function() { return this.start(this.current); }, /** * @private * Runs the next operation, relative to this.current. * @return {Ext.data.Batch} this */ runNextOperation: function() { return this.runOperation(this.current + 1); }, /** * Pauses execution of the batch, but does not cancel the current operation * @return {Ext.data.Batch} this */ pause: function() { this.isRunning = false; return this; }, /** * Executes an operation by its numeric index in the {@link #operations} array * @param {Number} index The operation index to run * @return {Ext.data.Batch} this */ runOperation: function(index) { var me = this, operations = me.operations, operation = operations[index], onProxyReturn; if (operation === undefined) { me.isRunning = false; me.isComplete = true; me.fireEvent('complete', me, operations[operations.length - 1]); } else { me.current = index; onProxyReturn = function(operation) { var hasException = operation.hasException(); if (hasException) { me.hasException = true; me.exceptions.push(operation); me.fireEvent('exception', me, operation); } if (hasException && me.pauseOnException) { me.pause(); } else { operation.setCompleted(); me.fireEvent('operationcomplete', me, operation); me.runNextOperation(); } }; operation.setStarted(); me.proxy[operation.action](operation, onProxyReturn, me); } return me; } }); /** * 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. */ Ext.define('Ext.data.Connection', { mixins: { observable: 'Ext.util.Observable' }, statics: { requestId: 0 }, url: null, async: true, method: null, username: '', 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} 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, /** * @cfg {String} disableCachingParam * Change the parameter which is sent went disabling caching through a cache buster. */ disableCachingParam: '_dc', /** * @cfg {Number} timeout * The timeout in milliseconds to be used for requests. */ timeout : 30000, /** * @cfg {Object} extraParams * Any parameters to be appended to the request. */ /** * @cfg {Boolean} [autoAbort=false] * Whether this request should abort any pending requests. */ /** * @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. */ /** * @cfg {Object} defaultHeaders * An object containing request headers which are added to each request made by this object. */ useDefaultHeader : true, defaultPostHeader : 'application/x-www-form-urlencoded; charset=UTF-8', useDefaultXhrHeader : true, defaultXhrHeader : 'XMLHttpRequest', constructor : function(config) { config = config || {}; Ext.apply(this, config); /** * @event beforerequest * 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. * 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. */ this.requests = {}; this.mixins.observable.constructor.call(this); }, /** * Sends an HTTP request to a remote server. * * **Important:** Ajax server requests are asynchronous, and this call will * return before the response has been received. Process any returned data * in a callback function. * * 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 {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 30 seconds. * * @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 `