/* * Notification / Toastwindow extension for Ext JS 4.x * * Copyright (c) 2011 Eirik Lorentsen (http://www.eirik.net/) * * Examples and documentation at: http://www.eirik.net/Ext/ux/window/Notification.html * * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses. * * Version: 2.0 * Last changed date: 2012-03-06 */ Ext.define('Ext.ux.window.Notification', { extend: 'Ext.window.Window', alias: 'widget.uxNotification', cls: 'ux-notification-window', autoHide: true, autoHeight: true, plain: false, draggable: false, shadow: false, focus: Ext.emptyFn, // For alignment and to store array of rendered notifications. Defaults to document if not set. manager: null, useXAxis: false, // Options: br, bl, tr, tl, t, l, b, r position: 'br', // Pixels between each notification spacing: 6, // Pixels from the managers borders to start the first notification paddingX: 30, paddingY: 10, slideInAnimation: 'easeIn', slideBackAnimation: 'bounceOut', slideInDuration: 1500, slideBackDuration: 1000, hideDuration: 500, autoHideDelay: 7000, stickOnClick: true, stickWhileHover: true, // Private. Do not override! isHiding: false, readyToHide: false, // Caching coordinates to be able to align to final position of siblings being animated xPos: 0, yPos: 0, statics: { defaultManager: { el: null } }, getXposAlignedToManager: function () { var me = this; var xPos = 0; // Avoid error messages if the manager does not have a dom element if (me.manager && me.manager.el && me.manager.el.dom) { if (!me.useXAxis) { // Element should already be aligned verticaly return me.el.getLeft(); } else { // Using getAnchorXY instead of getTop/getBottom should give a correct placement when document is used // as the manager but is still 0 px high. Before rendering the viewport. if (me.position == 'br' || me.position == 'tr' || me.position == 'r') { xPos += me.manager.el.getAnchorXY('r')[0]; xPos -= (me.el.getWidth() + me.paddingX); } else { xPos += me.manager.el.getAnchorXY('l')[0]; xPos += me.paddingX; } } } return xPos; }, getYposAlignedToManager: function () { var me = this; var yPos = 0; // Avoid error messages if the manager does not have a dom element if (me.manager && me.manager.el && me.manager.el.dom) { if (me.useXAxis) { // Element should already be aligned horizontaly return me.el.getTop(); } else { // Using getAnchorXY instead of getTop/getBottom should give a correct placement when document is used // as the manager but is still 0 px high. Before rendering the viewport. if (me.position == 'br' || me.position == 'bl' || me.position == 'b') { yPos += me.manager.el.getAnchorXY('b')[1]; yPos -= (me.el.getHeight() + me.paddingY); } else { yPos += me.manager.el.getAnchorXY('t')[1]; yPos += me.paddingY; } } } return yPos; }, getXposAlignedToSibling: function (sibling) { var me = this; if (me.useXAxis) { if (me.position == 'tl' || me.position == 'bl' || me.position == 'l') { // Using sibling's width when adding return (sibling.xPos + sibling.el.getWidth() + sibling.spacing); } else { // Using own width when subtracting return (sibling.xPos - me.el.getWidth() - me.spacing); } } else { return me.el.getLeft(); } }, getYposAlignedToSibling: function (sibling) { var me = this; if (me.useXAxis) { return me.el.getTop(); } else { if (me.position == 'tr' || me.position == 'tl' || me.position == 't') { // Using sibling's width when adding return (sibling.yPos + sibling.el.getHeight() + sibling.spacing); } else { // Using own width when subtracting return (sibling.yPos - me.el.getHeight() - sibling.spacing); } } }, getNotifications: function (alignment) { var me = this; if (!me.manager.notifications[alignment]) { me.manager.notifications[alignment] = []; } return me.manager.notifications[alignment]; }, beforeShow: function () { var me = this; // 1.x backwards compatibility if (Ext.isDefined(me.corner)) { me.position = me.corner; } if (Ext.isDefined(me.slideDownAnimation)) { me.slideBackAnimation = me.slideDownAnimation; } if (Ext.isDefined(me.autoDestroyDelay)) { me.autoHideDelay = me.autoDestroyDelay; } if (Ext.isDefined(me.slideInDelay)) { me.slideInDuration = me.slideInDelay; } if (Ext.isDefined(me.slideDownDelay)) { me.slideBackDuration = me.slideDownDelay; } if (Ext.isDefined(me.fadeDelay)) { me.hideDuration = me.fadeDelay; } // 'bc', lc', 'rc', 'tc' compatibility me.position = me.position.replace(/c/, ''); switch (me.position) { case 'br': me.paddingFactorX = -1; me.paddingFactorY = -1; me.siblingAlignment = "br-br"; if (me.useXAxis) { me.managerAlignment = "bl-br"; } else { me.managerAlignment = "tr-br"; } break; case 'bl': me.paddingFactorX = 1; me.paddingFactorY = -1; me.siblingAlignment = "bl-bl"; if (me.useXAxis) { me.managerAlignment = "br-bl"; } else { me.managerAlignment = "tl-bl"; } break; case 'tr': me.paddingFactorX = -1; me.paddingFactorY = 1; me.siblingAlignment = "tr-tr"; if (me.useXAxis) { me.managerAlignment = "tl-tr"; } else { me.managerAlignment = "br-tr"; } break; case 'tl': me.paddingFactorX = 1; me.paddingFactorY = 1; me.siblingAlignment = "tl-tl"; if (me.useXAxis) { me.managerAlignment = "tr-tl"; } else { me.managerAlignment = "bl-tl"; } break; case 'b': me.paddingFactorX = 0; me.paddingFactorY = -1; me.siblingAlignment = "b-b"; me.useXAxis = 0; me.managerAlignment = "t-b"; break; case 't': me.paddingFactorX = 0; me.paddingFactorY = 1; me.siblingAlignment = "t-t"; me.useXAxis = 0; me.managerAlignment = "b-t"; break; case 'l': me.paddingFactorX = 1; me.paddingFactorY = 0; me.siblingAlignment = "l-l"; me.useXAxis = 1; me.managerAlignment = "r-l"; break; case 'r': me.paddingFactorX = -1; me.paddingFactorY = 0; me.siblingAlignment = "r-r"; me.useXAxis = 1; me.managerAlignment = "l-r"; break; } if (typeof me.manager == 'string') { me.manager = Ext.getCmp(me.manager); } // If no manager is provided or found, then the static object is used and the el property pointed to the body document. if (!me.manager) { me.manager = me.statics().defaultManager; if (!me.manager.el) { me.manager.el = Ext.getBody(); } } if (typeof me.manager.notifications == 'undefined') { me.manager.notifications = {}; } if (me.stickOnClick) { if (me.body && me.body.dom) { Ext.fly(me.body.dom).on('click', function () { me.cancelAutoHide(); me.addCls('notification-fixed'); }, me); } } me.el.hover( function () { me.mouseIsOver = true; }, function () { me.mouseIsOver = false; }, me ); if (me.autoHide) { me.task = new Ext.util.DelayedTask(me.doAutoHide, me); me.task.delay(me.autoHideDelay); } var notifications = me.getNotifications(me.managerAlignment); if (notifications.length) { me.el.alignTo(notifications[notifications.length - 1].el, me.siblingAlignment, [0, 0]); me.xPos = me.getXposAlignedToSibling(notifications[notifications.length - 1]); me.yPos = me.getYposAlignedToSibling(notifications[notifications.length - 1]); } else { me.el.alignTo(me.manager.el, me.managerAlignment, [(me.paddingX * me.paddingFactorX), (me.paddingY * me.paddingFactorY)]); me.xPos = me.getXposAlignedToManager(); me.yPos = me.getYposAlignedToManager(); } Ext.Array.include(notifications, me); me.stopAnimation(); me.el.animate({ to: { x: me.xPos, y: me.yPos, opacity: 1 }, easing: me.slideInAnimation, duration: me.slideInDuration, dynamic: true }); }, slideBack: function () { var me = this; var notifications = me.getNotifications(me.managerAlignment); var index = Ext.Array.indexOf(notifications, me) // Not animating the element if it already started to hide itself or if the manager is not present in the dom if (!me.isHiding && me.el && me.manager && me.manager.el && me.manager.el.dom && me.manager.el.isVisible()) { if (index) { me.xPos = me.getXposAlignedToSibling(notifications[index - 1]); me.yPos = me.getYposAlignedToSibling(notifications[index - 1]); } else { me.xPos = me.getXposAlignedToManager(); me.yPos = me.getYposAlignedToManager(); } me.stopAnimation(); me.el.animate({ to: { x: me.xPos, y: me.yPos }, easing: me.slideBackAnimation, duration: me.slideBackDuration, dynamic: true }); } }, cancelAutoHide: function() { var me = this; if (me.autoHide) { me.task.cancel(); me.autoHide = false; } }, doAutoHide: function () { var me = this; /* Delayed hiding when mouse leaves the component. Doing this before me.mouseIsOver is checked below to avoid a race condition while resetting event handlers */ me.el.hover( function () { }, function () { me.hide(); }, me ); if (!(me.stickWhileHover && me.mouseIsOver)) { // Hide immediately me.hide(); } }, hide: function () { var me = this; // Avoids restarting the last animation on an element already underway with its hide animation if (!me.isHiding && me.el) { me.isHiding = true; me.cancelAutoHide(); me.stopAnimation(); me.el.animate({ to: { opacity: 0 }, easing: 'easeIn', duration: me.hideDuration, dynamic: false, listeners: { afteranimate: function () { if (me.manager) { var notifications = me.getNotifications(me.managerAlignment); var index = Ext.Array.indexOf(notifications, me); if (index != -1) { Ext.Array.erase(notifications, index, 1); // Slide "down" all notifications "above" the hidden one for (;index < notifications.length; index++) { notifications[index].slideBack(); } } } me.readyToHide = true; me.hide(); } } }); } // Calling parents hide function to complete hiding if (me.readyToHide) { me.isHiding = false; me.readyToHide = false; me.removeCls('notification-fixed'); this.callParent(arguments); } } }); /* Changelog: * * 2011-09-01 - 1.1: Bugfix. Array.indexOf not universally implemented, causing errors in IE<=8. Replaced with Ext.Array.indexOf. * 2011-09-12 - 1.2: Added config options: stickOnClick and stickWhileHover. * 2011-09-13 - 1.3: Cleaned up component destruction. * 2012-03-06 - 2.0: Renamed some properties ending with "Delay" to the more correct: "Duration". * Moved the hiding animation out of destruction and into hide. * Renamed the corresponding "destroy" properties to "hide". * (Hpsam) Changed addClass to addCls. * (Hpsam) Avoiding setting 'notification-fixed' when auto hiding. * (Justmyhobby) Using separate arrays to enable managers to mix alignments. * (Kreeve_ctisn) Removed default title. * (Jmaia) Center of edges can be used for positioning. Renamed corner property to position. * (Hpsam) Hiding or destroying manager does not cause errors. */