toastr2
Version:
ToastrJS is a JavaScript library for Gnome / Growl type non-blocking notifications. jQuery is required. The goal is to create a simple core library that can be customized and extended.
633 lines (514 loc) • 15.8 kB
text/typescript
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<T> = {
[P in keyof T]-?: T[P];
}
export type ToastType = {
info?: string;
error?: string;
warning?: string;
success?: string;
};
export type RequiredToastType = Required<ToastType>;
export type ToastrOptions<T = ToastType> = {
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<ToastrOptions<RequiredToastType>> = {
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: '<button type="button">×</button>',
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<ToastrOptions> = 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<ToastrOptions> = 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<ToastrOptions> = 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, '<')
.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;