slackgmailskypefacebook-workplaceoutlookemailmicrosoft-teamsdiscordmessengercustom-servicesmacoslinuxwindowsinboxwhatsappicloudtweetdeckhipchattelegramhangouts
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
571 lines
17 KiB
571 lines
17 KiB
/** |
|
* 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', |
|
|
|
/* Begin Definitions */ |
|
|
|
mixins: [ |
|
'Ext.util.StoreHolder' |
|
], |
|
|
|
uses: ['Ext.data.StoreManager'], |
|
|
|
/* End Definitions */ |
|
|
|
/** |
|
* @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. |
|
*/ |
|
|
|
//<locale> |
|
/** |
|
* @cfg {String} [msg="Loading..."] |
|
* The text to display in a centered loading message box. |
|
*/ |
|
msg: 'Loading...', |
|
//</locale> |
|
|
|
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: 'status', |
|
focusable: true, |
|
tabIndex: 0, |
|
|
|
autoEl: { |
|
tag: 'div', |
|
role: 'status' |
|
}, |
|
|
|
childEls: [ |
|
'msgWrapEl', |
|
'msgEl', |
|
'msgTextEl' |
|
], |
|
|
|
renderTpl: [ |
|
'<div id="{id}-msgWrapEl" data-ref="msgWrapEl" class="{[values.$comp.msgWrapCls]}">', |
|
'<div id="{id}-msgEl" data-ref="msgEl" class="{[values.$comp.msgCls]} ', |
|
Ext.baseCSSPrefix, 'mask-msg-inner {childElCls}">', |
|
'<div id="{id}-msgTextEl" data-ref="msgTextEl" class="', |
|
Ext.baseCSSPrefix, 'mask-msg-text', |
|
'{childElCls}">{msg}</div>', |
|
'</div>', |
|
'</div>' |
|
], |
|
|
|
/** |
|
* 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(); |
|
|
|
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) { |
|
me.onOwnerToFront(target, target.el.getZIndex()); |
|
} |
|
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 |
|
*/ |
|
bindStore : function(store, initial) { |
|
var me = this; |
|
me.mixins.storeholder.bindStore.apply(me, arguments); |
|
store = me.store; |
|
if (store && store.isLoading()) { |
|
me.onBeforeLoad(); |
|
} |
|
}, |
|
|
|
getStoreListeners: function(store) { |
|
var load = 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: load, |
|
buffer: 100 |
|
} |
|
}; |
|
|
|
// Only need to mask on load if the proxy is asynchronous - ie: Ajax/JsonP |
|
if (!store.loadsSynchronously()) { |
|
result.beforeload = beforeLoad; |
|
result.load = load; |
|
} |
|
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 layed out, defer so that the ZIndexManager |
|
// gets to read its layed 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(); |
|
|
|
if (!owner.isVisible(true)) { |
|
me.showNext = true; |
|
} |
|
else if (me.loading && owner.rendered) { |
|
me.show(); |
|
} |
|
}, |
|
|
|
hide: function() { |
|
var me = this, |
|
ownerCt = me.ownerCt; |
|
|
|
// Element support to be deprecated |
|
if (me.isElement) { |
|
ownerCt.unmask(); |
|
me.fireEvent('hide', this); |
|
|
|
return; |
|
} |
|
|
|
ownerCt.enableTabbing(); |
|
ownerCt.setMasked(false); |
|
|
|
delete me.showNext; |
|
|
|
return me.callParent(arguments); |
|
}, |
|
|
|
show: function() { |
|
var me = this; |
|
|
|
// 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, |
|
el = me.el; |
|
|
|
me.loading = true; |
|
me.callParent(arguments); |
|
|
|
// Allow dynamic setting of msgWrapCls |
|
if (me.hasOwnProperty('msgWrapCls')) { |
|
el.dom.className = me.msgWrapCls; |
|
} |
|
|
|
if (me.useMsg) { |
|
me.msgTextEl.setHtml(me.msg); |
|
} else { |
|
// Only the mask is visible if useMsg is false |
|
me.msgEl.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(); |
|
} |
|
|
|
ownerCt.disableTabbing(); |
|
ownerCt.setMasked(true); |
|
|
|
// Owner's disabled tabbing will also make the mask |
|
// untabbable since it is rendered within the target |
|
el.restoreTabbableState(); |
|
|
|
// If owner contains focus, focus this. |
|
// Component level onHide processing takes care of focus reversion on hide. |
|
if (ownerCt.containsFocus) { |
|
me.focus(); |
|
} |
|
me.sizeMask(); |
|
}, |
|
|
|
// @private |
|
onLoad : function() { |
|
this.loading = false; |
|
this.hide(); |
|
}, |
|
|
|
beforeDestroy: function() { |
|
// We don't have a real ownerCt, so clear it out here to prevent |
|
// spurious warnings when we are destroyed |
|
this.ownerCt = null; |
|
this.bindStore(null); |
|
this.callParent(); |
|
}, |
|
|
|
onDestroy: function() { |
|
var me = this; |
|
|
|
if (me.isElement) { |
|
me.ownerCt.unmask(); |
|
} |
|
|
|
me.callParent(); |
|
}, |
|
|
|
privates: { |
|
getFocusEl: function() { |
|
return this.el; |
|
} |
|
} |
|
});
|
|
|