/** * 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();});