'use strict';
class Notification {
/** @summary Create an instance of a notification handler
* @author Arthur Beaulieu
* @since June 2018
* @description Build the notification singleton handler that will handle all incoming Notifications
* @param {object} [options] - The notification handler global options
* @param {string} [options.position=top-right] - <i>top-left; top-right; bottom-left; bottom-right;</i>
* @param {string} [options.thickBorder=top] - <i>top; bottom; left; right; none;</i>
* @param {number} [options.duration=3000] - Notification life cycle duration (in ms) in range N*
* @param {number} [options.transition=100] - Notification fade animation transition timing (in ms) in range N*
* @param {number} [options.maxActive=5] - Maximum of simultaneously opened notification in range N* */
constructor(options) {
if (!!Notification.instance) { // GoF Singleton
return Notification.instance;
}
Notification.instance = this;
// Attributes declaration
/** @private
* @member {boolean} - Dismiss all operation in progress flag */
this._dismissAllLock = false;
/** @private
* @member {object} - Notification handler container node */
this._dom = {};
/** @private
* @member {object} - Active notifications object : retrieve a notification using its ID (this._active[ID]) */
this._active = {};
/** @private
* @member {object} - Queue notifications when max active has been reached */
this._queue = {};
/** @private
* @member {object} - Notification handler default values */
this._default = {};
/** @private
* @member {string} - The handler position in viewport - <i>top-left; top-right; bottom-left; bottom-right;</i> */
this._position = '';
/** @private
* @member {string} - The thick border position in the Notification - <i>top; bottom; left; right; none;</i> */
this._thickBorder = '';
/** @private
* @member {number} - The Notification on screen duration in ms */
this._duration = 0;
/** @private
* @member {number} - The fade transition time in ms */
this._transition = 0;
/** @private
* @member {number} - The maximum amount of active Notification */
this._maxActive = 0;
/** @public
* @member {number} - The component version */
this.version = '1.1.0';
// Build singleton and attach
this._init(options);
// Return singleton
return this;
}
/** @method
* @name destroy
* @public
* @memberof Notification
* @author Arthur Beaulieu
* @since March 2019
* @description Destroy the singleton and detach it from the DOM */
destroy() {
document.body.removeChild(this._dom);
// Delete object attributes
Object.keys(this).forEach(key => {
delete this[key];
});
// Clear singleton instance
Notification.instance = null;
}
/* --------------------------------------------------------------------------------------------------------------- */
/* ------------------------------ NOTIFICATION JS HANDLER CONSTRUCTION METHODS -------------------------------- */
/* */
/* The following methods only concerns the singleton creation. It handle all arguments and will fallback on */
/* default values if any argument doesn't meet its expected value or type. */
/* --------------------------------------------------------------------------------------------------------------- */
/** @method
* @name _init
* @private
* @memberof Notification
* @author Arthur Beaulieu
* @since July 2018
* @description Create the handler DOM element, set default values, test given options and properly add CSS class to the handler
* @param {object} [options] - The notification handler global options
* @param {string} [options.position=top-right] - <i>top-left; top-right; bottom-left; bottom-right;</i>
* @param {string} [options.thickBorder=top] - <i>top; bottom; left; right; none;</i>
* @param {number} [options.duration=3000] - Notification life cycle duration (in ms) in range N*
* @param {number} [options.transition=100] - Notification fade animation transition timing (in ms) in range N*
* @param {number} [options.maxActive=5] - Maximum of simultaneously opened notification in range N* */
_init(options) {
// Declare options as object if empty
if (options === undefined) {
options = {};
}
// Create notification main container
this._dom = document.createElement('DIV'); // Notification handler DOM container
this._dom.classList.add('notification-container'); // Set proper CSS class
// Notification.js default values
this._default = {
handler: {
position: 'top-right',
thickBorder: 'top',
duration: 5000,
transition: 200,
maxActive: 10
},
notification: {
type: 'info',
message: '',
title: '',
iconless: false,
closable: true,
sticky: false,
renderTo: this._dom,
CBtitle: '',
callback: null,
isDimmed: false
},
color: {
success: 'rgb(76, 175, 80)',
info: 'rgb(3, 169, 244)',
warning: 'rgb(255, 152, 0)',
error: 'rgb(244, 67, 54)'
},
svgPath: {
success: 'M12.5 0C5.602 0 0 5.602 0 12.5S5.602 25 12.5 25 25 19.398 25 12.5 19.398 0 12.5 0zm-2.3 18.898l-5.5-5.5 1.8-1.796 3.7 3.699L18.5 7l1.8 1.8zm0 0',
info: 'M12.504.035a12.468 12.468 0 100 24.937 12.468 12.468 0 000-24.937zM15.1 19.359c-.643.25-1.153.445-1.537.576-.384.134-.825.199-1.333.199-.775 0-1.381-.192-1.813-.57a1.832 1.832 0 01-.642-1.442c0-.227.015-.459.047-.693.03-.24.083-.504.154-.806l.802-2.835c.069-.272.132-.527.182-.77.048-.244.069-.467.069-.668 0-.36-.075-.615-.223-.756-.153-.144-.437-.213-.857-.213-.207 0-.422.036-.639.095a9.914 9.914 0 00-.56.184l.213-.874a19.777 19.777 0 011.51-.549 4.48 4.48 0 011.361-.23c.77 0 1.368.19 1.784.56a1.857 1.857 0 01.626 1.452c0 .122-.012.341-.04.652a4.44 4.44 0 01-.162.856l-.798 2.831a8.133 8.133 0 00-.176.775c-.05.288-.075.51-.075.66 0 .374.082.633.251.771.165.134.458.202.875.202.192 0 .412-.037.66-.1.243-.073.42-.127.531-.18zm-.144-11.483a1.901 1.901 0 01-1.343.518 1.93 1.93 0 01-1.352-.518 1.65 1.65 0 01-.562-1.258 1.688 1.688 0 01.562-1.266 1.914 1.914 0 011.35-.522c.524 0 .975.173 1.345.523a1.673 1.673 0 01.56 1.266 1.65 1.65 0 01-.56 1.257z',
warning: 'M24.585 21.17L13.774 3.24a1.51 1.51 0 00-2.586 0L.376 21.17a1.51 1.51 0 001.293 2.29h21.623a1.51 1.51 0 001.292-2.29zM12.49 8.714c.621 0 1.146.35 1.146.97 0 1.895-.223 4.618-.223 6.513 0 .494-.541.7-.923.7-.51 0-.94-.208-.94-.701 0-1.894-.223-4.617-.223-6.511 0-.62.51-.971 1.163-.971zm.015 11.734a1.225 1.225 0 01-1.225-1.226c0-.669.525-1.227 1.225-1.227.652 0 1.21.558 1.21 1.227 0 .652-.557 1.225-1.21 1.225z',
error: 'M12.469.027c-3.332 0-6.465 1.301-8.824 3.653-4.86 4.86-4.86 12.777 0 17.636a12.392 12.392 0 008.824 3.653c3.336 0 6.465-1.301 8.824-3.653 4.863-4.859 4.863-12.777 0-17.636A12.417 12.417 0 0012.469.027zm5.61 18.086a1.137 1.137 0 01-.802.332c-.285 0-.582-.113-.8-.332l-4.008-4.008-4.008 4.008a1.137 1.137 0 01-.8.332c-.286 0-.583-.113-.802-.332a1.132 1.132 0 010-1.605l4.008-4.004L6.86 8.496a1.132 1.132 0 010-1.605 1.127 1.127 0 011.602 0l4.008 4.007 4.008-4.007a1.127 1.127 0 011.601 0c.45.449.45 1.164 0 1.605l-4.004 4.008 4.004 4.004c.45.449.45 1.164 0 1.605zm0 0'
}
};
// Build singleton from options and sanitize them
this._setOptionsDefault(options);
this._position = options.position;
this._thickBorder = options.thickBorder;
this._duration = options.duration;
this._transition = options.transition;
this._maxActive = options.maxActive;
this._setAttributesDefault();
// Add position CSS class only after this._position is sure to be a valid value
this._dom.classList.add(this._position);
this._attach();
}
/** @method
* @name _setOptionsDefault
* @private
* @memberof Notification
* @summary Set singleton options
* @author Arthur Beaulieu
* @since March 2019
* @description Build the notification singleton according to the user options
* @param {object} options - The singleton options to set */
_setOptionsDefault(options) {
if (options.position === undefined) {
options.position = this._default.handler.position;
}
if (options.thickBorder === undefined) {
options.thickBorder = this._default.handler.thickBorder;
}
if (options.duration === undefined) {
options.duration = this._default.handler.duration;
}
if (options.transition === undefined) {
options.transition = this._default.handler.transition;
}
if (options.maxActive === undefined) {
options.maxActive = this._default.handler.maxActive;
}
}
/** @method
* @name _setAttributesDefault
* @private
* @memberof Notification
* @summary Check the notification singleton options validity
* @author Arthur Beaulieu
* @since March 2019
* @description Fallback on default attributes value if the notification singleton options are invalid */
_setAttributesDefault() {
if (this._position !== 'top-left' && /* Illegal value for position */
this._position !== 'top-right' &&
this._position !== 'bottom-left' &&
this._position !== 'bottom-right') {
this._position = this._default.handler.position; // Default value
}
if (this._thickBorder !== 'top' && /* Illegal value for thick border */
this._thickBorder !== 'bottom' &&
this._thickBorder !== 'left' &&
this._thickBorder !== 'right' &&
this._thickBorder !== 'none') {
this._thickBorder = this._default.handler.thickBorder; // Default value
}
if (typeof this._duration !== 'number' || this._duration <= 0) { // Illegal value for duration
this._duration = this._default.handler.duration; // Default value
}
if (typeof this._transition !== 'number' || this._duration < (this._transition * 2) || this._transition <= 0) { // Transition over (duration / 2)
this._transition = this._default.handler.transition; // Default value for _maxActive
}
if (typeof this._maxActive !== 'number' || this._maxActive <= 0) { // Illegal value for maxActive
this._maxActive = this._default.handler.maxActive; // Default value for _maxActive
}
}
/** @method
* @name _attach
* @private
* @memberof Notification
* @author Arthur Beaulieu
* @since July 2018
* @description Attach the notification handler to the dom using a fragment */
_attach() {
const fragment = document.createDocumentFragment();
fragment.appendChild(this._dom);
document.body.appendChild(fragment);
}
/* --------------------------------------------------------------------------------------------------------------- */
/* ------------------------------------- NOTIFICATION SPECIFIC METHODS ---------------------------------------- */
/* */
/* The following methods implements notification features. It handle its events, lifecycle depending on its */
/* parameters, its DOM structure, and its animations. The Notification singleton will handle the notification */
/* stacking the in user interface. */
/* --------------------------------------------------------------------------------------------------------------- */
/** @method
* @name _events
* @private
* @memberof Notification
* @author Arthur Beaulieu
* @since June 2018
* @description Handle mouse events for the given notification
* @param {{id: number}} notification - The notification object
* @param {number} notification.id - Notification personnal ID
* @param {object} notification.dom - Notifiction DOM element
* @param {number} notification.requestCount - Notification inner call counter
* @param {number} notification.timeoutID - Notification own setTimeout ID
* @param {boolean} notification.sticky - Notification sticky behvaior
* @param {boolean} notification.closable - Make notification closable flag */
_events(notification) {
let closeFired = false; // Close fired flag
// Inner callback functions
const _unDim = () => { // Undim notification
if (notification.isDimmed) {
this._unDim(notification);
}
};
const _close = () => { // Close notification
if (this._active[notification.id] === undefined) {
return;
}
// Update counter DOM element
if (notification.requestCount > 1) {
this._decrementRequestCounter(notification, true);
}
// Remove notification element from the DOM tree
else if (!closeFired) {
closeFired = true;
window.clearTimeout(notification.timeoutID); // Clear life cycle timeout
notification.dom.close.removeEventListener('click', _close); // Avoid error when spam clicking the close button
this._close(notification);
}
};
const _resetTimeout = () => { // Reset life cycle timeout
if (this._active[notification.id] === undefined) {
return;
}
if (!closeFired && !notification.isDimmed) { // Only reset timeout if no close event has been fired
this._resetTimeout(notification);
}
};
// Mouse event listeners
if (notification.sticky) {
notification.dom.addEventListener('mouseenter', _unDim.bind(this));
notification.dom.addEventListener('mouseout', _unDim.bind(this));
}
if (notification.closable) {
notification.dom.addEventListener('click', _close.bind(this));
notification.dom.close.addEventListener('click', _close.bind(this));
}
notification.dom.addEventListener('mouseover', _resetTimeout.bind(this));
}
/** @method
* @name _buildUI
* @private
* @memberof Notification
* @author Arthur Beaulieu
* @since June 2018
* @description Builds the DOM element that contains and that adapts to all given options
* @param {object} notification - The notification object
* @param {number} notification.id - Notification personnal ID
* @param {string} notification.type - Error, Warning, Info, Success
* @param {string} notification.title - Notification title
* @param {string} notification.message - Notification message
* @param {boolean} notification.iconless - No icon flag
* @param {string} notification.thickBorder - Notification border side (override handler side value)
* @param {boolean} notification.closable - Make notification closable flag
* @param {boolean} notification.sticky - Make notification sticky flag
* @param {string} notification.CBtitle - Notification callback title
* @param {function} notification.callback - Notification callback button
* @returns {object} Enhanced and ready notification object */
_buildUI(notification) {
notification.requestCount = 1;
notification.totalRequestCount = 1;
this._buildUIDom(notification);
this._buildNotificationType(notification);
if (notification.iconless) {
notification.dom.message.classList.add('iconless-width');
}
notification.dom.text.appendChild(notification.dom.maintitle);
notification.dom.text.appendChild(notification.dom.message);
// Add callback button and listener if needed
if (notification.callback) {
const callbackButton = document.createElement('BUTTON');
callbackButton.innerHTML = notification.CBtitle;
notification.dom.text.appendChild(callbackButton);
callbackButton.addEventListener('click', () => {
this._close(notification);
notification.callback();
});
}
// Fill notification DOM element
if (!notification.iconless) {
notification.dom.appendChild(notification.dom.icon);
}
notification.dom.appendChild(notification.dom.text);
// Append close button if needed
if (notification.closable) {
notification.dom.appendChild(notification.dom.close);
}
// Return final notification
return notification;
}
/** @method
* @name _buildUIDom
* @private
* @memberof Notification
* @summary Create the Notification DOM tree
* @author Arthur Beaulieu
* @since March 2019
* @description Build all the Notification internal structure
* @param {object} notification - The notification to create */
_buildUIDom(notification) {
// Create notification DOM elements
notification.dom = document.createElement('DIV');
notification.dom.icon = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
notification.dom.iconPath = document.createElementNS('http://www.w3.org/2000/svg', 'path');
notification.dom.text = document.createElement('DIV');
notification.dom.close = document.createElement('DIV');
notification.dom.maintitle = document.createElement('H6');
notification.dom.message = document.createElement('P');
// Class assignation
notification.dom.classList.add('notification');
notification.dom.icon.classList.add('vector-container');
notification.dom.text.classList.add('text-container');
notification.dom.close.classList.add('close');
// Changing border side
if (notification.thickBorder === 'top') {
notification.dom.classList.add('top-border');
} else if (notification.thickBorder === 'bottom') {
notification.dom.classList.add('bottom-border');
} else if (notification.thickBorder === 'left') {
notification.dom.classList.add('left-border');
} else if (notification.thickBorder === 'right') {
notification.dom.classList.add('right-border');
}
// Text modification
notification.dom.maintitle.innerHTML = notification.title || '';
notification.dom.message.innerHTML = notification.message || '';
notification.dom.close.innerHTML = '✖';
// Image vector
notification.dom.icon.setAttribute('viewBox', '0 0 25 25');
notification.dom.icon.setAttribute('width', '25');
notification.dom.icon.setAttribute('height', '25');
notification.dom.icon.appendChild(notification.dom.iconPath);
}
/** @method
* @name _buildNotificationType
* @private
* @memberof Notification
* @summary Attach proper assets and css
* @author Arthur Beaulieu
* @since March 2019
* @description Fills the Notification icon and class according to its inner type
* @param {object} notification - The notification to fill */
_buildNotificationType(notification) {
// Type specification (title, icon, color)
if (['success', 'warning', 'error', 'info'].indexOf(notification.type) !== -1){
notification.dom.classList.add(notification.type);
if (!notification.iconless) {
notification.dom.iconPath.setAttribute('fill', this._default.color[notification.type]);
notification.dom.iconPath.setAttribute('d', this._default.svgPath[notification.type]);
}
} else {
notification.dom.classList.add('info');
if (!notification.iconless) {
notification.dom.iconPath.setAttribute('fill', this._default.color.info);
notification.dom.iconPath.setAttribute('d', this._default.svgPath.info);
}
}
}
/** @method
* @name _start
* @private
* @memberof Notification
* @author Arthur Beaulieu
* @since June 2018
* @description Call this method to add the new notification to the DOM container, and launch its life cycle
* @param {object} notification - The notification object
* @param {number} notification.id - Notification own ID */
_start(notification) {
if (Object.keys(this._active).length >= this._maxActive) {
this._queue[notification.id] = notification;
} else {
this._active[notification.id] = notification; // Append the new notification to the _active object
this._events(notification); // Listen to mouse events on the newly created notification
this._open(notification); // Open the new notification
notification.timeoutID = window.setTimeout(() => {
this._checkCounter(notification); // Check notification request count to act accordingly
}, notification.duration); // Use Notification master duration
}
}
/** @method
* @name _open
* @private
* @memberof Notification
* @author Arthur Beaulieu
* @since June 2018
* @description Open and add the notification to the container
* @param {{id: number}} notification - The notification object
* @param {number} notification.id - Notification personnal ID
* @param {object} notification.dom - Notifiction DOM element */
_open(notification) {
// Reverse insertion when notifications are on bottom
if (this._position === 'bottom-right' || this._position === 'bottom-left') {
notification.renderTo.insertBefore(notification.dom, notification.renderTo.firstChild);
} else {
notification.renderTo.appendChild(notification.dom);
}
notification.opened = Date.now();
window.setTimeout(() => {
notification.dom.style.opacity = 1;
}, 10);
}
/** @method
* @name _close
* @private
* @memberof Notification
* @author Arthur Beaulieu
* @since June 2018
* @description Close and remove the notification from the container
* @param {{id: number}|{id: number, dom: Object, requestCount: number, timeoutID: number, sticky: boolean, closable: boolean}} notification - The notification object
* @param {number} notification.id - Notification personnal ID
* @param {boolean} notification.isClosing - Already closing flag
* @param {object} notification.dom - Notifiction DOM element
* @param {object} notification.renderTo - DOM object to render the notification in */
_close(notification) {
if (notification.isClosing) { // Avoid double close on a notification (in case dismiss/dismissAll is triggerred when notification is already closing)
return;
}
notification.isClosing = true; // Lock notification to one fadeOut animation
notification.closed = Date.now();
notification.effectiveDuration = notification.closed - notification.opened;
notification.dom.style.opacity = 0;
window.setTimeout(() => {
notification.renderTo.removeChild(notification.dom); // Remove this notification from the DOM tree
delete this._active[notification.id];
if (Object.keys(this._queue).length > 0) { // Notification queue is not empty
this._start(this._queue[Object.keys(this._queue)[0]]); // Start first queued notification
delete this._queue[Object.keys(this._queue)[0]]; // Shift queue object
} else if (Object.keys(this._active).length === 0) { // Check this._active emptyness
this._dismissAllLock = false; // Unlock dismissAllLock
}
}, 1000); // Transition value set in _notification.scss
}
/** @method
* @name _incrementRequestCounter
* @private
* @memberof Notification
* @author Arthur Beaulieu
* @since June 2018
* @description This method is called when a notification is requested another time
* @param {object} notification - The notification object
* @param {number} notification.id - Notification personnal ID
* @param {number} notification.requestCount - Notification inner call counter
* @param {object} notification.dom - Notifiction DOM element
* @param {boolean} notification.sticky - Notification sticky behvaior
* @param {boolean} notification.isDimmed - Notification dimmed status (only useful if notification.sticky is true) */
_incrementRequestCounter(notification) {
++notification.requestCount; // Increment notification.requestCount
if (notification.totalRequestCount < notification.requestCount) {
notification.totalRequestCount = notification.requestCount;
}
// Update counter DOM element
if (notification.requestCount > 1) {
let valueToDisplay = '∞';
if (notification.requestCount < 100) {
valueToDisplay = notification.requestCount;
}
if (notification.dom.counter) { // Update existing counter
notification.dom.counter.innerHTML = valueToDisplay;
} else { // Create counter DOM element
notification.dom.counter = document.createElement('DIV');
notification.dom.counter.classList.add('counter');
notification.dom.counter.innerHTML = valueToDisplay;
notification.dom.appendChild(notification.dom.counter);
}
}
// Undim notification if it is a sticky/dimmed one
if (notification.sticky && notification.isDimmed) {
this._unDim(notification);
}
}
/** @method
* @name _decrementRequestCounter
* @private
* @memberof Notification
* @author Arthur Beaulieu
* @since June 2018
* @description This method is called each notification cycle end to update its inner counter
* @param {{id: number, dom: Object, requestCount: number, timeoutID: number, sticky: boolean, closable: boolean}} notification - The notification object
* @param {number} notification.id - Notification personnal ID
* @param {boolean} notification.sticky - Notification sticky behvaior
* @param {boolean} notification.isDimmed - Notification dimmed status (only useful if notification.sticky is true)
* @param {number} notification.requestCount - Notification inner call counter
* @param {object} notification.dom - Notification DOM element
* @param {boolean} force - To force the notification.requestCount decrementation */
_decrementRequestCounter(notification, force) {
if (notification.sticky && !force) {
if (!notification.isDimmed) {
this._dim(notification);
}
return;
}
this._resetTimeout(notification);
--notification.requestCount; // Decrement notification.requestCount
// Update counter DOM element
if (notification.requestCount > 1) {
let valueToDisplay = '∞';
if (notification.requestCount < 100) {
valueToDisplay = notification.requestCount;
}
notification.dom.counter.innerHTML = valueToDisplay;
} else { // Remove counter element from the DOM tree
notification.dom.removeChild(notification.dom.counter);
delete notification.dom.counter;
}
}
/** @method
* @name _checkCounter
* @private
* @memberof Notification
* @author Arthur Beaulieu
* @since June 2018
* @description This method will reset the fadeout/dim timeout or close/dim the notification depending on its requestCount
* @param {{id: number}} notification - The notification object
* @param {number} notification.id - Notification personnal ID
* @param {number} notification.requestCount - Notification inner call counter
* @param {object} notification.dom - Notifiction DOM element
* @param {number} notification.timeoutID - Notification own setTimeout ID
* @param {boolean} notification.sticky - Notification sticky behvaior */
_checkCounter(notification) {
// This notification as still more than one cycle to live
if (notification.requestCount > 1) {
this._decrementRequestCounter(notification);
} else { // This notification reached the end of its life cycle
if (notification.renderTo.contains(notification.dom)) {
window.clearTimeout(notification.timeoutID);
if (notification.sticky) { // FadeOut/Dim depending on sticky behavior
this._dim(notification);
} else {
this._close(notification);
}
}
}
}
/** @method
* @name _clearRequestCount
* @private
* @memberof Notification
* @author Arthur Beaulieu
* @since June 2018
* @description Method that clear every pending request
* @param {object} notification - The notification object
* @param {number} notification.id - Notification personnal ID
* @param {object} notification.dom - Notifiction DOM element */
_clearRequestCount(notification) {
notification.requestCount = 1;
notification.dom.removeChild(notification.dom.counter);
delete notification.dom.counter;
}
/** @method
* @name _resetTimeout
* @private
* @memberof Notification
* @author Arthur Beaulieu
* @since June 2018
* @description Use this to reset a notification life cycle, and delay its close event
* @param {{id: number}|{id: number, dom: Object, requestCount: number, timeoutID: number, sticky: boolean, closable: boolean}} notification - The notification object
* @param {number} notification.id - Notification personnal ID
* @param {number} notification.timeoutID - Notification own setTimeout ID */
_resetTimeout(notification) {
window.clearTimeout(notification.timeoutID); // Clear previous life cycle
notification.timeoutID = window.setTimeout(() => {
this._checkCounter(notification); // Check notification request count to act accordingly
}, notification.duration); // Use Notification master duration
}
/** @method
* @name _dim
* @private
* @memberof Notification
* @author Arthur Beaulieu
* @since June 2018
* @description Only useful for sticky notification that dim instead of close at the end of its life cycle
* @param {{id: number, requestCount: number, dom: Object, timeoutID: number, sticky: boolean}} notification - The notification object
* @param {number} notification.id - Notification personnal ID
* @param {object} notification.dom - Notifiction DOM element
* @param {boolean} notification.sticky - Notification sticky behvaior
* @param {boolean} notification.isDimmed - Notification dimmed status (only useful if notification.sticky is true) */
_dim(notification) {
const that = this;
let i = 100;
(function halfFadeOut() { // Start animation immediatly
if (i >= 0) {
notification.dom.style.opacity = i / 100;
--i;
if (i === 50 && notification.sticky) { // Opacity has reached 0.51
notification.dom.style.opacity = 0.5; // Set half transparency on notification
notification.isDimmed = true; // Update notification dim status
return; // End function
}
}
window.setTimeout(halfFadeOut, that._transition / 100); // Split animation transition into 100 iterations (50 for real here)
})();
}
/** @method
* @name _unDim
* @private
* @memberof Notification
* @author Arthur Beaulieu
* @since June 2018
* @description Call this method when a notification is not inactive anymore
* @param {object} notification - The notification object
* @param {number} notification.id - Notification personnal ID
* @param {object} notification.dom - Notifiction DOM element
* @param {boolean} notification.isDimmed - Notification dimmed status (only useful if notification.sticky is true) */
_unDim(notification) {
const that = this;
let i = 50;
(function halfFadeIn() {
if (i < 100) {
notification.dom.style.opacity = i / 100;
++i;
} else if (i === 100) {
notification.dom.style.opacity = 1; // Set full visibility on notification
notification.isDimmed = false; // Update notification dim status
that._resetTimeout(notification); // Reset life cycle timeout
return; // End function
}
window.setTimeout(halfFadeIn, that._transition / 100); // Split animation transition into 100 iterations (50 for real here)
})();
}
/* --------------------------------------------------------------------------------------------------------------- */
/* ----------------------------- SINGLE NOTIFICATION CONSTRUCTION UTILS METHODS ------------------------------- */
/* */
/* The following methods only concerns a new notification request. It will test the options validity, default to */
/* fallback value if necessary and give the notification a pseudo unique identifier. */
/* --------------------------------------------------------------------------------------------------------------- */
/** @method
* @name _checkNotificationOptionsValidity
* @private
* @memberof Notification
* @summary Check the Notification options validity
* @author Arthur Beaulieu
* @since March 2019
* @description Check a Notification options object against the required parameters.
* @param {object} options - The notification options to check validity */
_checkNotificationOptionsValidity(options) {
// Check for mandatory arguments existence
if (options === undefined || (options.type === undefined || options.message === undefined)) {
return false;
}
// Check existing message
if (typeof options.message !== 'string' || options.message.length === 0) {
return false;
}
// Check for unclosable at all notification
if (options.sticky && options.closable === false) {
return false;
}
// Test Notification inner variables validity
if (options.type !== 'info' && options.type !== 'success' && options.type !== 'warning' && options.type !== 'error') {
options.type = this._default.notification.type;
}
// Unlock dismissAllLock
if (this._dismissAllLock) {
this._dismissAllLock = false;
}
return true;
}
/** @method
* @name _setOptionsFallback
* @private
* @memberof Notification
* @summary Set Notification fallback options
* @author Arthur Beaulieu
* @since March 2019
* @description Check a Notification options object and fill it with default value in case they are empty.
* @param {object} options - The notification options to fill with default value if empty */
_setOptionsFallback(options) {
if (options.title === undefined) {
options.title = this._default.notification.title;
}
if (options.duration === undefined) {
options.duration = this._duration;
}
if (options.iconless === undefined) {
options.iconless = this._default.notification.iconless;
}
if (options.thickBorder === undefined) {
options.thickBorder = this._thickBorder;
}
if (options.closable === undefined) {
options.closable = this._default.notification.closable;
}
if (options.sticky === undefined) {
options.sticky= this._default.notification.sticky;
}
if (options.renderTo === undefined) {
options.renderTo = this._default.notification.renderTo;
}
if (options.CBtitle === undefined) {
options.CBtitle = this._default.notification.CBtitle;
}
if (options.callback === undefined) {
options.callback = this._default.notification.callback;
}
if (options.isDimmed === undefined) {
options.isDimmed = this._default.notification.isDimmed;
}
}
/** @method
* @name _idGenerator
* @private
* @memberof Notification
* @summary Generate an ID
* @author Arthur Beaulieu
* @since June 2018
* @description Hash the seed to generate an ID
* @param {string} seed - The seed string to hash
* @param {number} length - The length of the returned ID */
_idGenerator(seed, length) {
/* Original code from:
* http://werxltd.com/wp/2010/05/13/javascript-implementation-of-javas-string-hashcode-method/
* Tweaked to fit Notification class needs
*/
let hash = 0;
let character = '';
if (seed.length === 0 || length > 12) { return undefined; }
for (let i = 0; i < seed.length; ++i) {
character = seed.charCodeAt(i);
hash = ((hash << 5) - hash) + character;
hash |= 0; // Convert to 32bit integer
}
return (Math.abs(hash).toString(36) + '' + Math.abs(hash / 2).toString(36).split('').reverse().join('')).substring(0, length).toUpperCase(); // Here is the twekead line
}
/* --------------------------------------------------------------------------------------------------------------- */
/* -------------------------------------- NOTIFICATION PUBLIC METHODS ----------------------------------------- */
/* */
/* The following methods are the exposed API of the Notification component. It allow to raise standard or custom */
/* notification without bothering their lifecycle, position or other JavaScript expensive implementation. */
/* --------------------------------------------------------------------------------------------------------------- */
/** @method
* @name new
* @public
* @memberof Notification
* @author Arthur Beaulieu
* @since June 2018
* @description Build a notification according to the given options, then append it to notification container.
* @param {object} options - The notification options object
* @param {string} options.type - <i>Error; Warning; Info; Success;</i>
* @param {string} [options.title=options.type] - Notification title
* @param {string} options.message - Notification message
* @param {number} [options.duration=handler] - Notification duration (override handler duration value)
* @param {boolean} [options.iconless=false] - No icon flag
* @param {string} [options.thickBorder=handler] - Notification border side (override handler side value)
* @param {boolean} [options.closable=true] - Make notification closable flag
* @param {boolean} [options.sticky=false] - Make notification sticky flag
* @param {object} [options.renderTo=handler] - Dom object to render the notification in
* @param {string} [options.CBtitle=Callback] - Notification callback title
* @param {function} [options.callback=undefined] - Notification callback button
* @returns {number} The newly created notification ID */
new(options) {
if (this._checkNotificationOptionsValidity(options) === false) {
console.error('Notification.js : new() options argument object is invalid.');
return -1;
}
this._setOptionsFallback(options);
// Build notification DOM element according to the given options
let notification = this._buildUI({
id: this._idGenerator(`${options.type}${options.message}`, 5), // Generating an ID of 5 characters long from notification mandatory fields
type: options.type,
message: options.message,
title: options.title,
duration: options.duration,
iconless: options.iconless,
thickBorder: options.thickBorder,
closable: options.closable,
sticky: options.sticky,
renderTo: options.renderTo,
CBtitle: options.CBtitle,
callback: options.callback,
isDimmed: options.isDimmed // Only useful if sticky is set to true
});
// Create a new notification in the container: No notification with the same ID is already open
if (!this._active[notification.id]) {
this._start(notification);
} else { // Use existing notification: increment request count and reset timeout
this._resetTimeout(this._active[notification.id]);
this._incrementRequestCounter(this._active[notification.id]);
notification = this._active[notification.id]; // Clear local new notification since it already exists in this._active
}
return notification.id;
}
/** @method
* @name info
* @public
* @memberof Notification
* @author Arthur Beaulieu
* @since June 2018
* @description Build an info notification
* @param {object} options - The notification options object (see new() arguments since this is an abstraction of new())
* @returns {number} The newly created notification ID */
info(options) {
if (options) {
options.type = 'info';
return this.new(options);
} else {
console.error('Notification.js : No arguments provided for info() method.');
}
}
/** @method
* @name success
* @public
* @memberof Notification
* @author Arthur Beaulieu
* @since June 2018
* @description Build a success notification
* @param {object} options - The notification options object (see new() arguments since this is an abstraction of new())
* @returns {number} The newly created notification ID */
success(options) {
if (options) {
options.type = 'success';
return this.new(options);
} else {
console.error('Notification.js : No arguments provided for success() method.');
}
}
/** @method
* @name warning
* @public
* @memberof Notification
* @author Arthur Beaulieu
* @since June 2018
* @description Build a warning notification
* @param {object} options - The notification options object (see new() arguments since this is an abstraction of new())
* @returns {number} The newly created notification ID */
warning(options) {
if (options) {
options.type = 'warning';
return this.new(options);
} else {
console.error('Notification.js : No arguments provided for warning() method.');
}
}
/** @method
* @name error
* @public
* @memberof Notification
* @author Arthur Beaulieu
* @since June 2018
* @description Build an error notification
* @param {object} options - The notification options object (see new() arguments since this is an abstraction of new())
* @returns {number} The newly created notification ID */
error(options) {
if (options) {
options.type = 'error';
return this.new(options);
} else {
console.error('Notification.js : No arguments provided for error() method.');
}
}
/** @method
* @name dismiss
* @public
* @memberof Notification
* @author Arthur Beaulieu
* @since June 2018
* @description Dismiss a specific notification via its ID
* @param {string} id - The notification ID to dismiss */
dismiss(id) {
window.clearTimeout(this._active[id].timeoutID); // Clear notification timeout
if (this._active[id].requestCount > 1) { // Several request are pending
this._clearRequestCount(this._active[id]); // Clear all pending request
}
this._close(this._active[id]); // Close notification
}
/** @method
* @name dismissAll
* @public
* @memberof Notification
* @author Arthur Beaulieu
* @since June 2018
* @description Clear the notification handler from all its active notifications */
dismissAll() {
if (!this._dismissAllLock && Object.keys(this._active).length !== 0) { // Check that _dimissAllLock is disable and that there is still notification displayed
this._dismissAllLock = true; // dismissAllLock will be unlocked at the last _close() method call
this._queue = {}; // Clear queue object
for (const id in this._active) { // Iterate over notifications
this.dismiss(id);
}
}
}
/** @method
* @name dismissType
* @public
* @memberof Notification
* @author Arthur Beaulieu
* @since June 2018
* @description Dismiss all notifications from a given type
* @param {string} type - <i>succes; info; warning; error;</i> */
dismissType(type) {
if (Object.keys(this._active).length !== 0) { // Check that _dismissAllLock is disable and that there is still notification displayed
for (const id in this._active) { // Iterate over notifications
if (this._active[id].type === type) {
this.dismiss(id);
}
}
}
}
}
export default Notification;