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