//notifications const smNotifications = document.createElement('template') smNotifications.innerHTML = `
`; customElements.define('sm-notifications', class extends HTMLElement { constructor() { super(); this.shadow = this.attachShadow({ mode: 'open' }).append(smNotifications.content.cloneNode(true)) this.notificationPanel = this.shadowRoot.querySelector('.notification-panel') this.animationOptions = { duration: 300, fill: "forwards", easing: "cubic-bezier(0.175, 0.885, 0.32, 1.275)" } this.push = this.push.bind(this) this.createNotification = this.createNotification.bind(this) this.removeNotification = this.removeNotification.bind(this) this.clearAll = this.clearAll.bind(this) this.remove = this.remove.bind(this) this.handleTouchMove = this.handleTouchMove.bind(this) this.startX = 0; this.currentX = 0; this.endX = 0; this.swipeDistance = 0; this.swipeDirection = ''; this.swipeThreshold = 0; this.startTime = 0; this.swipeTime = 0; this.swipeTimeThreshold = 200; this.currentTarget = null; this.notificationTimeout = 5000; this.mediaQuery = window.matchMedia('(min-width: 640px)') this.handleOrientationChange = this.handleOrientationChange.bind(this) this.isBigViewport = false } set timeout(value) { if (isNaN(value)) return; this.notificationTimeout = value; } randString(length) { let result = ''; const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; for (let i = 0; i < length; i++) result += characters.charAt(Math.floor(Math.random() * characters.length)); return result; } createNotification(message, options = {}) { const { pinned = false, icon, action, timeout = this.notificationTimeout } = options; const notification = document.createElement('div') notification.id = this.randString(8) notification.className = `notification ${pinned ? 'pinned' : ''}` notification.style.setProperty('--timeout', `${timeout}ms`); notification.innerHTML = ` ${icon ? `
${icon}
` : ''} ${message} ${action ? `` : ''} `; if (action) { notification.querySelector('.action').addEventListener('click', action.callback) } notification.querySelector('.close').addEventListener('click', () => { this.removeNotification(notification); }); if (pinned) { } else { setTimeout(() => { this.removeNotification(notification, this.isBigViewport ? 'left' : 'top'); }, timeout); } return notification; } push(message, options = {}) { const notification = this.createNotification(message, options); if (this.isBigViewport) this.notificationPanel.append(notification); else this.notificationPanel.prepend(notification); notification.scrollIntoView({ behavior: 'smooth' }); this.notificationPanel.animate( [ { transform: `translateY(${this.isBigViewport ? '' : '-'}${notification.clientHeight}px)`, }, { transform: `none`, }, ], this.animationOptions ) notification.animate([ { transform: `translateY(-1rem)`, opacity: '0' }, { transform: `none`, opacity: '1' }, ], this.animationOptions).onfinish = (e) => { e.target.commitStyles() e.target.cancel() } return notification.id; } removeNotification(notification, direction = 'left') { if (!notification) return; const sign = direction === 'left' || direction === 'top' ? '-' : '+'; if (!this.isBigViewport && direction === 'top') { notification.animate([ { transform: this.currentX ? `translateY(${this.currentX}px)` : `none`, opacity: '1' }, { transform: `translateY(calc(${sign}${Math.abs(this.currentX)}px ${sign} 1rem))`, opacity: '0' } ], this.animationOptions).onfinish = () => { notification.remove(); }; } else { notification.animate([ { transform: this.currentX ? `translateX(${this.currentX}px)` : `none`, opacity: '1' }, { transform: `translateX(calc(${sign}${Math.abs(this.currentX)}px ${sign} 1rem))`, opacity: '0' } ], this.animationOptions).onfinish = () => { notification.remove(); }; } } remove(id) { const notification = this.notificationPanel.querySelector(`#${id}`); if (!notification) return; this.removeNotification(notification); } clearAll() { Array.from(this.notificationPanel.children).forEach(child => { this.removeNotification(child); }); } handleTouchMove(e) { this.currentX = e.touches[0].clientX - this.startX; this.currentTarget.style.transform = `translateX(${this.currentX}px)`; } handleOrientationChange(e) { this.isBigViewport = e.matches if (e.matches) { // landscape } else { // portrait } } connectedCallback() { this.handleOrientationChange(this.mediaQuery); this.mediaQuery.addEventListener('change', this.handleOrientationChange); this.notificationPanel.addEventListener('touchstart', e => { if (e.target.closest('.close')) { this.removeNotification(e.target.closest('.notification')); } else if (e.target.closest('.notification')) { this.swipeThreshold = e.target.closest('.notification').getBoundingClientRect().width / 2; this.currentTarget = e.target.closest('.notification'); this.startTime = Date.now(); this.startX = e.touches[0].clientX; this.startY = e.touches[0].clientY; this.notificationPanel.addEventListener('touchmove', this.handleTouchMove, { passive: true }); } }, { passive: true }); this.notificationPanel.addEventListener('touchend', e => { this.endX = e.changedTouches[0].clientX; this.endY = e.changedTouches[0].clientY; this.swipeDistance = Math.abs(this.endX - this.startX); this.swipeTime = Date.now() - this.startTime; if (this.endX > this.startX) { this.swipeDirection = 'right'; } else { this.swipeDirection = 'left'; } if (this.swipeTime < this.swipeTimeThreshold) { if (this.swipeDistance > 50) this.removeNotification(this.currentTarget, this.swipeDirection); } else { if (this.swipeDistance > this.swipeThreshold) { this.removeNotification(this.currentTarget, this.swipeDirection); } else { this.currentTarget.animate([ { transform: `translateX(${this.currentX}px)`, }, { transform: `none`, }, ], this.animationOptions).onfinish = (e) => { e.target.commitStyles() e.target.cancel() } } } this.notificationPanel.removeEventListener('touchmove', this.handleTouchMove) this.currentX = 0; }); } disconnectedCallback() { mediaQueryList.removeEventListener('change', handleOrientationChange); } });