/*jshint esversion: 6 */ const smButton = document.createElement('template') smButton.innerHTML = `
`; customElements.define('sm-button', class extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }).append(smButton.content.cloneNode(true)); } static get observedAttributes() { return ['disabled']; } get disabled() { return this.hasAttribute('disabled'); } set disabled(value) { if (value) { this.setAttribute('disabled', ''); } else { this.removeAttribute('disabled'); } } focusIn() { this.focus(); } handleKeyDown(e) { if (!this.hasAttribute('disabled') && (e.key === 'Enter' || e.key === ' ')) { e.preventDefault(); this.click(); } } connectedCallback() { if (!this.hasAttribute('disabled')) { this.setAttribute('tabindex', '0'); } this.setAttribute('role', 'button'); this.addEventListener('keydown', this.handleKeyDown); } attributeChangedCallback(name) { if (name === 'disabled') { if (this.hasAttribute('disabled')) { this.removeAttribute('tabindex'); } else { this.setAttribute('tabindex', '0'); } this.setAttribute('aria-disabled', this.hasAttribute('disabled')); } } }) const smForm = document.createElement('template'); smForm.innerHTML = ` `; customElements.define('sm-form', class extends HTMLElement { constructor() { super() this.attachShadow({ mode: 'open' }).append(smForm.content.cloneNode(true)) this.form = this.shadowRoot.querySelector('form'); this.formElements this.requiredElements this.submitButton this.resetButton this.allRequiredValid = false; this.debounce = this.debounce.bind(this) this._checkValidity = this._checkValidity.bind(this) this.handleKeydown = this.handleKeydown.bind(this) this.reset = this.reset.bind(this) this.elementsChanged = this.elementsChanged.bind(this) } debounce(callback, wait) { let timeoutId = null; return (...args) => { window.clearTimeout(timeoutId); timeoutId = window.setTimeout(() => { callback.apply(null, args); }, wait); }; } _checkValidity() { this.allRequiredValid = this.requiredElements.every(elem => elem.isValid) if (!this.submitButton) return; if (this.allRequiredValid) { this.submitButton.disabled = false; } else { this.submitButton.disabled = true; } } handleKeydown(e) { if (e.key === 'Enter' && !e.target.tagName.includes('TEXTAREA')) { if (this.allRequiredValid) { if (this.submitButton) { this.submitButton.click() } this.dispatchEvent(new CustomEvent('submit', { bubbles: true, composed: true, })) } else { this.requiredElements.find(elem => !elem.isValid).vibrate() } } } reset() { this.formElements.forEach(elem => elem.reset()) } elementsChanged() { this.formElements = [...this.querySelectorAll('sm-input, sm-textarea, sm-checkbox, tags-input, file-input, sm-switch, sm-radio')] this.requiredElements = this.formElements.filter(elem => elem.hasAttribute('required')); this.submitButton = this.querySelector('[variant="primary"], [type="submit"]'); this.resetButton = this.querySelector('[type="reset"]'); if (this.resetButton) { this.resetButton.addEventListener('click', this.reset); } this._checkValidity() } connectedCallback() { const slot = this.shadowRoot.querySelector('slot') slot.addEventListener('slotchange', this.elementsChanged) this.addEventListener('input', this.debounce(this._checkValidity, 100)); this.addEventListener('keydown', this.debounce(this.handleKeydown, 100)); } disconnectedCallback() { this.removeEventListener('input', this.debounce(this._checkValidity, 100)); this.removeEventListener('keydown', this.debounce(this.handleKeydown, 100)); } }) const smInput = document.createElement('template') smInput.innerHTML = `${message}
`; if (pinned) { notification.classList.add('pinned'); composition += ` `; } notification.innerHTML = composition; 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() } return notification.id; } removeNotification(notification, direction = 'left') { 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(); }; } 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('.notification')) { this.swipeThreshold = this.clientWidth / 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; }); this.notificationPanel.addEventListener('click', e => { if (e.target.closest('.close')) { this.removeNotification(e.target.closest('.notification')); } }); 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); } }); class Stack { constructor() { this.items = []; } push(element) { this.items.push(element); } pop() { if (this.items.length == 0) return "Underflow"; return this.items.pop(); } peek() { return this.items[this.items.length - 1]; } } const popupStack = new Stack(); const smPopup = document.createElement('template'); smPopup.innerHTML = ` `; customElements.define('sm-popup', class extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }).append(smPopup.content.cloneNode(true)); this.allowClosing = false; this.isOpen = false; this.pinned = false; this.offset = 0; this.touchStartY = 0; this.touchEndY = 0; this.touchStartTime = 0; this.touchEndTime = 0; this.touchEndAnimation = undefined; this.focusable this.autoFocus this.mutationObserver this.popupContainer = this.shadowRoot.querySelector('.popup-container'); this.backdrop = this.shadowRoot.querySelector('.background'); this.popup = this.shadowRoot.querySelector('.popup'); this.popupBodySlot = this.shadowRoot.querySelector('.popup-body slot'); this.popupHeader = this.shadowRoot.querySelector('.popup-top'); this.resumeScrolling = this.resumeScrolling.bind(this); this.setStateOpen = this.setStateOpen.bind(this); this.show = this.show.bind(this); this.hide = this.hide.bind(this); this.handleTouchStart = this.handleTouchStart.bind(this); this.handleTouchMove = this.handleTouchMove.bind(this); this.handleTouchEnd = this.handleTouchEnd.bind(this); this.detectFocus = this.detectFocus.bind(this); } static get observedAttributes() { return ['open']; } get open() { return this.isOpen; } animateTo(element, keyframes, options) { const anime = element.animate(keyframes, { ...options, fill: 'both' }) anime.finished.then(() => { anime.commitStyles() anime.cancel() }) return anime } resumeScrolling() { const scrollY = document.body.style.top; window.scrollTo(0, parseInt(scrollY || '0') * -1); document.body.style.overflow = ''; document.body.style.top = 'initial'; } setStateOpen() { if (!this.isOpen || this.offset) { const animOptions = { duration: 300, easing: 'ease' } const initialAnimation = (window.innerWidth > 640) ? 'scale(1.1)' : `translateY(${this.offset ? `${this.offset}px` : '100%'})` this.animateTo(this.popup, [ { opacity: this.offset ? 1 : 0, transform: initialAnimation }, { opacity: 1, transform: 'none' }, ], animOptions) } } show(options = {}) { const { pinned = false } = options; if (!this.isOpen) { const animOptions = { duration: 300, easing: 'ease' } popupStack.push({ popup: this, permission: pinned }); if (popupStack.items.length > 1) { this.animateTo(popupStack.items[popupStack.items.length - 2].popup.shadowRoot.querySelector('.popup'), [ { transform: 'none' }, { transform: (window.innerWidth > 640) ? 'scale(0.95)' : 'translateY(-1.5rem)' }, ], animOptions) } this.popupContainer.classList.remove('hide'); if (!this.offset) this.backdrop.animate([ { opacity: 0 }, { opacity: 1 }, ], animOptions) this.setStateOpen() this.dispatchEvent( new CustomEvent("popupopened", { bubbles: true, detail: { popup: this, } }) ); this.pinned = pinned; this.isOpen = true; document.body.style.overflow = 'hidden'; document.body.style.top = `-${window.scrollY}px`; const elementToFocus = this.autoFocus || this.focusable[0]; elementToFocus.tagName.includes('SM-') ? elementToFocus.focusIn() : elementToFocus.focus(); if (!this.hasAttribute('open')) this.setAttribute('open', ''); } } hide() { const animOptions = { duration: 150, easing: 'ease' } this.backdrop.animate([ { opacity: 1 }, { opacity: 0 } ], animOptions) this.animateTo(this.popup, [ { opacity: 1, transform: (window.innerWidth > 640) ? 'none' : `translateY(${this.offset ? `${this.offset}px` : '0'})` }, { opacity: 0, transform: (window.innerWidth > 640) ? 'scale(1.1)' : 'translateY(100%)' }, ], animOptions).finished .finally(() => { this.popupContainer.classList.add('hide'); this.popup.style = '' this.removeAttribute('open'); if (this.forms.length) { this.forms.forEach(form => form.reset()); } this.dispatchEvent( new CustomEvent("popupclosed", { bubbles: true, detail: { popup: this, } }) ); this.isOpen = false; }) popupStack.pop(); if (popupStack.items.length) { this.animateTo(popupStack.items[popupStack.items.length - 1].popup.shadowRoot.querySelector('.popup'), [ { transform: (window.innerWidth > 640) ? 'scale(0.95)' : 'translateY(-1.5rem)' }, { transform: 'none' }, ], animOptions) } else { this.resumeScrolling(); } } handleTouchStart(e) { this.offset = 0 this.popupHeader.addEventListener('touchmove', this.handleTouchMove, { passive: true }); this.popupHeader.addEventListener('touchend', this.handleTouchEnd, { passive: true }); this.touchStartY = e.changedTouches[0].clientY; this.touchStartTime = e.timeStamp; } handleTouchMove(e) { if (this.touchStartY < e.changedTouches[0].clientY) { this.offset = e.changedTouches[0].clientY - this.touchStartY; this.touchEndAnimation = window.requestAnimationFrame(() => { this.popup.style.transform = `translateY(${this.offset}px)`; }); } } handleTouchEnd(e) { this.touchEndTime = e.timeStamp; cancelAnimationFrame(this.touchEndAnimation); this.touchEndY = e.changedTouches[0].clientY; this.threshold = this.popup.getBoundingClientRect().height * 0.3; if (this.touchEndTime - this.touchStartTime > 200) { if (this.touchEndY - this.touchStartY > this.threshold) { if (this.pinned) { this.setStateOpen(); return; } else this.hide(); } else { this.setStateOpen(); } } else { if (this.touchEndY > this.touchStartY) if (this.pinned) { this.setStateOpen(); return; } else this.hide(); } this.popupHeader.removeEventListener('touchmove', this.handleTouchMove, { passive: true }); this.popupHeader.removeEventListener('touchend', this.handleTouchEnd, { passive: true }); } detectFocus(e) { if (e.key === 'Tab') { const lastElement = this.focusable[this.focusable.length - 1]; const firstElement = this.focusable[0]; if (e.shiftKey && document.activeElement === firstElement) { e.preventDefault(); lastElement.tagName.includes('SM-') ? lastElement.focusIn() : lastElement.focus(); } else if (!e.shiftKey && document.activeElement === lastElement) { e.preventDefault(); firstElement.tagName.includes('SM-') ? firstElement.focusIn() : firstElement.focus(); } } } updateFocusableList() { this.focusable = this.querySelectorAll('sm-button:not([disabled]), button:not([disabled]), [href], sm-input, input:not([readonly]), sm-select, select, sm-checkbox, sm-textarea, textarea, [tabindex]:not([tabindex="-1"])') this.autoFocus = this.querySelector('[autofocus]') } connectedCallback() { this.popupBodySlot.addEventListener('slotchange', () => { this.forms = this.querySelectorAll('sm-form'); this.updateFocusableList() }); this.popupContainer.addEventListener('mousedown', e => { if (e.target === this.popupContainer && !this.pinned) { if (this.pinned) { this.setStateOpen(); } else this.hide(); } }); const resizeObserver = new ResizeObserver(entries => { for (let entry of entries) { if (entry.contentBoxSize) { // Firefox implements `contentBoxSize` as a single content rect, rather than an array const contentBoxSize = Array.isArray(entry.contentBoxSize) ? entry.contentBoxSize[0] : entry.contentBoxSize; this.threshold = contentBoxSize.blockSize.height * 0.3; } else { this.threshold = entry.contentRect.height * 0.3; } } }); resizeObserver.observe(this); this.mutationObserver = new MutationObserver(entries => { this.updateFocusableList() }) this.mutationObserver.observe(this, { attributes: true, childList: true, subtree: true }) this.addEventListener('keydown', this.detectFocus); this.popupHeader.addEventListener('touchstart', this.handleTouchStart, { passive: true }); } disconnectedCallback() { this.removeEventListener('keydown', this.detectFocus); resizeObserver.unobserve(); this.mutationObserver.disconnect() this.popupHeader.removeEventListener('touchstart', this.handleTouchStart, { passive: true }); } attributeChangedCallback(name) { if (name === 'open') { if (this.hasAttribute('open')) { this.show(); } } } }); const smSwitch = document.createElement('template') smSwitch.innerHTML = ` ` customElements.define('sm-switch', class extends HTMLElement { constructor() { super() this.attachShadow({ mode: 'open' }).append(smSwitch.content.cloneNode(true)) this.switch = this.shadowRoot.querySelector('.switch'); this.input = this.shadowRoot.querySelector('input') this.isChecked = false this.isDisabled = false this.dispatch = this.dispatch.bind(this) } static get observedAttributes() { return ['disabled', 'checked'] } get disabled() { return this.isDisabled } set disabled(val) { if (val) { this.setAttribute('disabled', '') } else { this.removeAttribute('disabled') } } get checked() { return this.isChecked } set checked(value) { if (value) { this.setAttribute('checked', '') } else { this.removeAttribute('checked') } } reset() { } dispatch() { this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true, detail: { value: this.isChecked } })) } connectedCallback() { this.addEventListener('keydown', e => { if (e.key === ' ' && !this.isDisabled) { e.preventDefault() this.input.click() } }) this.input.addEventListener('click', e => { if (this.input.checked) this.checked = true else this.checked = false this.dispatch() }) } attributeChangedCallback(name, oldValue, newValue) { if (oldValue !== newValue) { if (name === 'disabled') { if (this.hasAttribute('disabled')) { this.disabled = true } else { this.disabled = false } } else if (name === 'checked') { if (this.hasAttribute('checked')) { this.isChecked = true this.input.checked = true } else { this.isChecked = false this.input.checked = false } } } } }) const themeToggle = document.createElement('template'); themeToggle.innerHTML = ` `; class ThemeToggle extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }).append(themeToggle.content.cloneNode(true)); this.isChecked = false; this.hasTheme = 'light'; this.toggleState = this.toggleState.bind(this); this.fireEvent = this.fireEvent.bind(this); this.handleThemeChange = this.handleThemeChange.bind(this); } static get observedAttributes() { return ['checked']; } daylight() { this.hasTheme = 'light'; document.body.dataset.theme = 'light'; this.setAttribute('aria-checked', 'false'); } nightlight() { this.hasTheme = 'dark'; document.body.dataset.theme = 'dark'; this.setAttribute('aria-checked', 'true'); } toggleState() { this.toggleAttribute('checked'); this.fireEvent(); } handleKeyDown(e) { if (e.key === ' ') { this.toggleState(); } } handleThemeChange(e) { if (e.detail.theme !== this.hasTheme) { if (e.detail.theme === 'dark') { this.setAttribute('checked', ''); } else { this.removeAttribute('checked'); } } } fireEvent() { this.dispatchEvent( new CustomEvent('themechange', { bubbles: true, composed: true, detail: { theme: this.hasTheme } }) ); } connectedCallback() { this.setAttribute('role', 'switch'); this.setAttribute('aria-label', 'theme toggle'); if (localStorage.getItem(`${window.location.hostname}-theme`) === "dark") { this.nightlight(); this.setAttribute('checked', ''); } else if (localStorage.getItem(`${window.location.hostname}-theme`) === "light") { this.daylight(); this.removeAttribute('checked'); } else { if (window.matchMedia(`(prefers-color-scheme: dark)`).matches) { this.nightlight(); this.setAttribute('checked', ''); } else { this.daylight(); this.removeAttribute('checked'); } } this.addEventListener("click", this.toggleState); this.addEventListener("keydown", this.handleKeyDown); document.addEventListener('themechange', this.handleThemeChange); } disconnectedCallback() { this.removeEventListener("click", this.toggleState); this.removeEventListener("keydown", this.handleKeyDown); document.removeEventListener('themechange', this.handleThemeChange); } attributeChangedCallback(name, oldVal, newVal) { if (name === 'checked') { if (this.hasAttribute('checked')) { this.nightlight(); localStorage.setItem(`${window.location.hostname}-theme`, "dark"); } else { this.daylight(); localStorage.setItem(`${window.location.hostname}-theme`, "light"); } } } } window.customElements.define('theme-toggle', ThemeToggle); const smCopy = document.createElement('template'); smCopy.innerHTML = `