/*
* 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} hideDuration
* The number of milliseconds it takes for a Toast to hide.
*/
hideDuration: 500,
/**
* @cfg {Number} autoCloseDelay
* 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,
/**
* @property focusable
* @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;
};
});