UNPKG

axentix

Version:

Axentix is a framework mixing fully customizable components & utility-first classes, leaving the design choice to the developer.

312 lines (259 loc) 9.33 kB
import { getCssVar, registerComponent, instances } from '../../utils/config'; import { createEvent, extend, getClientXPosition, getInstanceByType, getPointerType, } from '../../utils/utilities'; interface IToastOptions { animationDuration?: number; duration?: number; classes?: string; position?: 'right' | 'left'; direction?: 'top' | 'bottom'; mobileDirection?: 'bottom' | 'top'; offset?: { x?: string; y?: string; mobileX?: string; mobileY?: string }; isClosable?: boolean; isSwipeable?: boolean; closableContent?: string; loading?: { enabled?: boolean; border?: string; }; } const ToastOptions: IToastOptions = { animationDuration: 400, duration: 4000, classes: '', position: 'right', direction: 'top', mobileDirection: 'bottom', offset: { x: '5%', y: '0%', mobileX: '10%', mobileY: '0%' }, isClosable: false, isSwipeable: true, closableContent: 'x', loading: { enabled: true, border: '2px solid #E2E2E2', }, }; export class Toast { static getDefaultOptions = () => ToastOptions; options: IToastOptions; id: string; #content: string; #toasters: { right?: HTMLElement; left?: HTMLElement; }; #pointerType: string; #touchStartRef: any; #touchMoveRef: any; #touchReleaseRef: any; #isPressed: boolean; #xStart: number; constructor(content: string, options?: IToastOptions) { if (getInstanceByType('Toast').length > 0) { console.error("[Axentix] Toast: Don't try to create multiple toast instances"); return; } instances.push({ type: 'Toast', instance: this }); this.id = Math.random().toString().split('.')[1]; this.#content = content; this.options = extend(Toast.getDefaultOptions(), options); this.#pointerType = getPointerType(); // @ts-ignore this.options.position = this.options.position.toLowerCase(); // @ts-ignore this.options.direction = this.options.direction.toLowerCase(); // @ts-ignore this.options.mobileDirection = this.options.mobileDirection.toLowerCase(); this.#toasters = {}; } destroy() { const index = instances.findIndex((ins) => ins.instance.id === this.id); instances.splice(index, 1); } /** Create toast container */ #createToaster() { let toaster = document.createElement('div'); const positionList = ['right', 'left']; if (!positionList.includes(this.options.position)) this.options.position = 'right'; if (this.options.position === 'right') toaster.style.right = this.options.offset.x; else toaster.style.left = this.options.offset.x; const directionList = ['bottom', 'top']; if (!directionList.includes(this.options.direction)) this.options.direction = 'top'; if (this.options.direction === 'top') toaster.style.top = this.options.offset.y; else toaster.style.bottom = this.options.offset.y; if (!directionList.includes(this.options.mobileDirection)) this.options.mobileDirection = 'bottom'; toaster.style.setProperty( getCssVar('toaster-m-width'), 100 - (this.options.offset.mobileX.slice(0, -1) as any) + '%' ); toaster.style.setProperty(getCssVar('toaster-m-offset'), this.options.offset.mobileY); if (this.options.loading.enabled) toaster.style.setProperty(getCssVar('toast-loading-border'), this.options.loading.border); toaster.className = `toaster toaster-${this.options.position} toast-${this.options.direction} toaster-m-${this.options.mobileDirection}`; this.#toasters[this.options.position] = toaster; document.body.appendChild(toaster); } /** Remove toast container */ #removeToaster() { for (const key in this.#toasters) { let toaster: HTMLElement = this.#toasters[key]; if (toaster.childElementCount <= 0) { toaster.remove(); delete this.#toasters[key]; } } } /** Toast in animation */ #fadeInToast(toast: HTMLElement) { setTimeout(() => { createEvent(toast, 'toast.show'); if (this.options.loading.enabled) { toast.classList.add('toast-loading'); toast.style.setProperty(getCssVar('toast-loading-duration'), this.options.duration + 'ms'); } toast.classList.add('toast-animated'); setTimeout(() => { createEvent(toast, 'toast.shown'); if (this.options.loading.enabled) toast.classList.add('toast-load'); }, this.options.animationDuration); }, 50); } /** Toast out animation */ #fadeOutToast(toast: HTMLElement) { setTimeout(() => { createEvent(toast, 'toast.hide'); this.#hide(toast); }, this.options.duration + this.options.animationDuration); } /** Anim out toast */ #animOut(toast: HTMLElement) { toast.style.transitionTimingFunction = 'cubic-bezier(0.445, 0.05, 0.55, 0.95)'; toast.style.paddingTop = '0'; toast.style.paddingBottom = '0'; toast.style.margin = '0'; toast.style.height = '0'; } #setupSwipeListeners(toast) { this.#touchStartRef = this.#handleDragStart.bind(this); this.#touchMoveRef = this.#handleDragMove.bind(this); this.#touchReleaseRef = this.#handleDragRelease.bind(this); toast.addEventListener( `${this.#pointerType}${this.#pointerType === 'touch' ? 'start' : 'down'}`, this.#touchStartRef ); toast.addEventListener(`${this.#pointerType}move`, this.#touchMoveRef); toast.addEventListener( `${this.#pointerType}${this.#pointerType === 'touch' ? 'end' : 'up'}`, this.#touchReleaseRef ); toast.addEventListener( this.#pointerType === 'pointer' ? 'pointerleave' : 'mouseleave', this.#touchReleaseRef ); } #setupCloseListeners(toast: HTMLDivElement) { toast.querySelectorAll("[data-toast-close]").forEach(el => { el.addEventListener('click', () => { this.#hide(toast); }) }); } #handleDragStart(e: any) { if ((e.target as HTMLElement).closest('.toast-trigger')) return; const toast = e.target.closest('.toast') as HTMLElement; if (toast.dataset.closing) return; this.#xStart = getClientXPosition(e); this.#isPressed = true; toast.style.transitionProperty = 'height, margin, padding, transform, box-shadow'; } #handleDragMove(e: any) { if (!this.#isPressed) return; const toast: HTMLElement = e.target.closest('.toast'); const client = toast.getBoundingClientRect(); const absDiff = Math.abs(getClientXPosition(e) - this.#xStart); toast.style.left = getClientXPosition(e) - this.#xStart + 'px'; toast.style.opacity = absDiff < client.width ? (0.99 - absDiff / client.width).toString() : '0.01'; } #handleDragRelease(e: any) { if (!this.#isPressed) return; if (e.cancelable) e.preventDefault(); this.#isPressed = false; const toast = e.target.closest('.toast'); toast.style.transitionProperty = 'height, margin, opacity, padding, transform, box-shadow, left'; if (Math.abs(getClientXPosition(e) - this.#xStart) > toast.getBoundingClientRect().width / 2) { this.#hide(toast); toast.dataset.closing = 'true'; } else { toast.style.left = '0px'; toast.style.opacity = 1; } } #handleSwipe(toast: HTMLElement) { this.#setupSwipeListeners(toast); } #createToast() { let toast = document.createElement('div'); toast.className = 'toast shadow-1 ' + this.options.classes; toast.innerHTML = this.#content; toast.style.transitionDuration = this.options.animationDuration + 'ms'; if (this.options.isClosable) { let trigger = document.createElement('div'); trigger.className = 'toast-trigger'; trigger.innerHTML = this.options.closableContent; (trigger as any).listenerRef = this.#hide.bind(this, toast, trigger); trigger.addEventListener('click', (trigger as any).listenerRef); toast.appendChild(trigger); } if (this.options.isSwipeable) this.#handleSwipe(toast); this.#fadeInToast(toast); this.#setupCloseListeners(toast); this.#toasters[this.options.position].appendChild(toast); this.#fadeOutToast(toast); const height = toast.clientHeight; toast.style.height = height + 'px'; } /** Hide toast */ #hide(toast: HTMLElement, trigger?: HTMLElement, e?: Event) { if ((toast as any).isAnimated) return; let timer = 1; if (e) { e.preventDefault(); timer = 0; if (this.options.isClosable) trigger.removeEventListener('click', (trigger as any).listenerRef); } toast.style.opacity = '0'; (toast as any).isAnimated = true; const delay = timer * this.options.animationDuration + this.options.animationDuration; setTimeout(() => { this.#animOut(toast); }, delay / 2); setTimeout(() => { toast.remove(); createEvent(toast, 'toast.remove'); this.#removeToaster(); }, delay * 1.45); } /** Showing the toast */ show() { try { if (!Object.keys(this.#toasters).includes(this.options.position)) this.#createToaster(); this.#createToast(); } catch (error) { console.error('[Axentix] Toast error', error); } } /** Change */ change(content: string, options?: IToastOptions) { this.#content = content; this.options = extend(this.options, options); } } registerComponent({ class: Toast, name: 'Toast', });