Ext.namespace('Zarafa.core.ui.notifier'); /** * @class Zarafa.core.ui.notifier.SliderContainer * @extends Object * * Special container to be used by the {@link Zarafa.core.ui.notifier.SliderNotifyPlugin}. * This supports holding multiple notification messages within a single DIV element, * and slide new ones into the container, and old ones out. The code is structured so only * one animation occurs at a time, and the container can be positioned anywhere on the screen. */ Zarafa.core.ui.notifier.SliderContainer = Ext.extend(Object, { /** * @cfg {Ext.Element} parentContainer The parent container in which the * slider will be positioned. */ parentContainer : undefined, /** * @cfg {String} containerCls The CSS class to be applied on the {@link #container}. */ containerCls : 'zarafa-notifier-container', /** * @cfg {String} itemCls The CSS class to be applied to the DIV wrapper around * the messages added in {@link #createMessage}. */ itemCls : 'zarafa-notifier-container-item', /** * @cfg {String} containerPosition The position of the container * in which the notifier will be shown. This can be any of the following values: * - 'tl': The top left corner * - 't': The center of the top edge (default) * - 'tr': The top right corner * - 'l': The center of the left edge * - 'c': In the center of the element * - 'r': The center of the right edge * - 'bl': The bottom left corner * - 'b': The center of the bottom edge * - 'br': The bottom right corner */ containerPosition : 't', /** * @cfg {Char} slideinDirection the animation slidein direction for notification */ slideinDirection : 't', /** * @cfg {Char} slideoutDirection the animation slideout direction for notification */ slideoutDirection : 't', /** * @cfg {Boolean} animatedContainerResize True to animate the resizing of the container * when a new notification is added into the container. This defaults to 'false' if * {@link #slideinDirection} is 't', 'true' otherwise. */ animatedContainerResize : false, /** * The container in which the sliders will be placed. * @property * @type Ext.Element * @private */ container : undefined, /** * The array of {@link Ext.Element Elements} which are pending to be displayed in the {@link #container}. * Because we cannot animate two messages simultaneously, we keep the list of pending messages here, which * serves as FIFO queue. Whenever a message has been animated, this list will be checked to see if a message * is in the queue to be animated. * @property * @type Array * @private */ pendingMessages : undefined, /** * The array of {@link Ext.Element Elements} which are currently displayed in the {@link #container}. * @property * @type Array * @private */ stackedMessages : undefined, /** * Used by {@link #showNextMessage} to check if there currently is a Message which is being animated. * When this field is set to 'true', then no other message is allowed to start animating. * @property * @type Array * @private */ animating : false, /** * @constructor * @param {Object} config Configuration object */ constructor : function(config) { Ext.apply(this, config); this.container = this.getContainer(); this.pendingMessages = []; this.stackedMessages = []; // update animatedContainerResize depending on configuration if (!Ext.isDefined(config.animatedContainerResize)) { this.animatedContainerResize = this.slideinDirection !== 't'; } }, /** * Add a new slider to the container. This will animate the slider into the {@link #container}. * @param {String} html The HTML string for the slider to create * @param {Number} timeout (optional) If provided, the message will be removed automatically after the timeout * @return {Ext.Element} The slider element which was created */ createMessage : function(html, timeout) { // Construct the Ext.Element object var element = Ext.DomHelper.append(this.container, { html : html, cls : this.itemCls, style : 'visibility: hidden;' }, true); // Push it to pending messages this.pendingMessages.push(element); element.timeout = timeout; element.slider = this; // Check if there are no pending messages, and we can direct display this new one. this.showNextMessage(); return element; }, /** * Update a message which was previously created using {@link #createMessage}. * @param {Ext.Element} The element to update * @param {String} html The HTML string for the slider to update * @return {Ext.Element} The slider element which was updated * @private */ updateMessage : function(element, html) { element.dom.innerHTML = html; return element; }, /** * Remove a message which was previously created using {@link #createMessage}. * @param {Ext.Element} element The element to remove */ removeMessage : function(element) { this.animating = true; if (this.stackedMessages.indexOf(element) >= 0) { element.ghost(this.slideoutDirection, { remove : true, stopFx : true, callback : this.onRemoveComplete.createDelegate(this, [ element ]), scope : this }); } }, /** * Called whenever a message has been added to the {@link #pendingMessages} queue, or when * a message has been removed from the {@link #stackedMessages}. When we still have pending * messages in {@link #pendingMessages}, we first check if we are currently busy * {@link #animating animating} a message, or if there is no room for a new message to appear. * When neither problem occurs, then we start animating the new message into the screen. * * @private */ showNextMessage : function() { var element = this.pendingMessages[0]; var msgHeight = element.getHeight(); var ctHeight = this.container.getHeight(); var maxHeight = this.parentContainer.getHeight(); // The <body> element might sometimes have height 0, compensate this // by asking for the document height in that case. if (maxHeight === 0 && this.parentContainer.dom.tagName === 'BODY') { maxHeight = document.height; } if (this.animating || (ctHeight + msgHeight) > maxHeight) { return; } this.animating = true; // Update the dimensions of the container to make room for the new element. // All existing messages will be nicely shifted the require height higher. var newHeight = ctHeight + msgHeight; this.updateContainer(newHeight, this.animatedContainerResize); // Store the original height of the message for later, // we need it for onMessageSlideOutComplete() when the real height has been reset, // and we need to resize the container. element.height = msgHeight; // Move the element to the next queue this.pendingMessages.remove(element); this.stackedMessages.push(element); this.animMessage(element); }, /** * Start the animation which will {@link Ext.Fx#slideIn slidein} the given element. * If the slider was configured with a timeout, it will {@link Ext.Fx#pause show} the * element for the given time and then {@link Ext.Fx#ghost remove} it again. * @param {Ext.Element} element The element to show * @private */ animMessage : function(element) { element = element.slideIn(this.slideinDirection, { callback : this.onShowComplete, scope : this }); // Check if this element must automatically be removed // after a certain timeout. if (Ext.isNumber(element.timeout)) { element.pause(element.timeout, { callback : this.onPauseComplete, scope : this }).ghost(this.slideoutDirection, { remove : true, callback : this.onRemoveComplete.createDelegate(this, [ element ]), scope : this }); } }, /** * Event handler which is called when the {@link Ext.Fx#slideIn slideIn} animation has been * completed. This will reset the {@link #animating} flag, and call {@link #showNextMessage} * to start animating any pending messages. * * @private */ onShowComplete : function() { this.animating = false; // Show the next message if there are pending messages, // and the parent container still exists (it might have been // deleted while the element was animating). if (!Ext.isEmpty(this.pendingMessages) && this.parentContainer.dom) { this.showNextMessage(); } }, /** * Event handler which is called when the {@link Ext.Fx#pause pause} has been completed. * This will set the {@link #animating} field again to prepare for the {@link Ext.Fx#ghost ghost} * animation. * * @private */ onPauseComplete : function() { this.animating = true; }, /** * Event handler which is called when the {@link Ext.Fx#ghost ghost} animation has been * completed. This will resize the {@link #container} to make sure it will only * contain enough room for the visible {@link #stackedMessages messages}. * @param {Ext.Element} element The element which was removed * @private */ onRemoveComplete : function(element) { var newHeight = this.container.getHeight() - element.height; var index = this.stackedMessages.indexOf(element); // Technically shouldn't happen... if (index < 0) { return; } // Remove the element from stack this.stackedMessages.splice(index, 1); this.animating = false; if (newHeight < 0) { newHeight = 0; } // After cleaning up, we need to check if the parent container // still exists, as it might have been deleted while the element // was animating. if (!this.parentContainer.dom) { return; } // Don't animate the repositioning, otherwise all visible // messages will be bumbed above and then animated down again. // By not animating, the messages will remain exactly where they // are. this.updateContainer(newHeight, false); if (!Ext.isEmpty(this.pendingMessages)) { this.showNextMessage(); } }, /** * Obtain the {@link Ext.Element container} in which this slider will operate. * This uses the {@link #parentContainer} to find if any slider container already * exists, if not, it will create one at the {@link #containerPosition}. * @return {Ext.Element} The slider container * @private */ getContainer : function() { var parentId = this.parentContainer.id; var sliderCls = this.containerCls + ' ' + this.containerCls + '-' + this.containerPosition; var sliderId = parentId + '-' + this.containerCls + '-' + this.containerPosition; var ct = Ext.DomQuery.select('#' + sliderId).shift(); if (!ct) { // Insert the notifier message into the given container ct = Ext.DomHelper.insertFirst(this.parentContainer, { id : sliderId, cls : sliderCls }, true); // Set initial height to 0 ct.setHeight(0); // Position the container to the correct location ct.alignTo(this.parentContainer, this.containerPosition); } else { ct = Ext.get(ct); } return ct; }, /** * Update the {@link #container} with a new height. This will recalculate the * position of the {@link #container} to make sure the container remains fixed * to the {@link #containerPosition. * * @param {Number} height The new height which must be applied to the container * @param {Boolean} animate True when the resize/repositioning of the container * should be animated. * @private */ updateContainer : function(height, animate) { // The position of the element should be: br-br, t-t, etc, to ensure // that the bottom-right corner of the container is in the bottom-right // corner of the parentContainer. var position = this.containerPosition + '-' + this.containerPosition; // Update positioning of the container this.container.setHeight(height); this.container.alignTo(this.parentContainer, position, undefined, animate); } });