linuxwindowsinboxwhatsappicloudtweetdeckhipchattelegramhangoutsslackgmailskypefacebook-workplaceoutlookemailmicrosoft-teamsdiscordmessengercustom-servicesmacos
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
622 lines
18 KiB
622 lines
18 KiB
/* |
|
* 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 of bottom right in 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, |
|
|
|
cls: Ext.baseCSSPrefix + 'toast', |
|
|
|
bodyPadding: 10, |
|
autoClose: true, |
|
plain: false, |
|
draggable: false, |
|
resizable: false, |
|
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="br"] |
|
* 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: 'br', |
|
|
|
/** |
|
* @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. |
|
*/ |
|
animate: true, |
|
|
|
// Pixels between each 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, |
|
|
|
slideInAnimation: 'easeIn', |
|
slideBackAnimation: 'bounceOut', |
|
slideInDuration: 1500, |
|
slideBackDuration: 1000, |
|
hideDuration: 500, |
|
autoCloseDelay: 3000, |
|
stickOnClick: true, |
|
stickWhileHover: true, |
|
closeOnMouseDown: 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, |
|
|
|
initComponent: function() { |
|
var me = this; |
|
|
|
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); |
|
} |
|
me.closeTask.delay(me.autoCloseDelay); |
|
} |
|
|
|
// 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.animate) { |
|
// 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 |
|
}); |
|
} |
|
else { |
|
me.setLocalXY(me.xPos, me.yPos); |
|
} |
|
}, |
|
|
|
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.animate) { |
|
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; |
|
} |
|
}, |
|
|
|
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); |
|
// Must come after callParent() since it will pass through hide() again triggered by destroy() |
|
me.removeFromAnchor(); |
|
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.animate) { |
|
el.fadeOut({ |
|
opacity: 0, |
|
easing: 'easeIn', |
|
duration: me.hideDuration, |
|
listeners: { |
|
afteranimate: function () { |
|
me.isFading = false; |
|
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; |
|
}; |
|
});
|
|
|