/**
* @author Wiktor Olejniczak <myfrom.13th@gmail.com>
* @license MIT
*
* @module notifier
*/
if (!document) throw new Error('Notifier can\'t run without document object.');
// Read global options to local const
const options = window.NotifierOptions || {};
/**
* Elements that are required for full Notifier functionality
*
* @constant
*/
const elementsToImport = [
'@polymer/paper-dialog/paper-dialog.js', '@polymer/paper-dialog-scrollable/paper-dialog-scrollable.js',
'@polymer/paper-button/paper-button.js', '@polymer/paper-toast/paper-toast.js',
'web-animations-js/web-animations.min.js', '@polymer/neon-animation/animations/fade-in-animation.js',
'@polymer/neon-animation/animations/fade-out-animation.js', '@polymer/neon-animation/animations/slide-from-bottom-animation.js',
'@polymer/neon-animation/animations/slide-down-animation.js'
]
const loadingImports = [];
if (!options.elementsImported) {
elementsToImport.forEach(url => {
loadingImports.push(import(url));
});
}
if (!options.stylesLoaded)
loadingImports.push(import('./styles-loader.js'));
const mobileMediaQuery = options.mobileMediaQuery ||
['(orientation: landscape) and (max-width: 960px)',
'(orientation: portrait) and (max-width: 600px)'];
/**
* Main class. It contains all functions and manages paper-dialog and paper-toast elements currently on the page.
* You don't have to worry about multiple instances
*
* @class
* @demo demo/demo.html
*/
class Notifier {
/**
* Get toast element or create one if needed
*
* @returns {Element} Toast element
* @throws This will throw if paper-toast element isn't imported
*/
get toast() {
if (!customElements.get('paper-toast'))
throw new Error('You must import paper-toast for Notifier.showToast functionality to work');
if (this._toast)
return this._toast;
else {
const toastSearchRes = document.querySelector('paper-toast.notifier');
if (toastSearchRes) {
return this._toast = toastSearchRes
} else {
const toastEl = document.createElement('paper-toast');
document.body.appendChild(toastEl);
return this._toast = toastEl;
}
}
}
/**
* Get dialog element or create one if needed
*
* @returns {Element} Dialog element
* @throws This will throw if paper-dialog element isn't imported
*/
get dialog() {
if (!customElements.get('paper-dialog'))
throw new Error('You must import paper-dialog for Notifier.showDialog functionality to work');
if (this._dialog)
return this._dialog;
else {
const dialogSearchRes = document.querySelector('paper-dialog.notifier');
if (dialogSearchRes) {
return this._dialog = dialogSearchRes
} else {
const dialogEl = document.createElement('paper-dialog');
document.body.appendChild(dialogEl);
return this._dialog = dialogEl;
}
}
}
/**
* @throws This will throw if run in non-browser environment
*/
constructor() {
// Check for window object
if (typeof window === 'undefined')
throw new Error('Notifier can\'t be run in non-browser environment');
// Add shortcut for layout
window.addEventListener('resize', e => {
this._mobile = window.matchMedia(mobileMediaQuery).matches;
});
this._mobile = window.matchMedia(mobileMediaQuery).matches;
// Define Material animation for dialogs
// This is a part related to neon-animation and will be removed in future
/** @constant */
this.MATERIAL_DIALOG_ANIMATION = {
'entry': [
{
'name': 'slide-from-bottom-animation',
'node': this.dialog,
'timing': {
'duration': 160,
'easing': 'ease-out'
}
},
{
'name': 'fade-in-animation',
'node': this.dialog,
'timing': {
'duration': 160,
'easing': 'ease-out'
}
}
],
'exit': [
{
'name': 'slide-down-animation',
'node': this.dialog,
'timing': {
'duration': 160,
'easing': 'ease-in'
}
},
{
'name': 'fade-out-animation',
'node': this.dialog,
'timing': {
'duration': 160,
'easing': 'ease-in'
}
}
]
}
}
/**
* Opens a toast with provided message
* @param {string} msg Message to be shown
* @param {object} [options]
* @param {string} [options.btnText] Text on paper button, leave empty to not show
* @param {EventListener} [options.btnFunction] Function to be called when button is pressed
* @param {number} [options.duration = 3000] Time in milliseconds before dialog will close, set to 0 to only allow manual close
* @param {object} [options.attributes] Attributes to be passed down to the dialog, { attr: value }
* @throws This will throw if `msg` is empty
*/
async showToast(msg, options = {}) {
await Promise.all(loadingImports);
if (!msg) throw new Error('Provided empty toast message');
if (!options.attributes) options.attributes = [];
const toast = this.toast;
if (toast.opened) toast.close();
toast.innerHTML = options.btnText ? `<paper-button>${options.btnText}</paper-button>` : null;
for (let i = 0; i < toast.attributes.length; i++) {
const attrName = toast.attributes[i].name;
if (options.attributes[attrName])
toast.setAttribute(attrName, options.attributes[attrName]);
else
toast.removeAttribute(attrName);
}
Object.keys(options.attributes).forEach(attrName => {
toast.setAttribute(attrName, options.attributes[attrName]);
});
toast.classList.toggle('fit-bottom', this._mobile);
toast.text = msg;
toast.duration = String(typeof options.duration).toLowerCase() === 'number' ? options.duration : 3000;
if (options.btnText && options.btnFunction) {
toast.querySelector('paper-button').addEventListener('tap', options.btnFunction);
}
if (options.btnText) {
const btnWidth = toast.querySelector('paper-button').getBoundingClientRect().width;
toast.style.paddingRight = btnWidth + 48 + 'px';
}
toast.classList.add('notifier');
toast.open();
}
/**
* Opens a dialog
* @param {string} header Header of the dialog
* @param {string} content Content of the dialog, must be a string with all tags, including bottom buttons
* @param {object} [options]
* @param {object} [options.attributes] Attributes to be passed down to the dialog, { attr: value }
* @param {boolean} [options.noBackdrop] Don't show backdrop behind dialog
* @param {boolean} [options.formatted=false] If true,`content` will be put directly into element, otherwise put inside `<paper-scrollable-dialog>`
* @param {Element} [options.target=window] Target element on which dialog will be appended
* @param {function} [options.beforeClose] Function to be run after accepting but before removing the dialog, if set promise will resolve with it's resoluts
* @param {object} [options.animationConfig] animationConfig on dialog element, if unset will default to Material animation
* @returns {Promise} A promise that will resolve if dialog was accepted or reject with `error: false` when cancelled
*/
showDialog(header, content, options = {}) {
return new Promise((resolve, reject) => {
Promise.all(loadingImports).then(() => {
if (!options.attributes) options.attributes = [];
const dialog = this.dialog;
if (dialog.opened) dialog.close();
const target = options.target || document.body;
if (dialog.parentElement !== target) target.appendChild(dialog);
const innerHTML =
(header ? `<h2>${header}</h2>` : '') +
(options.formatted ? content : `<paper-dialog-scrollable>${content}</paper-dialog-scrollable>`);
if ('ShadyDOM' in window && ShadyDOM.inUse) {
const template = document.createElement('template');
template.innerHTML = innerHTML;
dialog.innerHTML = '';
dialog.appendChild(document.importNode(template.content, true));
} else
dialog.innerHTML = innerHTML;
for (let i = 0; i < dialog.attributes.length; i++) {
const attrName = dialog.attributes[i].name;
if (attrName === 'animation-config' && dialog.animationConfig === this.MATERIAL_DIALOG_ANIMATION)
continue;
else if (options.attributes[attrName])
dialog.setAttribute(attrName, options.attributes[attrName]);
else
dialog.removeAttribute(attrName);
}
Object.keys(options.attributes).forEach(attrName => {
dialog.setAttribute(attrName, options.attributes[attrName]);
});
if (!dialog.animationConfig) {
dialog.animationConfig = this.MATERIAL_DIALOG_ANIMATION;
}
if (!dialog.withBackdrop && !options.noBackdrop) {
dialog.withBackdrop = true;
}
const closedHandler = e => {
if (e.target !== dialog) return;
if (e.detail.confirmed)
resolve(options.beforeClose && options.beforeClose(e));
else
reject({ error: false });
dialog.removeEventListener('iron-overlay-closed', closedHandler);
};
dialog.addEventListener('iron-overlay-closed', closedHandler);
dialog.classList.add('notifier');
dialog.open();
});
});
}
/**
* Predefined dialog with a yes/no question
* @param {String} [msg='Are you sure?'] Header text
* @param {Object} [options]
* @param {String} [options.innerMsg] Massage in dialog body
* @param {String} [options.cancelText='No'] Text to show in cancel button
* @param {String} [options.acceptText='Yes'] Text to show in accept button
* @param {object} [options.attributes] Attributes to be passed down to the dialog, { attr: value }
* @param {boolean} [options.noBackdrop] Don't show backdrop behind dialog
* @param {boolean} [options.formatted=false] If true,`content` will be put directly into element, otherwise put inside `<paper-scrollable-dialog>`
* @param {Element} [options.target=window] Target element on which dialog will be appended
* @param {function} [options.beforeClose] Function to be run after accepting but before removing the dialog, if set returned promise will resolve with it's results
* @param {object} [options.animationConfig] animationConfig on dialog element, if unset will default to Material animation
* @returns {Promise} A promise that will resolve if dialog was accepted or reject with `error: false` when cancelled
*/
askDialog(msg = 'Are you sure?', options = {}) {
const innerMsg = options.innerMsg || '',
cancelText = options.cancelText || 'No',
acceptText = options.acceptText || 'Yes',
content = `
<paper-dialog-scrollable>
${innerMsg}
</paper-dialog-scrollable>
<div class="buttons">
<paper-button dialog-dismiss>${cancelText}</paper-button>
<paper-button dialog-confirm autofocus>${acceptText}</paper-button>
</div>
`;
options.formatted = true;
return this.showDialog(msg, content, options);
}
}
/** Initialised Notifier class */
export default new Notifier();
export { elementsToImport };