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