/* * This class is a derived work from: * * Notification extension for Ext JS 4.0.2+ * Version: 2.1.3 * * Copyright (c) 2011 Eirik Lorentsen (http://www.eirik.net/) * * Follow project on GitHub: https://github.com/EirikLorentsen/Ext.ux.window.Notification * * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php) * and GPL (http://opensource.org/licenses/GPL-3.0) licenses. */ /** * This class provides for lightweight, auto-dismissing pop-up notifications called "toasts". * At the base level, you can display a toast message by calling `Ext.toast` like so: * * Ext.toast('Data saved'); * * This will result in a toast message, which displays in the default location at the top of your viewport. * * You may expand upon this simple example with the following parameters: * * Ext.toast(message, title, align, iconCls); * * For example, the following toast will appear top-middle in your viewport. It will display * the 'Data Saved' message with a title of 'Title' * * Ext.toast('Data Saved', 'Title', 't') * * It should be noted that the toast's width is determined by the message's width. * If you need to set a specific width, or any of the other available configurations for your toast, * you can create the toast object as seen below: * * Ext.toast({ * html: 'Data Saved', * title: 'My Title', * width: 200, * align: 't' * }); * * This component is derived from the excellent work of a Sencha community member, Eirik * Lorentsen. */Ext.define('Ext.window.Toast', { extend: 'Ext.window.Window', xtype: 'toast', isToast: true, /** * @cfg cls * @inheritdoc */ cls: Ext.baseCSSPrefix + 'toast', /** * @cfg bodyPadding * @inheritdoc */ bodyPadding: 10, /** * @cfg {Boolean} autoClose * This config ensures that the Toast is closed automatically after a certain amount of time. If this is set to * `false`, closing the Toast will have to be handled some other way (e.g., Setting `closable: true`). */ autoClose: true, /** * @cfg plain * @inheritdoc */ plain: false, /** * @cfg draggable * @inheritdoc */ draggable: false, /** * @cfg resizable * @inheritdoc */ resizable: false, /** * @cfg shadow * @inheritdoc */ shadow: false, focus: Ext.emptyFn, /** * @cfg {String/Ext.Component} [anchor] * The component or the `id` of the component to which the `toast` will be anchored. * The default behavior is to anchor a `toast` to the document body (no component). */ anchor: null, /** * @cfg {Boolean} [useXAxis] * Directs the toast message to animate on the x-axis (if `true`) or y-axis (if `false`). * This value defaults to a value based on the `align` config. */ useXAxis: false, /** * @cfg {"br"/"bl"/"tr"/"tl"/"t"/"l"/"b"/"r"} [align] * Specifies the basic alignment of the toast message with its {@link #anchor}. This * controls many aspects of the toast animation as well. For fine grain control of * the final placement of the toast and its `anchor` you may set * {@link #anchorAlign} as well. * * Possible values: * * - br - bottom-right * - bl - bottom-left * - tr - top-right * - tl - top-left * - t - top * - l - left * - b - bottom * - r - right */ align: 't', /** * @cfg alwaysOnTop * @inheritdoc */ alwaysOnTop: true, /** * @cfg {String} [anchorAlign] * This string is a full specification of how to position the toast with respect to * its `anchor`. This is set to a reasonable value based on `align` but the `align` * also sets defaults for various other properties. This config controls only the * final position of the toast. */ /** * @cfg {Boolean} [animate=true] * Set this to `false` to make toasts appear and disappear without animation. * This is helpful with applications' unit and integration testing. */ // Pixels between each notification /** * @cfg {Number} spacing * The number of pixels between each Toast notification. */ spacing: 6, //TODO There should be a way to control from and to positions for the introduction. //TODO The align/anchorAlign configs don't actually work as expected. // Pixels from the anchor's borders to start the first notification paddingX: 30, paddingY: 10, /** * @cfg {String} slideInAnimation * The animation used for the Toast to slide in. */ slideInAnimation: 'easeIn', /** * @cfg {String} slideBackAnimation * The animation used for the Toast to slide back. */ slideBackAnimation: 'bounceOut', /** * @cfg {Number} slideInDuration * The number of milliseconds it takes for a Toast to slide in. */ slideInDuration: 500, /** * @cfg {Number} slideBackDuration * The number of milliseconds it takes for a Toast to slide back. */ slideBackDuration: 500, /** * @cfg {Number} * The number of milliseconds it takes for a Toast to hide. */ hideDuration: 500, /** * @cfg {Number} * The number of milliseconds a Toast waits before automatically closing. */ autoCloseDelay: 3000, /** * @cfg {Boolean} [stickOnClick] * This config will prevent the Toast from closing when you click on it. If this is set to `true`, * closing the Toast will have to be handled some other way (e.g., Setting `closable: true`). */ stickOnClick: false, /** * @cfg {Boolean} [stickWhileHover] * This config will prevent the Toast from closing while you're hovered over it. */ stickWhileHover: true, /** * @cfg {Boolean} [closeOnMouseDown] * This config will prevent the Toast from closing when a user produces a mousedown event. */ closeOnMouseDown: false, /** * @cfg closable * @inheritdoc */ closable: false, /** * @cfg minHeight * @inheritdoc */ minHeight: 1, /** * @inheritdoc */ focusable: false, // Private. Do not override! isHiding: false, isFading: false, destroyAfterHide: false, closeOnMouseOut: false, // Caching coordinates to be able to align to final position of siblings being animated xPos: 0, yPos: 0, constructor: function(config) { config = config || {}; if (config.animate === undefined) { config.animate = Ext.isBoolean(this.animate) ? this.animate : Ext.enableFx; } this.enableAnimations = config.animate; delete config.animate; this.callParent([config]); }, initComponent: function() { var me = this; // Close tool is not really helpful to sight impaired users // when Toast window is set to auto-close on timeout; however // if it's forced, respect that. if (me.autoClose && me.closable == null) { me.closable = false; } me.updateAlignment(me.align); me.setAnchor(me.anchor); me.callParent(); }, onRender: function() { var me = this; me.callParent(arguments); me.el.hover(me.onMouseEnter, me.onMouseLeave, me); // Mousedown outside of this, when visible, hides it immediately if (me.closeOnMouseDown) { Ext.getDoc().on('mousedown', me.onDocumentMousedown, me); } }, /* * These properties are keyed by "align" and set defaults for various configs. */ alignmentProps: { br: { paddingFactorX: -1, paddingFactorY: -1, siblingAlignment: "br-br", anchorAlign: "tr-br" }, bl: { paddingFactorX: 1, paddingFactorY: -1, siblingAlignment: "bl-bl", anchorAlign: "tl-bl" }, tr: { paddingFactorX: -1, paddingFactorY: 1, siblingAlignment: "tr-tr", anchorAlign: "br-tr" }, tl: { paddingFactorX: 1, paddingFactorY: 1, siblingAlignment: "tl-tl", anchorAlign: "bl-tl" }, b: { paddingFactorX: 0, paddingFactorY: -1, siblingAlignment: "b-b", useXAxis: 0, anchorAlign: "t-b" }, t: { paddingFactorX: 0, paddingFactorY: 1, siblingAlignment: "t-t", useXAxis: 0, anchorAlign: "b-t" }, l: { paddingFactorX: 1, paddingFactorY: 0, siblingAlignment: "l-l", useXAxis: 1, anchorAlign: "r-l" }, r: { paddingFactorX: -1, paddingFactorY: 0, siblingAlignment: "r-r", useXAxis: 1, anchorAlign: "l-r" }, /* * These properties take priority over the above and applied only when useXAxis * is set to true. Again these are keyed by "align". */ x: { br: { anchorAlign: "bl-br" }, bl: { anchorAlign: "br-bl" }, tr: { anchorAlign: "tl-tr" }, tl: { anchorAlign: "tr-tl" } } }, updateAlignment: function (align) { var me = this, alignmentProps = me.alignmentProps, props = alignmentProps[align], xprops = alignmentProps.x[align]; if (xprops && me.useXAxis) { Ext.applyIf(me, xprops); } Ext.applyIf(me, props); }, getXposAlignedToAnchor: function () { var me = this, align = me.align, anchor = me.anchor, anchorEl = anchor && anchor.el, el = me.el, xPos = 0; // Avoid error messages if the anchor does not have a dom element if (anchorEl && anchorEl.dom) { if (!me.useXAxis) { // Element should already be aligned vertically xPos = el.getLeft(); } // Using getAnchorXY instead of getTop/getBottom should give a correct placement when document is used // as the anchor but is still 0 px high. Before rendering the viewport. else if (align === 'br' || align === 'tr' || align === 'r') { xPos += anchorEl.getAnchorXY('r')[0]; xPos -= (el.getWidth() + me.paddingX); } else { xPos += anchorEl.getAnchorXY('l')[0]; xPos += me.paddingX; } } return xPos; }, getYposAlignedToAnchor: function () { var me = this, align = me.align, anchor = me.anchor, anchorEl = anchor && anchor.el, el = me.el, yPos = 0; // Avoid error messages if the anchor does not have a dom element if (anchorEl && anchorEl.dom) { if (me.useXAxis) { // Element should already be aligned horizontally yPos = el.getTop(); } // Using getAnchorXY instead of getTop/getBottom should give a correct placement when document is used // as the anchor but is still 0 px high. Before rendering the viewport. else if (align === 'br' || align === 'bl' || align === 'b') { yPos += anchorEl.getAnchorXY('b')[1]; yPos -= (el.getHeight() + me.paddingY); } else { yPos += anchorEl.getAnchorXY('t')[1]; yPos += me.paddingY; } } return yPos; }, getXposAlignedToSibling: function (sibling) { var me = this, align = me.align, el = me.el, xPos; if (!me.useXAxis) { xPos = el.getLeft(); } else if (align === 'tl' || align === 'bl' || align === 'l') { // Using sibling's width when adding xPos = (sibling.xPos + sibling.el.getWidth() + sibling.spacing); } else { // Using own width when subtracting xPos = (sibling.xPos - el.getWidth() - me.spacing); } return xPos; }, getYposAlignedToSibling: function (sibling) { var me = this, align = me.align, el = me.el, yPos; if (me.useXAxis) { yPos = el.getTop(); } else if (align === 'tr' || align === 'tl' || align === 't') { // Using sibling's width when adding yPos = (sibling.yPos + sibling.el.getHeight() + sibling.spacing); } else { // Using own width when subtracting yPos = (sibling.yPos - el.getHeight() - sibling.spacing); } return yPos; }, getToasts: function () { var anchor = this.anchor, alignment = this.anchorAlign, activeToasts = anchor.activeToasts || (anchor.activeToasts = {}); return activeToasts[alignment] || (activeToasts[alignment] = []); }, setAnchor: function (anchor) { var me = this, Toast; me.anchor = anchor = ((typeof anchor === 'string') ? Ext.getCmp(anchor) : anchor); // If no anchor is provided or found, then the static object is used and the el // property pointed to the body document. if (!anchor) { Toast = Ext.window.Toast; me.anchor = Toast.bodyAnchor || (Toast.bodyAnchor = { el: Ext.getBody() }); } }, beforeShow: function () { var me = this; if (me.stickOnClick) { me.body.on('click', function () { me.cancelAutoClose(); }); } if (me.autoClose) { if (!me.closeTask) { me.closeTask = new Ext.util.DelayedTask(me.doAutoClose, me); } } // Shunting offscreen to avoid flicker me.el.setX(-10000); me.el.setOpacity(1); }, afterShow: function () { var me = this, el = me.el, activeToasts, sibling, length, xy; me.callParent(arguments); activeToasts = me.getToasts(); length = activeToasts.length; sibling = length && activeToasts[length - 1]; if (sibling) { el.alignTo(sibling.el, me.siblingAlignment, [0, 0]); me.xPos = me.getXposAlignedToSibling(sibling); me.yPos = me.getYposAlignedToSibling(sibling); } else { el.alignTo(me.anchor.el, me.anchorAlign, [ (me.paddingX * me.paddingFactorX), (me.paddingY * me.paddingFactorY) ], false); me.xPos = me.getXposAlignedToAnchor(); me.yPos = me.getYposAlignedToAnchor(); } Ext.Array.include(activeToasts, me); if (me.enableAnimations) { // Repeating from coordinates makes sure the windows does not flicker // into the center of the viewport during animation xy = el.getXY(); el.animate({ from: { x: xy[0], y: xy[1] }, to: { x: me.xPos, y: me.yPos, opacity: 1 }, easing: me.slideInAnimation, duration: me.slideInDuration, dynamic: true, callback: me.afterPositioned, scope: me }); } else { me.setLocalXY(me.xPos, me.yPos); me.afterPositioned(); } }, afterPositioned: function() { var me = this; // This method can be called from afteranimation event being fired // during destruction sequence. if (!me.destroying && !me.destroyed && me.autoClose) { me.closeTask.delay(me.autoCloseDelay); } }, onDocumentMousedown: function(e) { if (this.isVisible() && !this.owns(e.getTarget())) { this.hide(); } }, slideBack: function () { var me = this, anchor = me.anchor, anchorEl = anchor && anchor.el, el = me.el, activeToasts = me.getToasts(), index = Ext.Array.indexOf(activeToasts, me); // Not animating the element if it already started to hide itself or if the anchor is not present in the dom if (!me.isHiding && el && el.dom && anchorEl && anchorEl.isVisible()) { if (index) { me.xPos = me.getXposAlignedToSibling(activeToasts[index - 1]); me.yPos = me.getYposAlignedToSibling(activeToasts[index - 1]); } else { me.xPos = me.getXposAlignedToAnchor(); me.yPos = me.getYposAlignedToAnchor(); } me.stopAnimation(); if (me.enableAnimations) { el.animate({ to: { x: me.xPos, y: me.yPos }, easing: me.slideBackAnimation, duration: me.slideBackDuration, dynamic: true }); } } }, update: function () { var me = this; if (me.isVisible()) { me.isHiding = true; me.hide(); //TODO offer a way to just update and reposition after layout } me.callParent(arguments); me.show(); }, cancelAutoClose: function() { var closeTask = this.closeTask; if (closeTask) { closeTask.cancel(); } }, doAutoClose: function () { var me = this; if (!(me.stickWhileHover && me.mouseIsOver)) { // Close immediately me.close(); } else { // Delayed closing when mouse leaves the component. me.closeOnMouseOut = true; } }, doDestroy: function() { this.removeFromAnchor(); this.cancelAutoClose(); this.callParent(); }, onMouseEnter: function () { this.mouseIsOver = true; }, onMouseLeave: function () { var me = this; me.mouseIsOver = false; if (me.closeOnMouseOut) { me.closeOnMouseOut = false; me.close(); } }, removeFromAnchor: function () { var me = this, activeToasts, index; if (me.anchor) { activeToasts = me.getToasts(); index = Ext.Array.indexOf(activeToasts, me); if (index !== -1) { Ext.Array.erase(activeToasts, index, 1); // Slide "down" all activeToasts "above" the hidden one for (;index < activeToasts.length; index++) { activeToasts[index].slideBack(); } } } }, getFocusEl: Ext.emptyFn, hide: function () { var me = this, el = me.el; me.cancelAutoClose(); if (me.isHiding) { if (!me.isFading) { me.callParent(arguments); me.isHiding = false; } } else { // Must be set right away in case of double clicks on the close button me.isHiding = true; me.isFading = true; me.cancelAutoClose(); if (el) { if (me.enableAnimations && !me.destroying && !me.destroyed) { el.fadeOut({ opacity: 0, easing: 'easeIn', duration: me.hideDuration, listeners: { scope: me, afteranimate: function() { var me = this; me.isFading = false; if (!me.destroying && !me.destroyed) { me.hide(me.animateTarget, me.doClose, me); } } } }); } else { me.isFading = false; me.hide(me.animateTarget, me.doClose, me); } } } return me; }},function (Toast) { Ext.toast = function (message, title, align, iconCls) { var config = message, toast; if (Ext.isString(message)) { config = { title: title, html: message, iconCls: iconCls }; if (align) { config.align = align; } } toast = new Toast(config); toast.show(); return toast; };});