notus
Version:
Yet another pop-up notification library for the web, strictly without jQuery
584 lines (480 loc) • 20.7 kB
JavaScript
/**
* notus v0.3.1
*
* Author: Kushal Pandya <kushalspandya@gmail.com> (https://doublslash.com)
* Date: 12 February, 2016
*
* Main Notus Script.
*/
(function() {
"use strict";
var self = this,
hasRequire = (typeof require !== 'undefined'),
old_notus = self.notus,
_n = {},
notus;
var htmlEntityMap = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
"'": ''',
'/': '/',
'`': '`',
'=': '='
},
notusTypeMap = {
'popup': 'popup',
'toast': 'toast',
'snackbar': 'snackbar'
},
positionShorts = {
'top-left': 'tl',
'top-right': 'tr',
'bottom-left': 'bl',
'bottom-right': 'br',
'top': 'top',
'bottom': 'bottom',
'tl': 'tl',
'tr': 'tr',
'bl': 'bl',
'br': 'br',
't': 'top',
'b': 'bottom'
},
positionForType = {
'popup': ['tl', 'tr', 'bl', 'br'],
'toast': ['top', 'bottom'],
'snackbar': ['top', 'bottom'],
},
alertTypeMap = {
'success': 'success',
'failure': 'failure',
'warning': 'warning',
'custom': 'custom',
'none': ''
},
animationTypeMap = {
'slide': 'slide',
'fade': 'fade',
'custom': 'custom'
};
var fnValidateConfig,
fnGetParentClassList,
fnCreateNotusContainer,
fnGetEntryAnimatorStyle,
fnGetExitAnimatorStyle,
fnBindCloseHandler,
fnBindCloseListener,
fnBindActionHandler,
fnCreateNotusEl;
/**
* Local Utility API.
*/
_n = {
/**
* Generate timestamp based unique ID.
*/
genId: function() {
return 'notus-' + Math.floor(Math.random() * 10000000000000001);
},
/**
* Simple Object extender (similar to jQuery's $.extend()).
* courtesy; SO < http://stackoverflow.com/a/11197343/414749 >
*/
extend: function() {
var key, i;
for (i = 1; i < arguments.length; i++)
{
for (key in arguments[i])
{
if (arguments[i].hasOwnProperty(key))
arguments[0][key] = arguments[i][key];
}
}
return arguments[0];
},
/**
* String formatter.
* Accepts first param as target string, and following params as strings to
* replace placeholders (eg; {0}).
* courtesy: SO <http://stackoverflow.com/a/1038930/414749>
*/
format: function() {
var str = arguments[0],
reg,
i;
for (i = 0; i < arguments.length - 1; i++) {
reg = new RegExp("\\{" + i + "\\}", "gm");
str = str.replace(reg, arguments[i + 1]);
}
return str;
},
/**
* Escapes HTML tags present in the string.
* courtesy: Mustache.js < https://github.com/janl/mustache.js >
*/
escapeHtml: function(string) {
return String(string).replace(/[&<>"'`=\/]/g, function fromEntityMap (s) {
return htmlEntityMap[s];
});
},
/**
* Removes an Element from the DOM.
* This method uses el.remove() internally in supported browsers
* except for IE11, where it is using el.parent.removeChild().
* courtesy: MDN < https://developer.mozilla.org/en-US/docs/Web/API/ChildNode/remove >
*/
removeEl: function(el) {
if (typeof el.remove === 'function')
el.remove();
else if (el.parentElement)
el.parentElement.removeChild(el);
}
};
/** Helpers Begin **/
/**
* Validate user configuration and throw Errors accordingly.
*/
fnValidateConfig = function(config) {
var type = config.notusType,
position = config.notusPosition;
if (notusTypeMap[type] === undefined)
throw new Error('Unknown value for notusType');
if (positionShorts[position] === undefined)
throw new Error('Unknown value for notusPosition');
if (alertTypeMap[config.alertType] === undefined)
throw new Error('Unknown value for alertType');
if (animationTypeMap[config.animationType] === undefined)
throw new Error('Unknown value for animationType');
if (config.message === undefined)
throw new Error('Unknown value for message');
if (positionForType[type].indexOf(positionShorts[position]) < 0)
throw new Error('Unsupported position "' + position + '" for notusType "' + type + '"');
if (config.closeHandler &&
typeof config.closeHandler !== "function")
throw new Error("closeHandler is not a function");
if (config.actionable &&
(typeof config.primaryAction !== "object" && typeof config.secondaryAction !== "object"))
throw new Error("primaryAction or secondaryAction undefined for actionable notus");
};
/**
* Creates list of CSS classes which are to be applied main Notus element based on config.
*/
fnGetParentClassList = function(config) {
var classList = [],
type = config.notusType,
alertType = config.alertType;
classList.push('notus');
classList.push('notus-type-' + notusTypeMap[type]);
if (alertTypeMap[alertType])
classList.push('notus-alert-' + alertTypeMap[alertType]);
if (config.themeClass)
classList.push(config.themeClass);
return classList;
};
/**
* Creates new (or returns existing) Notus container elements to house Notus for a particular position.
* Note that it creates containers for all position types that Notus supports, and it will return
* reference to an existing container if it was previously created.
*/
fnCreateNotusContainer = function(config) {
var type = config.notusType,
position = config.notusPosition,
containerCls = ['notus-container'],
containerEl;
containerCls.push('notus-position-' + positionShorts[position]);
if (type !== 'popup')
containerCls.push((type === 'toast') ? 'notus-container-toast' : 'notus-container-snackbar');
containerEl = document.querySelector('.' + containerCls.join('.'));
if (!containerEl)
{
containerEl = document.createElement('div');
containerEl.setAttribute('class', containerCls.join(' '));
document.body.appendChild(containerEl);
}
return containerEl;
};
/**
* Binds Timer event on each Notus that has autoClose set to true.
* the timer waits for autoCloseDuration to finish before closing & destroying Notus.
*/
fnBindCloseListener = function(config, notusEl) {
var type = config.notusType,
animationType = config.animationType,
animationClass = config.animationClass;
setTimeout(function() {
notusEl.setAttribute('style', fnGetExitAnimatorStyle(config).join(';'));
if (animationType === 'slide')
{
notusEl.classList.remove('notus-slide-in');
notusEl.classList.add('notus-slide-out');
}
else if (animationType === 'fade')
{
notusEl.classList.remove('notus-fade-in');
notusEl.classList.add('notus-fade-out');
}
else
{
notusEl.classList.remove(animationClass.extry);
notusEl.classList.add(animationClass.exit);
}
setTimeout(function() {
_n.removeEl(notusEl);
}, config.animationDuration);
}, config.autoCloseDuration);
};
/**
* Binds Mouse Click event handler on Close element of Notus.
* it optionally supports closeHandler function that gets called before Notus is destroyed.
* if closeHandler returns false (boolean false), Notus will be prevented from destroying.
*/
fnBindCloseHandler = function(config, notusEl) {
var closeEl = notusEl.querySelector('.notus-close');
closeEl.onclick = function(e) {
var persist;
if (config.closeHandler)
persist = config.closeHandler.apply(this, arguments);
if (!persist)
_n.removeEl(notusEl);
};
};
/**
* Binds Mouse Click event handler on action element of Notus type Snackbar.
*/
fnBindActionHandler = function(config, notusEl) {
var primaryActionEl = notusEl.querySelector('.notus-actions > .action-item.action-primary'),
secondaryActionEl = notusEl.querySelector('.notus-actions > .action-item.action-secondary'),
getActionHandler;
getActionHandler = function(actionConfig) {
return function(e) {
var persist = actionConfig.actionHandler.apply(this, arguments);
if (!persist)
_n.removeEl(notusEl);
};
};
if (primaryActionEl)
primaryActionEl.onclick = getActionHandler(config.primaryAction);
if (secondaryActionEl)
secondaryActionEl.onclick = getActionHandler(config.secondaryAction);
};
/**
* Updates and applies Entry Animation CSS styles and Classes of Notus Element based on config.
*/
fnGetEntryAnimatorStyle = function(config) {
var type = config.notusType,
animationType = config.animationType,
animationFunction = config.animationFunction,
animationDuration = config.animationDuration / 1000,
isSlide = animationType === 'slide',
animators = [];
if (animationType !== 'custom')
{
if (type === 'popup')
animators.push(isSlide ? 'transform: translateX(0%)' : 'opacity: 1');
else
animators.push(isSlide ? 'transform: translateY(0%)' : 'opacity: 1');
if (animationFunction)
animators.push(_n.format('animation-timing-function: {0}', animationFunction));
}
if (animationDuration > 0)
animators.push(_n.format('animation-duration: {0}s', animationDuration));
return animators;
};
/**
* Updates and applies Exit Animation CSS styles and Classes of Notus Element based on config.
*/
fnGetExitAnimatorStyle = function(config) {
var type = config.notusType,
position = config.notusPosition,
animationType = config.animationType,
animationFunction = config.animationFunction,
animationDuration = config.animationDuration / 1000,
isSlide = animationType === 'slide',
animators = [];
if (animationType !== 'custom')
{
if (type === 'popup')
{
if (positionShorts[position].indexOf('l') > -1)
animators.push(isSlide ? 'transform: translateX(-110%)' : 'opacity: 0');
else
animators.push(isSlide ? 'transform: translateX(110%)' : 'opacity: 0');
}
else
{
if (positionShorts[position] === 'top')
animators.push(isSlide ? 'transform: translateY(-110%)' : 'opacity: 0');
else
animators.push(isSlide ? 'transform: translateY(110%)' : 'opacity: 0');
}
if (animationFunction)
animators.push(_n.format('animation-timing-function: {0}', animationFunction));
}
if (animationDuration > 0)
animators.push(_n.format('animation-duration: {0}s', animationDuration));
return animators;
};
/**
* Creates main Notus DOM Element based with provided config.
*/
fnCreateNotusEl = function(config) {
var parentDiv = document.createElement('div'),
animationType = config.animationType,
animationClass = config.animationClass,
isSlide = animationType === 'slide',
htmlStringSupported = config.htmlString,
classList = [],
notusElTpl = '',
notusTitleElTpl = '',
primaryActionElTpl = '',
secondaryActionElTpl = '',
actionElTpl = '',
closeElTpl = '';
classList = fnGetParentClassList(config);
if (config.animate)
{
if (animationType !== 'custom')
{
classList.push(isSlide ? 'notus-slide' : 'notus-fade');
classList.push(isSlide ? 'notus-slide-in' : 'notus-fade-in');
}
else
{
classList.push(animationClass.fixed);
classList.push(animationClass.entry);
}
parentDiv.setAttribute('style', fnGetEntryAnimatorStyle(config).join(';'));
}
parentDiv.setAttribute('id', _n.genId());
parentDiv.setAttribute('class', classList.join(' '));
if (config.closable)
{
closeElTpl = [
'<div class="notus-body-item notus-close">',
'<span class="icon-close">×</span>',
'</div>'
].join('');
}
if (config.actionable)
{
if (config.primaryAction)
primaryActionElTpl = _n.format('<span class="action-item action-primary">{0}</span>', htmlStringSupported ? config.primaryAction.text : _n.escapeHtml(config.primaryAction.text));
if (config.secondaryAction)
secondaryActionElTpl = _n.format('<span class="action-item action-secondary">{0}</span>', htmlStringSupported ? config.secondaryAction.text : _n.escapeHtml(config.secondaryAction.text));
actionElTpl = [
'<div class="notus-body-item notus-actions">',
primaryActionElTpl,
secondaryActionElTpl,
'</div>'
].join('');
}
if (config.notusType !== 'snackbar')
notusTitleElTpl = '<div class="notus-content-title">{0}</div>';
notusElTpl = [
'<div class="notus-body-item notus-content">',
notusTitleElTpl,
'<div class="notus-content-body">',
'{1}',
'</div>',
'</div>',
closeElTpl,
actionElTpl
].join('');
parentDiv.innerHTML = _n.format(notusElTpl,
htmlStringSupported ? config.title : _n.escapeHtml(config.title),
htmlStringSupported ? config.message : _n.escapeHtml(config.message)
);
return parentDiv;
};
/** Helpers End **/
/** Main Notus Object Begin **/
notus = self.notus = function(userConfig) {
var bodyEL = document.body,
defaultConfig = {},
thisNotus = {};
/** Default Config options that Notus provides **/
defaultConfig = {
notusType: 'popup', /* Type can be anything from; 'popup', 'toast' or 'snackbar' */
notusPosition: 'top-right', /* Available positions for different notus types;
'popup' => 'top-left' ('tl'), 'bottom-left' ('bl'),
'top-right' ('tr') or 'bottom-right' ('br')
'toast' & 'snackbar' => 'top' ('t') or 'bottom' ('b') */
alertType: 'none', /* Alert type can be; 'none', 'success', 'failure' or 'warning' */
htmlString: false, /* Enable HTML support in strings provided for 'title', 'message' & 'text' within actions,
this is unsafe and hence, it is false by default */
closable: true, /* Show close button to close Notus */
autoClose: true, /* Automatically close Notus once autoCloseDuration completes */
autoCloseDuration: 3000, /* Milliseconds to wait before closing */
animate: true, /* Animate while showing/hiding Notus */
animationType: 'slide', /* Animation Type while showing/hiding Notus; it can be 'slide', 'fade' or 'custom'
Using 'custom' exposes an additional config animationClass which can be used
to provide animations using animate.css < https://github.com/daneden/animate.css > */
animationDuration: 300, /* Animation Duration to apply while showing/hiding Notus,
it supports values in milliseconds.
which is then passed to CSS animation-duration */
animationFunction: 'ease-out', /* Animation Timing Function to use while showing/hiding Notus,
it supports any value that CSS animation-timing-function supports,
including cubic-bezier() & steps()
value is then passed to CSS animation-timing-function */
animationClass: {
fixed: '',
entry: '',
exit: ''
}, /* This config is only applicable if animationType is 'custom'
Using this config, you can provide custom 'extry' & 'exit' classes to control
animations (exit animation only occurs if Notus auto-closes itself)
with this config, while providing 'fixed' (that stays applied always) class to control your overrides,
you can also use external library like animate.css */
themeClass: 'notus-material-light' /* Provide custom CSS class that you want to apply on Parent element of Notus */
};
userConfig = _n.extend(defaultConfig, userConfig);
/** Notus API begin **/
/**
* Resolve instance conflict in case module is loaded more than once.
*/
thisNotus.noConflict = function() {
self.notus = old_notus;
return notus;
};
/**
* Main Notus send() method to send supported type of notus Notifications.
*/
thisNotus.send = function(config) {
var containerEl,
notusEl;
config = _n.extend({}, userConfig, config);
fnValidateConfig(config);
containerEl = fnCreateNotusContainer(config);
notusEl = fnCreateNotusEl(config);
if (config.closable)
fnBindCloseHandler(config, notusEl);
if (config.actionable)
fnBindActionHandler(config, notusEl);
if (config.notusPosition.indexOf('bottom') > -1)
containerEl.insertBefore(notusEl, containerEl.firstChild);
else
containerEl.appendChild(notusEl);
if (config.autoClose)
fnBindCloseListener(config, notusEl);
return notusEl.getAttribute('id');
};
/** Notus API end **/
return thisNotus;
};
// UMD Definition < https://github.com/umdjs/umd >.
if (typeof exports !== 'undefined')
{
if (typeof module !== 'undefined' &&
module.exports)
{
exports = module.exports = notus;
}
exports.notus = notus;
}
else
{
self.notus = notus;
}
}).call(this);