import merge from 'lodash/merge'; import ProgressBar from './additions/ProgressBar'; import './toastr.scss'; import { version } from '../package.json'; import addClasses from './helpers/addClasses'; type Required = { [P in keyof T]-?: T[P]; } export type ToastType = { info?: string; error?: string; warning?: string; success?: string; }; export type RequiredToastType = Required; export type ToastrOptions = { tapToDismiss?: boolean; toastClass?: string | string[]; containerId?: string; debug?: boolean; showMethod?: 'fadeIn' | 'slideDown' | 'show'; showDuration?: number; showEasing?: 'swing' | 'linear'; onShown?: () => void; hideMethod?: 'fadeOut'; hideDuration?: number; hideEasing?: 'swing'; onHidden?: () => void; closeMethod?: boolean; closeDuration?: number | false; closeEasing?: boolean; closeOnHover?: boolean; extendedTimeOut?: number; iconClasses?: T; iconClass?: string | string[]; positionClass?: string | string[]; timeOut?: number; // Set timeOut and extendedTimeOut to 0 to make it sticky titleClass?: string | string[]; messageClass?: string | string[]; escapeHtml?: boolean; target?: string; closeHtml?: string; closeClass?: string | string[]; newestOnTop?: boolean; preventDuplicates?: boolean; progressBar?: boolean; progressClass?: string | string[]; onclick?: (event: MouseEvent) => void; onCloseClick?: (event: Event) => void; closeButton?: boolean; rtl?: boolean; } export type NotifyMap = { type: string; optionsOverride?: ToastrOptions; iconClass: string; title?: string; message?: string; } class Toastr { private listener: any; private toastId = 0; private previousToast: string | null = null; private toastType: RequiredToastType = { info: 'info', error: 'error', warning: 'warning', success: 'success', }; private version = version; public options: Required> = { tapToDismiss: true, toastClass: 'toast', containerId: 'toast-container', debug: false, showMethod: 'fadeIn', // fadeIn, slideDown, and show are built into jQuery showDuration: 300, showEasing: 'swing', // swing and linear are built into jQuery onShown: () => { }, hideMethod: 'fadeOut', hideDuration: 1000, hideEasing: 'swing', onHidden: () => { }, closeMethod: false, closeDuration: false, closeEasing: false, closeOnHover: true, extendedTimeOut: 1000, iconClasses: { error: 'toast-error', info: 'toast-info', success: 'toast-success', warning: 'toast-warning', }, iconClass: 'toast-info', positionClass: 'toast-top-right', timeOut: 5000, // Set timeOut and extendedTimeOut to 0 to make it sticky titleClass: 'toast-title', messageClass: 'toast-message', escapeHtml: false, target: 'body', closeHtml: '', closeClass: 'toast-close-button', newestOnTop: true, preventDuplicates: false, progressBar: false, progressClass: 'toast-progress', rtl: false, onCloseClick: () => { }, closeButton: false, onclick: () => { }, }; public $container: HTMLElement = document.createElement('div'); public constructor(options?: ToastrOptions) { this.options = merge({}, this.options, options); this.createContainer(); } public createContainer(): HTMLElement { this.$container = document.createElement('div'); this.$container.setAttribute('id', this.options.containerId); addClasses(this.$container, this.options.positionClass); const target = document.getElementsByTagName(this.options.target); if (target && target[0]) { target[0].appendChild(this.$container); } return this.$container; } public getContainer(options: Partial = this.options, create = false): HTMLElement { const $container = document.getElementById(options.containerId || ''); if ($container) { this.$container = $container; return this.$container; } if (create) { this.$container = this.createContainer(); } return this.$container; } public error( message?: string, title?: string, optionsOverride?: ToastrOptions, ): HTMLElement | null { return this.notify({ type: this.toastType.error, iconClass: this.options.iconClasses.error, message, optionsOverride, title, }); } public warning( message?: string, title?: string, optionsOverride?: ToastrOptions, ): HTMLElement | null { return this.notify({ type: this.toastType.warning, iconClass: this.options.iconClasses.warning, message, optionsOverride, title, }); } public success( message?: string, title?: string, optionsOverride?: ToastrOptions, ): HTMLElement | null { return this.notify({ type: this.toastType.success, iconClass: this.options.iconClasses.success, message, optionsOverride, title, }); } public info( message?: string, title?: string, optionsOverride?: ToastrOptions, ): HTMLElement | null { return this.notify({ type: this.toastType.info, iconClass: this.options.iconClasses.info, message, optionsOverride, title, }); } public subscribe(callback: (response: Toastr) => void): void { this.listener = callback; } public publish(args: Toastr): void { if (!this.listener) { return; } this.listener(args); } public clear(toastElement?: HTMLElement | null, clearOptions: { force?: boolean } = {}) { if (!this.$container) { this.getContainer(this.options); } if (!this.clearToast(toastElement, this.options, clearOptions)) { this.clearContainer(this.options); } } public remove(toastElement?: HTMLElement | null) { if (!this.$container) { this.getContainer(this.options); } if (!this.$container) { return; } if (toastElement && toastElement !== document.activeElement) { this.removeToast(toastElement); return; } if (!this.$container.hasChildNodes()) { const parentNode = this.$container.parentElement; if (parentNode) { parentNode.removeChild(this.$container); } } } public removeToast(toastElement: HTMLElement) { if (!this.$container) { this.getContainer(); } if (!this.$container || !toastElement.parentNode) { return; } // todo set after visible state // as this will be a transition of css toastElement.parentNode.removeChild(toastElement); // check if visible if (toastElement.offsetWidth > 0 && toastElement.offsetHeight > 0) { return; } // todo check if null makes sense // toastElement = null; if (!this.$container.hasChildNodes()) { if (this.$container.parentNode) { this.$container.parentNode.removeChild(this.$container); } this.previousToast = null; } } private clearContainer(options: Partial = this.options) { if (!this.$container) { return; } const toastsToClear = Array.from(this.$container.childNodes) as HTMLElement[]; for (let i = toastsToClear.length - 1; i >= 0; i -= 1) { this.clearToast(toastsToClear[i], options); } } private clearToast( toastElement?: HTMLElement | null, // eslint-disable-next-line no-unused-vars options: Partial = this.options, clearOptions: { force?: boolean } = {}, ): boolean { if (!toastElement) { return false; } const force = clearOptions.force || false; if (toastElement && (force || toastElement !== document.activeElement)) { // todo hide effect this.removeToast(toastElement); // toastElement[options.hideMethod]({ // duration: options.hideDuration, // easing: options.hideEasing, // complete: function () { removeToast(toastElement); } // }); return true; } return false; } private notify(map: NotifyMap): HTMLElement | null { let { options } = this; let iconClass = map.iconClass || this.options.iconClass; const shouldExit = (opts: ToastrOptions, exitMap: NotifyMap): boolean => { if (opts.preventDuplicates) { if (exitMap.message === this.previousToast) { return true; } this.previousToast = exitMap.message || ''; } return false; }; if (typeof map.optionsOverride !== 'undefined') { options = merge({}, options, map.optionsOverride); iconClass = map.optionsOverride.iconClass || iconClass; } if (shouldExit(options, map)) { return null; } this.toastId += 1; this.$container = this.getContainer(options, true); let intervalId: NodeJS.Timeout | null = null; let progressBar: null | ProgressBar = null; const toastElement = document.createElement('div'); const $titleElement = document.createElement('div'); const $messageElement = document.createElement('div'); const createdElement = document.createElement('div'); createdElement.innerHTML = options.closeHtml.trim(); const closeElement = createdElement.firstChild as HTMLElement | null; const response: any = { toastId: this.toastId, state: 'visible', startTime: new Date(), endTime: undefined, options, map, }; const hideToast = (override: any = null): void => { // const method = override && this.options.closeMethod !== false // ? this.options.closeMethod // : this.options.hideMethod; // const duration = override && this.options.closeDuration !== false // ? this.options.closeDuration // : this.options.hideDuration; // const easing = override && this.options.closeEasing !== false // ? this.options.closeEasing // : this.options.hideEasing; if (toastElement === document.activeElement && !override) { return; } if (progressBar) { progressBar.stop(); } // todo fade out toast this.removeToast(toastElement); if (intervalId) { clearTimeout(intervalId); } if (options.onHidden && response.state !== 'hidden') { options.onHidden(); } response.state = 'hidden'; response.endTime = new Date(); this.publish(response); // return toastElement[method]({ // duration: duration, // easing: easing, // }); }; const escapeHtml = (source: string | null): string => { const newSource = source !== null ? source : ''; return newSource .replace(/&/g, '&') .replace(/"/g, '"') .replace(/'/g, ''') .replace(//g, '>'); }; const setAria = (): void => { let ariaValue = ''; switch (iconClass) { case 'toast-success': case 'toast-info': ariaValue = 'polite'; break; default: ariaValue = 'assertive'; } toastElement.setAttribute('aria-live', ariaValue); }; const delayedHideToast = (): void => { if (options.timeOut > 0 || options.extendedTimeOut > 0) { intervalId = setTimeout(hideToast, options.extendedTimeOut); if (progressBar) { progressBar.reset(options.extendedTimeOut); progressBar.start(); } } }; const stickAround = (): void => { if (intervalId) { clearTimeout(intervalId); } if (progressBar) { progressBar.stop(); } // todo // toastElement.stop(true, true)[options.showMethod]( // {duration: options.showDuration, easing: options.showEasing} // ); }; const handleEvents = (): void => { if (options.closeOnHover) { toastElement.addEventListener('mouseover', () => stickAround()); toastElement.addEventListener('mouseout', () => delayedHideToast()); } if (!options.onclick && options.tapToDismiss) { toastElement.addEventListener('click', hideToast); } if (options.closeButton && closeElement) { closeElement.addEventListener('click', (event) => { if (event.stopPropagation) { event.stopPropagation(); } else if (event.cancelBubble !== undefined && event.cancelBubble !== true) { // eslint-disable-next-line no-param-reassign event.cancelBubble = true; } if (options.onCloseClick) { options.onCloseClick(event); } hideToast(true); }); } if (options.onclick) { toastElement.addEventListener('click', (event) => { // ts needs another check here if (options.onclick) { options.onclick(event); } hideToast(); }); } }; const setTitle = (): void => { if (map.title) { let suffix = map.title; if (options.escapeHtml) { suffix = escapeHtml(map.title); } $titleElement.innerHTML = suffix; addClasses($titleElement, options.titleClass); toastElement.appendChild($titleElement); } }; const setMessage = (): void => { if (map.message) { let suffix = map.message; if (options.escapeHtml) { suffix = escapeHtml(map.message); } $messageElement.innerHTML = suffix; addClasses($messageElement, options.messageClass); toastElement.appendChild($messageElement); } }; const setCloseButton = (): void => { if (options.closeButton && closeElement) { addClasses(closeElement, options.closeClass); closeElement.setAttribute('role', 'button'); toastElement.insertBefore(closeElement, toastElement.firstChild); } }; const setProgressBar = (): void => { if (options.progressBar) { progressBar = new ProgressBar(toastElement, options.progressClass); } }; const setRTL = (): void => { if (options.rtl) { addClasses(toastElement, 'rtl'); } }; const setIcon = (): void => { if (iconClass) { addClasses(toastElement, options.toastClass, iconClass); } }; const setSequence = (): void => { if (options.newestOnTop) { this.$container.insertBefore(toastElement, this.$container.firstChild); } else { this.$container.appendChild(toastElement); } }; const displayToast = (): void => { // todo hide toast // toastElement.hide(); // todo fade out toast if (options.onShown) { options.onShown(); } // toastElement[options.showMethod]( // eslint-disable-next-line // {duration: options.showDuration, easing: options.showEasing, complete: options.onShown} // ); if (options.timeOut > 0) { intervalId = setTimeout(hideToast, options.timeOut); if (progressBar) { progressBar.reset(options.timeOut); progressBar.start(); } } }; const personalizeToast = (): void => { setIcon(); setTitle(); setMessage(); setCloseButton(); setProgressBar(); setRTL(); setSequence(); setAria(); }; personalizeToast(); displayToast(); handleEvents(); this.publish(response); if (options.debug && console) { // eslint-disable-next-line no-console console.log(response); } return toastElement; } } export default Toastr;