import "./toast.css"; interface ToastOptions { html: string; displayLength: number; inDuration: number; outDuration: number; classes: string; completeCallback?: (toast: Toast) => void; activationPercent: number; } const defaultOptions: ToastOptions = { html: "", displayLength: 4000, inDuration: 300, outDuration: 375, classes: "", activationPercent: 0.7, }; export class Toast { static _container: HTMLElement | null = null; static _toasts: Array = []; element: HTMLElement; options: ToastOptions; isMoving: boolean; startTime: number; startingPosX: number; get activationDistance(): number { return this.element.offsetWidth * this.options.activationPercent; } constructor(options: Partial) { this.options = { ...defaultOptions, ...options }; if (Toast._container == null) { Toast._createContainer(); } this.startTime = Date.now(); this.startingPosX = 0; this.isMoving = false; this.element = document.createElement("div"); this.element.className = "toast " + this.options.classes; this.element.innerHTML = this.options.html; const onDragStart = (e: MouseEvent | TouchEvent) => { e.stopPropagation(); this.startingPosX = this._getposX(e); this.element.style.transition = "all 0s"; this.isMoving = true; document.addEventListener("touchmove", onDragMove); document.addEventListener("touchend", onDragEnd); document.addEventListener("mousemove", onDragMove); document.addEventListener("mouseup", onDragEnd); }; const onDragMove = (e: MouseEvent | TouchEvent) => { e.stopPropagation(); const totalDeltaX = this._getposX(e) - this.startingPosX; this.element.style.transform = `translateX(${totalDeltaX}px)`; this.element.style.opacity = "" + (1 - Math.abs(totalDeltaX / this.activationDistance)); }; const onDragEnd = (e: MouseEvent | TouchEvent) => { const totalDeltaX = this.startingPosX - this._getposX(e); this.isMoving = false; // Remove toast if (Math.abs(totalDeltaX) > this.activationDistance) { this.dismiss(); // Animate toast back to original position } else { this.element.style.transition = "transform .2s, opacity .2s"; this.element.style.transform = ""; this.element.style.opacity = ""; } document.removeEventListener("touchmove", onDragMove); document.removeEventListener("touchend", onDragEnd); document.removeEventListener("mousemove", onDragMove); document.removeEventListener("mouseup", onDragEnd); }; this.element.addEventListener("touchstart", onDragStart); this.element.addEventListener("mousedown", onDragStart); this.element.style.top = `30px`; this.element.style.opacity = `0`; this.element.style.transition = `top ${this.options.inDuration}ms, opacity ${this.options.inDuration}ms`; if (this.options.displayLength < Infinity) { this.update(); } Toast._container?.appendChild(this.element); setTimeout(() => { this.element.style.top = `0px`; this.element.style.opacity = `1`; }, 10); Toast._toasts.push(this); } update(): void { const ellapsed = Date.now() - this.startTime; if (this.options && ellapsed < this.options.displayLength) { this.element.style.setProperty( "--completion", Math.round((ellapsed / this.options.displayLength) * 100) + "%" ); setTimeout(() => this.update(), 20); } else { this.dismiss(); } } _getposX(e: MouseEvent | TouchEvent): number { if (e instanceof MouseEvent) { return e.clientX; } else { return e.touches[0].clientX; } } dismiss(): void { this.element.style.transition = `all ${this.options.outDuration}ms`; this.element.style.marginTop = `-${this.element.offsetHeight}px`; this.element.style.opacity = "0"; setTimeout(() => { Toast._container?.removeChild(this.element); Toast._toasts = Toast._toasts.filter((e) => e !== this); if (Toast._toasts.length === 0) { Toast._removeContainer(); } if (this.options.completeCallback) this.options.completeCallback(this); }, this.options.outDuration); } static dismissAll(): void { for (const toastIndex in Toast._toasts) { Toast._toasts[toastIndex].dismiss(); } } static _createContainer(): void { const container = document.createElement("div"); container.setAttribute("id", "toast-container"); document.body.appendChild(container); Toast._container = container; } static _removeContainer(): void { if (Toast._container) { document.body.removeChild(Toast._container); Toast._container = null; } } } export default function (options: Partial): Toast { return new Toast(options); }