@knighttower/toast
Version:
A dead simple toast library
556 lines (497 loc) • 18.7 kB
JavaScript
;
Object.defineProperty(exports, '__esModule', { value: true });
// // -----------------------------------------
// /**
// * @knighttower
// * @url knighttower.io
// * @git https://github.com/knighttower/
// */
// // -----------------------------------------
// -----------------------------------------
/**
* Checks if the value is a plain object.
*
* @param {any} value The value to check.
* @returns {boolean} True if the value is a plain object, false otherwise.
*/
const isPlainObject = (value) =>
value && typeof value === 'object' && value.constructor === Object;
/**
* Merges two or more objects into a single new object. Arrays and other types are overwritten.
*
* @param {object} target The target object.
* @param {...object} sources The source objects.
* @returns {object} A new merged object.
*/
const extend = (target, ...sources) => {
return sources.reduce(
(acc, source) => {
if (isPlainObject(source)) {
Object.entries(source).forEach(([key, value]) => {
acc[key] =
isPlainObject(value) && isPlainObject(acc[key])
? extend(acc[key], value)
: value; // If it's not an object, directly assign it
});
} else {
// If the source is not an object (like a number), just merge directly
Object.assign(acc, source);
}
return acc;
},
{ ...target }
);
};
// // utility; {
// convertToBool,
// currencyToDecimal,
// convertToNumber,
// dateFormat,
// decimalToCurrency,
// emptyOrValue,
// extend,
// formatPhoneNumber,
// getDynamicId,
// getGoogleMapsAddress,
// getRandomId,
// includes,
// isEmpty, // from https://moderndash.io/
// isNumber,
// instanceOf,
// openGoogleMapsAddress,
// toCurrency,
// toDollarString,
// typeOf,
// validateEmail,
// validatePhone,
// makeArray,
// uuid,
// uniqueId,
// };
/**
* x-toast.js
* A dead simple toast notification library for JavaScript.
* @version 1.0.0
* @license MIT
* @methods info, success, warning, error, config, transition
* @method info - show an info toast message.
* @method success - show a success toast message.
* @method warning - show a warning toast message.
* @method error - show an error toast message.
* @method config - set the default configuration options.
* @method transition - set the default CSS transition name.
* @configs classname, transition, insertBefore, duration, autoClose, progressBar, onShow, onHide, position
* @config classname- String - additional class name for the toast container.
* @config transition - String - name of the CSS transition that will be used to show and hide all toast by default.
* @config insertBefore - Boolean - specifies the way in which the toasts will be inserted in the HTML code.
* @config duration - Integer - duration that the toast will be displayed in milliseconds.
* @config autoClose - Boolean - enable or disable auto hiding on toast messages.
* @config progressBar - Boolean - enable or disable the progress.
* @config onShow - Function - will be fired when a toast message appears.
* @config onHide - Function - will be fired when a toast message disappears.
* @config position - String - the position where the toast will be inserted.
* @transitions fade, slide-left-right, slide-up-down, slide-right-left, slide-down-up
* @positions top-right, top-left, bottom-right, bottom-left
* @example
* $toast.info('This is an info message.');
* $toast.success('This is a success message.');
* $toast.warning('This is a warning message.');
* $toast.error('This is an error message.');
* $toast.config({ duration: 5000 });
* $toast.transition('fade').info('This is an info message.');
* $toast.config({ position: 'top-left' }).info('This is an info message.');
* @css
* uses the convention of {transition name}--init, {transition name}--show, {transition name}--hide for CSS transitions. Example: slide-left-right--init, slide-left-right--show, slide-left-right--hide.
*
*
*/
var toast = (function ($win, $doc) {
const baseClass = 'x-toast';
/**
* Default configuration for plug-in:
*
* @var object
*/
const _defaults = {
// STRING: additional class name for the toast container:
classname: 'x',
// STRING: name of the CSS transition that will be used to show and hide all toast by default:
transition: 'slide-left-right',
// BOOLEAN: specifies the way in which the toasts will be inserted in the HTML code:
// .... Set to BOOLEAN TRUE and the toast messages will be inserted before those already generated toasts.
// .... Set to BOOLEAN FALSE otherwise.
insertBefore: true,
// INTEGER: duration that the toast will be displayed in milliseconds:
// .... Default value is set to 4000 (4 seconds).
// .... If it set to 0, the duration for each toast is calculated by text-message length.
duration: 3000,
// BOOLEAN: enable or disable auto hiding on toast messages:
// .... Set to BOOLEAN TRUE - to enable auto hiding.
// .... Set to BOOLEAN FALSE - disable auto hiding. Instead the user must click on toast message to close it.
autoClose: true,
// BOOLEAN: enable or disable the progress:
// .... Set to BOOLEAN TRUE - enable the progress only if the autoClose option value is set to BOOLEAN TRUE.
// .... Set to BOOLEAN FALSE - disable the progress.
progressBar: true,
// callback:
// onShow function will be fired when a toast message appears.
onShow: function (type) {},
// callback:
// onHide function will be fired when a toast message disappears.
onHide: function (type) {},
// the position where the toast will be inserted:
position: 'top-right',
};
/**
* Map to create each necessary CSS classess:
*
* @var object
*/
const _mappings = {
container: '{:class-name}__container',
mainwrap: '{:class-name}__wrapper',
toasts: {
info: '{:class-name}--info',
success: '{:class-name}--success',
warning: '{:class-name}--warning',
error: '{:class-name}--error',
},
animate: {
init: '{:transition}--init',
show: '{:transition}--show',
hide: '{:transition}--hide',
},
progress: '{:class-name}__progress',
position: '--{:position}',
};
/**
* Simple creation of an Element Node with the specified 'name'.
*
* @return HTML Element
*/
function node(name) {
return $doc.createElement(name);
}
/**
* Returns the parent Element or Node from any other HTML Element.
*
* @return HTML Element
*/
function parentElement(el) {
return el.parentElement || el.parentNode;
}
function containsClass(el, className) {
return el.classList.contains(className); // Modern browsers support this
}
function addClass(el, ...classNames) {
classNames.forEach((name) => {
if (!containsClass(el, name)) {
el.classList.add(name);
}
});
return el;
}
function addEvent(el, evt, fn, bubble = false) {
el.addEventListener(evt, fn, bubble);
return el;
}
function removeEvent(el, evt, fn, bubble = false) {
el.removeEventListener(evt, fn, bubble);
return el;
}
let cachedTransitionEvent = null;
/**
* Detect the property name of supported transition event.
*
* Function from David Walsh:
* http://davidwalsh.name/css-animation-callback
*
* @return string|undefined (if transitions not supported by client)
*/
function whichTransitionEvent() {
if (cachedTransitionEvent) {
return cachedTransitionEvent;
}
const el = document.createElement('div');
const transitions = {
WebkitTransition: 'webkitTransitionEnd',
MozTransition: 'transitionend',
OTransition: 'oTransitionEnd otransitionend',
transition: 'transitionend',
};
for (let t in transitions) {
if (t in el.style) {
cachedTransitionEvent = transitions[t];
break;
}
}
return cachedTransitionEvent;
}
/**
* Replace each object values with a map of key => values:
*
* @return object
*/
function walker(obj, map) {
for (const o in obj)
if (obj.hasOwnProperty(o) === true) {
// ini loop:
switch (typeof obj[o]) {
case 'object':
walker(obj[o], map);
break;
case 'string':
for (const m in map)
if (map.hasOwnProperty(m) === true) {
obj[o] = obj[o].replace(m, map[m]);
}
break;
}
// end loop.
}
return obj;
}
/**
* Show the toast message with an CSS3 transition
* if transition event is supported:
*
* @return void
*/
const showToast = function (type, el, container, animate, duration, insertBefore, callback) {
const onShowToast = (e) => {
removeEvent(e.target, e.type, onShowToast, false);
if (typeof callback == 'function') callback(type);
};
const show = () => {
const transitionEvent = whichTransitionEvent();
if (transitionEvent !== undefined) {
// initialize the CSS transition event:
addEvent(el, transitionEvent, onShowToast, false);
} else {
// navigator does not support transition events:
if (typeof callback == 'function') callback(type);
}
addClass(el, animate.show);
};
// insert in the DOM and show toast:
let beforeNode = container.childNodes;
beforeNode = beforeNode[insertBefore === true ? 0 : beforeNode.length];
container.insertBefore(el, beforeNode);
setTimeout(show, 100);
};
/**
* Hide the toast message with an CSS3 transition
* if transition event is supported:
*
* @return void
*/
const hideToast = function (type, el, duration, animate, callback) {
const remove = () => {
const container = parentElement(el); // the wrapper.
el.remove();
const num = container.childNodes.length;
if (num < 1) {
parentElement(container).remove();
}
};
const onHideToast = (e) => {
removeEvent(e.target, e.type, onHideToast, false);
remove();
if (typeof callback == 'function') callback(type);
};
const hide = () => {
const transitionEvent = whichTransitionEvent();
if (transitionEvent !== undefined) {
// initialize the CSS transition event:
addEvent(el, transitionEvent, onHideToast, false);
} else {
// navigator does not support transition events:
remove();
if (typeof callback == 'function') callback(type);
}
addClass(el, animate.hide);
};
setTimeout(hide, duration);
};
/**
* Hide the toast message with an CSS3 transition when
* the user clicks on the message:
*
* @return void
*/
const hideToastOnClick = function (type, el, animate, callback, class2close) {
const hideOnClick = function (e) {
e.stopPropagation();
// removeClass(el, class2close);
hideToast(type, el, 0, animate, callback);
};
addClass(el, class2close);
addEvent(el, 'click', hideOnClick);
};
/**
* The progress:
*
* @return void
*/
const showProgressBar = function (type, el, duration, transition) {
const progress = () => {
const progressBar = addClass(
node('div'),
transition.progress,
transition.progress + '--' + type
);
el.appendChild(progressBar);
let iterat = 0,
offset = 0;
const interval = setInterval(() => {
iterat++;
offset = Math.round((1500 * iterat) / duration);
if (offset > 100) {
clearInterval(interval);
} else {
progressBar.style.width = offset + '%';
}
}, 10);
};
setTimeout(progress, 100);
};
/**
* Register a new transition only:
*
* @return string
*/
const registerTransition = function (self, name) {
if (typeof name === 'string') {
self.classmap[name] = walker(
{ ..._mappings },
{
'{:class-name}': baseClass,
'{:transition}': name,
'{:position}': self.settings.position,
}
);
}
return name;
};
/*!
* The exposed public object:
*/
class Toast {
constructor(options) {
this.settings = {};
this.classmap = {};
this.config(options || _defaults);
this.transition(this.settings.transition);
}
config(options) {
this.settings = extend(_defaults, this.settings, options || {});
return this;
}
transition(name) {
this.settings.transition = registerTransition(this, name);
return this;
}
toast(type, message, durationOroptions) {
const classes = this.classmap;
let options = this.settings;
if (typeof durationOroptions === 'number') {
options.duration = durationOroptions;
} else {
options = extend(this.settings, durationOroptions || {});
}
// check if the transition name provided in options
// exists in classes, if not register it:
if (classes.hasOwnProperty(options.transition) === false) {
registerTransition(this, options.transition);
}
// use the transition name provided in options:
const transition = classes[options.transition];
// check if the toast container exists:
let container = $doc.querySelector('.' + transition.container);
const containerExists = !!container;
if (containerExists) {
// create the toast container if not exists:
container = container.querySelector('.' + transition.mainwrap); // use the wrapper instead of main container.
} else {
container = addClass(
node('div'),
transition.container,
transition.container,
transition.position
);
// create a alert wrapper instance:
const wrapp = addClass(node('div'), transition.mainwrap);
// append the alert wrapper and now, this is the main container:
container.appendChild((container = wrapp));
}
// create a new toast instance
const newToast = addClass(
node('div'),
baseClass,
options.classname,
transition.toasts[type],
transition.animate.init
);
newToast.textContent = message;
// insert the toast container into the HTML:
if (!containerExists) {
$doc.body.append(parentElement(container));
}
// STEP 1:
// INI: showing the toas message
// --------------------------------------------------------------------
showToast(
type,
newToast,
container,
transition.animate,
options.duration,
options.insertBefore,
options.onShow
);
// --------------------------------------------------------------------
// END: showing the toas message
// STEP 2:
// INI: prepare the toast to hide it.
// --------------------------------------------------------------------
if (options.autoClose) {
// hide the toast message automatically:
hideToast(type, newToast, options.duration, transition.animate, options.onHide);
} else {
// hide the toast message on click it with an CSS3 transition:
hideToastOnClick(
type,
newToast,
transition.animate,
options.onHide,
'close-on-click'
);
}
// --------------------------------------------------------------------
// END: prepare the toast to hide it.
// OPTIONAL STEP (must be last):
// INI: Enable or disable the progress.
// --------------------------------------------------------------------
if (options.progressBar && options.autoClose) {
showProgressBar(type, newToast, options.duration, transition);
}
// --------------------------------------------------------------------
// END: Enable or disable the progress.
return this;
}
info(message, opts) {
this.toast('info', message, opts);
}
success(message, opts) {
this.toast('success', message, opts);
}
warning(message, opts) {
this.toast('warning', message, opts);
}
error(message, opts) {
this.toast('error', message, opts);
}
}
return ($win.$toast = $win.$toast || new Toast());
})(typeof window !== 'undefined' ? window : {}, typeof document !== 'undefined' ? document : {});
exports.default = toast;