//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.handlePointerMove = this.handlePointerMove.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.mediaQuery = window.matchMedia('(min-width: 640px)') this.handleOrientationChange = this.handleOrientationChange.bind(this) this.isLandscape = false } 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 } = options; const notification = document.createElement('div') notification.id = this.randString(8) notification.className = `notification ${pinned ? 'pinned' : ''}` const iconContainer = document.createElement('div') iconContainer.className = 'icon-container' iconContainer.innerHTML = icon const output = document.createElement('output') output.textContent = message notification.append(iconContainer, output) if (action) { const button = document.createElement('button') button.className = 'action' button.innerText = action.label button.addEventListener('click', action.callback) } if (pinned) { const button = document.createElement('button') button.className = 'close' button.innerHTML = ` ` button.addEventListener('click', () => { this.remove(notification.id) }) notification.append(button) } return notification; } push(message, options = {}) { const notification = this.createNotification(message, options); if (this.isLandscape) this.notificationPanel.append(notification); else this.notificationPanel.prepend(notification); this.notificationPanel.animate( [ { transform: `translateY(${this.isLandscape ? '' : '-'}${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() } if (notification.querySelector('.action')) notification.querySelector('.action').addEventListener('click', options.action.callback) return notification.id; } removeNotification(notification, direction = 'left') { if (!notification) return; const sign = direction === 'left' ? '-' : '+'; 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); }); } handlePointerMove(e) { this.currentX = e.clientX - this.startX; this.currentTarget.style.transform = `translateX(${this.currentX}px)`; } handleOrientationChange(e) { this.isLandscape = e.matches if (e.matches) { // landscape } else { // portrait } } connectedCallback() { this.handleOrientationChange(this.mediaQuery); this.mediaQuery.addEventListener('change', this.handleOrientationChange); this.notificationPanel.addEventListener('pointerdown', 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.currentTarget.setPointerCapture(e.pointerId); this.startTime = Date.now(); this.startX = e.clientX; this.startY = e.clientY; this.notificationPanel.addEventListener('pointermove', this.handlePointerMove); } }); this.notificationPanel.addEventListener('pointerup', e => { this.endX = e.clientX; this.endY = e.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('pointermove', this.handlePointerMove) this.notificationPanel.releasePointerCapture(e.pointerId); this.currentX = 0; }); const observer = new MutationObserver(mutationList => { mutationList.forEach(mutation => { if (mutation.type === 'childList') { if (mutation.addedNodes.length && !mutation.addedNodes[0].classList.contains('pinned')) { setTimeout(() => { this.removeNotification(mutation.addedNodes[0]); }, 5000); } } }); }); observer.observe(this.notificationPanel, { childList: true, }); } disconnectedCallback() { mediaQueryList.removeEventListener('change', handleOrientationChange); } });