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.code === 'Space')) { 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 smCarousel = document.createElement('template') smCarousel.innerHTML = ` `; customElements.define('sm-carousel', class extends HTMLElement { constructor() { super() this.attachShadow({ mode: 'open' }).append(smCarousel.content.cloneNode(true)) this.isAutoPlaying = false this.autoPlayInterval = 5000 this.autoPlayTimeout this.initialTimeout this.activeSlideNum = 0 this.carouselItems this.indicators this.showIndicator = false this.carousel = this.shadowRoot.querySelector('.carousel') this.carouselContainer = this.shadowRoot.querySelector('.carousel-container') this.carouselSlot = this.shadowRoot.querySelector('slot') this.navButtonRight = this.shadowRoot.querySelector('.carousel__button--right') this.navButtonLeft = this.shadowRoot.querySelector('.carousel__button--left') this.indicatorsContainer = this.shadowRoot.querySelector('.indicators') this.scrollLeft = this.scrollLeft.bind(this) this.scrollRight = this.scrollRight.bind(this) this.handleIndicatorClick = this.handleIndicatorClick.bind(this) this.showSlide = this.showSlide.bind(this) this.nextSlide = this.nextSlide.bind(this) this.autoPlay = this.autoPlay.bind(this) this.startAutoPlay = this.startAutoPlay.bind(this) this.stopAutoPlay = this.stopAutoPlay.bind(this) } static get observedAttributes() { return ['indicator', 'autoplay', 'interval'] } scrollLeft() { this.carousel.scrollBy({ left: -this.scrollDistance, behavior: 'smooth' }) } scrollRight() { this.carousel.scrollBy({ left: this.scrollDistance, behavior: 'smooth' }) } showSlide(slideNum) { this.carousel.scrollTo({ left: (this.carouselItems[slideNum].getBoundingClientRect().left - this.carousel.getBoundingClientRect().left + this.carousel.scrollLeft), behavior: 'smooth' }) } nextSlide() { if (!this.carouselItems) return let showSlideNo = (this.activeSlideNum + 1) < this.carouselItems.length ? this.activeSlideNum + 1 : 0 this.showSlide(showSlideNo) } autoPlay() { this.nextSlide() if (this.isAutoPlaying) { this.autoPlayTimeout = setTimeout(() => { this.autoPlay() }, this.autoPlayInterval); } } startAutoPlay() { this.setAttribute('autoplay', '') } stopAutoPlay() { this.removeAttribute('autoplay') } createIndicator(index) { let indicator = document.createElement('div') indicator.classList.add('indicator') indicator.dataset.rank = index return indicator } handleIndicatorClick(e) { if (e.target.closest('.indicator')) { const slideNum = parseInt(e.target.closest('.indicator').dataset.rank) if (this.activeSlideNum !== slideNum) { this.showSlide(slideNum) } } } handleKeyDown(e) { if (e.code === 'ArrowLeft') this.scrollRight() else if (e.code === 'ArrowRight') this.scrollRight() } connectedCallback() { let frag = document.createDocumentFragment(); this.carouselSlot.addEventListener('slotchange', e => { this.carouselItems = this.carouselSlot.assignedElements() this.carouselItems.forEach(item => allElementsObserver.observe(item)) if (this.carouselItems.length > 0) { firstOptionObserver.observe(this.carouselItems[0]) lastOptionObserver.observe(this.carouselItems[this.carouselItems.length - 1]) } else { navButtonLeft.classList.add('hide') navButtonRight.classList.add('hide') firstOptionObserver.disconnect() lastOptionObserver.disconnect() } if (this.showIndicator) { this.indicatorsContainer.innerHTML = `` this.carouselItems.forEach((item, index) => { frag.append( this.createIndicator(index) ) item.dataset.rank = index }) this.indicatorsContainer.append(frag) this.indicators = this.indicatorsContainer.children } }) const IOOoptions = { threshold: 0.9, root: this } const allElementsObserver = new IntersectionObserver(entries => { entries.forEach(entry => { if (this.showIndicator) { const activeRank = parseInt(entry.target.dataset.rank) if (entry.isIntersecting) { this.indicators[activeRank].classList.add('active') this.activeSlideNum = activeRank } else this.indicators[activeRank].classList.remove('active') } }) }, IOOoptions) const firstOptionObserver = new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.isIntersecting) { this.navButtonLeft.classList.add('hide') } else { this.navButtonLeft.classList.remove('hide') } }) }, IOOoptions ) const lastOptionObserver = new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.isIntersecting) { this.navButtonRight.classList.add('hide') } else { this.navButtonRight.classList.remove('hide') } }) }, IOOoptions ) const resObs = new ResizeObserver(entries => { entries.forEach(entry => { 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.scrollDistance = contentBoxSize.inlineSize * 0.6 } else { this.scrollDistance = entry.contentRect.width * 0.6 } }) }) resObs.observe(this) this.addEventListener('keydown', this.handleKeyDown) this.navButtonRight.addEventListener('click', this.scrollRight) this.navButtonLeft.addEventListener('click', this.scrollLeft) this.indicatorsContainer.addEventListener('click', this.handleIndicatorClick) } attributeChangedCallback(name, oldValue, newValue) { if (oldValue !== newValue) { if (name === 'indicator') { this.showIndicator = this.hasAttribute('indicator') } if (name === 'autoplay') { if (this.hasAttribute('autoplay')) { this.initialTimeout = setTimeout(() => { this.isAutoPlaying = true this.autoPlay() }, this.autoPlayInterval); } else { this.isAutoPlaying = false clearTimeout(this.autoPlayTimeout) clearTimeout(this.initialTimeout) } } if (name === 'interval') { if (this.hasAttribute('interval') && this.getAttribute('interval').trim() !== '') { this.autoPlayInterval = Math.abs(parseInt(this.getAttribute('interval').trim())) } else { this.autoPlayInterval = 5000 } } } } disconnectedCallback() { this.navButtonRight.removeEventListener('click', this.scrollRight) this.navButtonLeft.removeEventListener('click', this.scrollLeft) this.indicatorsContainer.removeEventListener('click', this.handleIndicatorClick) } }) const smCheckbox = document.createElement('template') smCheckbox.innerHTML = ` ` customElements.define('sm-checkbox', class extends HTMLElement { constructor() { super() this.attachShadow({ mode: 'open' }).append(smCheckbox.content.cloneNode(true)) this.defaultState this.checkbox = this.shadowRoot.querySelector('.checkbox'); this.reset = this.reset.bind(this) this.dispatch = this.dispatch.bind(this) this.handleKeyDown = this.handleKeyDown.bind(this) this.handleClick = this.handleClick.bind(this) } static get observedAttributes() { return ['value', 'disabled', 'checked'] } get disabled() { return this.hasAttribute('disabled') } set disabled(val) { if (val) { this.setAttribute('disabled', '') } else { this.removeAttribute('disabled') } } get checked() { return this.hasAttribute('checked') } set checked(value) { if (value) { this.setAttribute('checked', '') } else { this.removeAttribute('checked') } } set value(val) { this.setAttribute('value', val) } get value() { return this.getAttribute('value') } focusIn() { this.focus() } reset() { this.value = this.defaultState } dispatch() { this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true })) } handleKeyDown(e) { if (e.code === "Space") { e.preventDefault() this.click() } } handleClick(e) { this.toggleAttribute('checked') } connectedCallback() { if (!this.hasAttribute('disabled')) { this.setAttribute('tabindex', '0') } this.setAttribute('role', 'checkbox') this.defaultState = this.hasAttribute('checked') if (!this.hasAttribute('checked')) { this.setAttribute('aria-checked', 'false') } this.addEventListener('keydown', this.handleKeyDown) this.addEventListener('click', this.handleClick) } attributeChangedCallback(name, oldValue, newValue) { if (oldValue !== newValue) { if (name === 'checked') { this.setAttribute('aria-checked', this.hasAttribute('checked')) this.dispatch() } else if (name === 'disabled') { if (this.hasAttribute('disabled')) { this.removeAttribute('tabindex') } else { this.setAttribute('tabindex', '0') } } } } disconnectedCallback() { this.removeEventListener('keydown', this.handleKeyDown) this.removeEventListener('change', this.handleClick) } }) const smCopy = document.createElement('template'); smCopy.innerHTML = `

`; customElements.define('sm-copy', class extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }).append(smCopy.content.cloneNode(true)); this.copyContent = this.shadowRoot.querySelector('.copy-content'); this.copyButton = this.shadowRoot.querySelector('.copy-button'); this.copy = this.copy.bind(this); } static get observedAttributes() { return ['value']; } set value(val) { this.setAttribute('value', val); } get value() { return this.getAttribute('value'); } fireEvent() { this.dispatchEvent( new CustomEvent('copy', { composed: true, bubbles: true, cancelable: true, }) ); } copy() { navigator.clipboard.writeText(this.copyContent.textContent) .then(res => this.fireEvent()) .catch(err => console.error(err)); } connectedCallback() { this.copyButton.addEventListener('click', this.copy); } attributeChangedCallback(name, oldValue, newValue) { if (name === 'value') { this.copyContent.textContent = newValue; } } disconnectedCallback() { this.copyButton.removeEventListener('click', this.copy); } }); const fileInput = document.createElement('template') fileInput.innerHTML = ` ` customElements.define('file-input', class extends HTMLElement { constructor() { super() this.attachShadow({ mode: 'open' }).append(fileInput.content.cloneNode(true)) this.input = this.shadowRoot.querySelector('input') this.fileInput = this.shadowRoot.querySelector('.file-input') this.filesPreviewWraper = this.shadowRoot.querySelector('.files-preview-wrapper') this.reflectedAttributes = ['accept', 'multiple', 'capture'] this.reset = this.reset.bind(this) this.formatBytes = this.formatBytes.bind(this) this.createFilePreview = this.createFilePreview.bind(this) this.handleChange = this.handleChange.bind(this) this.handleKeyDown = this.handleKeyDown.bind(this) } static get observedAttributes() { return ['accept', 'multiple', 'capture'] } get files() { return this.input.files } set accept(val) { this.setAttribute('accept', val) } set multiple(val) { if (val) { this.setAttribute('mutiple', '') } else { this.removeAttribute('mutiple') } } set capture(val) { this.setAttribute('capture', val) } set value(val) { this.input.value = val } get isValid() { return this.input.value !== '' } reset() { this.input.value = '' this.filesPreviewWraper.innerHTML = '' } formatBytes(a, b = 2) { if (0 === a) return "0 Bytes"; const c = 0 > b ? 0 : b, d = Math.floor(Math.log(a) / Math.log(1024)); return parseFloat((a / Math.pow(1024, d)).toFixed(c)) + " " + ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"][d] } createFilePreview(file) { const filePreview = document.createElement('li') const { name, size } = file filePreview.className = 'file-preview' filePreview.innerHTML = `
${name}
${this.formatBytes(size)}
` return filePreview } handleChange(e) { this.filesPreviewWraper.innerHTML = '' const frag = document.createDocumentFragment() Array.from(e.target.files).forEach(file => { frag.append( this.createFilePreview(file) ) }); this.filesPreviewWraper.append(frag) } handleKeyDown(e) { if (e.key === 'Enter' || e.code === 'Space') { e.preventDefault() this.input.click() } } connectedCallback() { this.setAttribute('role', 'button') this.setAttribute('aria-label', 'File upload') this.input.addEventListener('change', this.handleChange) this.fileInput.addEventListener('keydown', this.handleKeyDown) } attributeChangedCallback(name) { if (this.reflectedAttributes.includes(name)) { if (this.hasAttribute(name)) { this.input.setAttribute(name, this.getAttribute(name) ? this.getAttribute(name) : '') } else { this.input.removeAttribute(name) } } } disconnectedCallback() { this.input.removeEventListener('change', this.handleChange) this.fileInput.removeEventListener('keydown', this.handleKeyDown) } }) 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 !== 'SM-TEXTAREA') { if (this.allRequiredValid) { if (this.submitButton && this.submitButton.tagName === 'SM-BUTTON') { 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 hamburgerMenu = document.createElement('template') hamburgerMenu.innerHTML = `
` class HamburgerMenu extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }).append(hamburgerMenu.content.cloneNode(true)) this.resumeScrolling = this.resumeScrolling.bind(this) this.open = this.open.bind(this) this.close = this.close.bind(this) this.sideNav = this.shadowRoot.querySelector('.side-nav') this.backdrop = this.shadowRoot.querySelector('.backdrop') this.isOpen = false this.animeOptions = { duration: 300, easing: 'ease' } } static get observedAttributes() { return ['open']; } resumeScrolling() { const scrollY = document.body.style.top; window.scrollTo(0, parseInt(scrollY || '0') * -1); setTimeout(() => { document.body.style.overflow = 'auto'; document.body.style.top = 'initial' }, 300); } open() { if (this.isOpen) return document.body.style.overflow = 'hidden'; document.body.style.top = `-${window.scrollY}px` this.classList.remove('hide') this.sideNav.classList.add('reveal') this.backdrop.classList.remove('hide') this.backdrop.animate([ { opacity: 0 }, { opacity: 1 }, ], this.animeOptions) .onfinish = () => { this.isOpen = true this.setAttribute('open', '') } } close() { if (!this.isOpen) return this.sideNav.classList.remove('reveal') this.backdrop.animate([ { opacity: 1 }, { opacity: 0 }, ], this.animeOptions) .onfinish = () => { this.backdrop.classList.add('hide') this.classList.add('hide') this.isOpen = false this.removeAttribute('open') } } connectedCallback() { this.backdrop.addEventListener('click', this.close) const resizeObserver = new ResizeObserver(entries => { if (window.innerWidth < 640 && this.isOpen) { this.classList.remove('hide') } else { this.classList.add('hide') } if (window.innerWidth > 640) { this.classList.remove('hide') } }); resizeObserver.observe(this) } disconnectedCallback() { this.backdrop.removeEventListener('click', this.close) } attributeChangedCallback(name, oldVal, newVal) { if (name === 'open') { if (this.hasAttribute('open')) { this.open() } } } } window.customElements.define('hamburger-menu', HamburgerMenu); const smInput = document.createElement('template') smInput.innerHTML = `

`; customElements.define('sm-input', class extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }).append(smInput.content.cloneNode(true)); this.inputParent = this.shadowRoot.querySelector('.input'); this.input = this.shadowRoot.querySelector('input'); this.clearBtn = this.shadowRoot.querySelector('.clear'); this.label = this.shadowRoot.querySelector('.label'); this.feedbackText = this.shadowRoot.querySelector('.feedback-text'); this.outerContainer = this.shadowRoot.querySelector('.outer-container'); this._helperText = ''; this._errorText = ''; this.isRequired = false; this.hideRequired = false; this.validationFunction = undefined; this.reflectedAttributes = ['value', 'required', 'disabled', 'type', 'inputmode', 'readonly', 'min', 'max', 'pattern', 'minlength', 'maxlength', 'step']; this.reset = this.reset.bind(this); this.focusIn = this.focusIn.bind(this); this.focusOut = this.focusOut.bind(this); this.fireEvent = this.fireEvent.bind(this); this.checkInput = this.checkInput.bind(this); this.vibrate = this.vibrate.bind(this); } static get observedAttributes() { return ['value', 'placeholder', 'required', 'disabled', 'type', 'inputmode', 'readonly', 'min', 'max', 'pattern', 'minlength', 'maxlength', 'step', 'helper-text', 'error-text', 'hiderequired']; } get value() { return this.input.value; } set value(val) { this.input.value = val; this.checkInput(); this.fireEvent(); } get placeholder() { return this.getAttribute('placeholder'); } set placeholder(val) { this.setAttribute('placeholder', val); } get type() { return this.getAttribute('type'); } set type(val) { this.setAttribute('type', val); } get validity() { return this.input.validity; } get disabled() { return this.hasAttribute('disabled'); } set disabled(value) { if (value) this.inputParent.classList.add('disabled'); else this.inputParent.classList.remove('disabled'); } get readOnly() { return this.hasAttribute('readonly'); } set readOnly(value) { if (value) { this.setAttribute('readonly', ''); } else { this.removeAttribute('readonly'); } } set customValidation(val) { this.validationFunction = val; } set errorText(val) { this._errorText = val; } set helperText(val) { this._helperText = val; } get isValid() { if (this.input.value !== '') { const _isValid = this.input.checkValidity(); let _customValid = true; if (this.validationFunction) { _customValid = Boolean(this.validationFunction(this.input.value)); } if (_isValid && _customValid) { this.feedbackText.classList.remove('error'); this.feedbackText.classList.add('success'); this.feedbackText.textContent = ''; } else { if (this._errorText) { this.feedbackText.classList.add('error'); this.feedbackText.classList.remove('success'); this.feedbackText.innerHTML = ` ${this._errorText} `; } } return (_isValid && _customValid); } } reset() { this.value = ''; } focusIn() { this.input.focus(); } focusOut() { this.input.blur(); } fireEvent() { let event = new Event('input', { bubbles: true, cancelable: true, composed: true }); this.dispatchEvent(event); } checkInput(e) { if (!this.hasAttribute('readonly')) { if (this.input.value.trim() !== '') { this.clearBtn.classList.remove('hide'); } else { this.clearBtn.classList.add('hide'); if (this.isRequired && !this.hideRequired) { this.feedbackText.textContent = '*required'; } } } if (!this.hasAttribute('placeholder') || this.getAttribute('placeholder').trim() === '') return; if (this.input.value !== '') { if (this.animate) this.inputParent.classList.add('animate-label'); else this.label.classList.add('hide'); } else { if (this.animate) this.inputParent.classList.remove('animate-label'); else this.label.classList.remove('hide'); } } vibrate() { this.outerContainer.animate([ { transform: 'translateX(-1rem)' }, { transform: 'translateX(1rem)' }, { transform: 'translateX(-0.5rem)' }, { transform: 'translateX(0.5rem)' }, { transform: 'translateX(0)' }, ], { duration: 300, easing: 'ease' }); } connectedCallback() { this.animate = this.hasAttribute('animate'); this.setAttribute('role', 'textbox'); this.input.addEventListener('input', this.checkInput); this.clearBtn.addEventListener('click', this.reset); } attributeChangedCallback(name, oldValue, newValue) { if (oldValue !== newValue) { if (this.reflectedAttributes.includes(name)) { if (this.hasAttribute(name)) { this.input.setAttribute(name, this.getAttribute(name) ? this.getAttribute(name) : ''); } else { this.input.removeAttribute(name); } } if (name === 'placeholder') { this.label.textContent = newValue; this.setAttribute('aria-label', newValue); } else if (this.hasAttribute('value')) { this.checkInput(); } else if (name === 'type') { if (this.hasAttribute('type') && this.getAttribute('type') === 'number') { this.input.setAttribute('inputmode', 'numeric'); } } else if (name === 'helper-text') { this._helperText = this.getAttribute('helper-text'); } else if (name === 'error-text') { this._errorText = this.getAttribute('error-text'); } else if (name === 'required') { this.isRequired = this.hasAttribute('required'); if (this.isRequired && !this.hideRequired) { this.feedbackText.textContent = ''; } else { this.feedbackText.textContent = '*required'; } if (this.isRequired) { this.setAttribute('aria-required', 'true'); } else { this.setAttribute('aria-required', 'false'); } } else if (name === 'hiderequired') { this.hideRequired = this.hasAttribute('hiderequired') } else if (name === 'readonly') { if (this.hasAttribute('readonly')) { this.inputParent.classList.add('readonly'); } else { this.inputParent.classList.remove('readonly'); } } else if (name === 'disabled') { if (this.hasAttribute('disabled')) { this.inputParent.classList.add('disabled'); } else { this.inputParent.classList.remove('disabled'); } } } } disconnectedCallback() { this.input.removeEventListener('input', this.checkInput); this.clearBtn.removeEventListener('click', this.reset); } }) const smMenu = document.createElement('template') smMenu.innerHTML = `
`; customElements.define('sm-menu', class extends HTMLElement { constructor() { super() this.attachShadow({ mode: 'open' }).append(smMenu.content.cloneNode(true)) this.isOpen = false; this.availableOptions this.containerDimensions this.animOptions = { duration: 200, easing: 'ease' } this.optionList = this.shadowRoot.querySelector('.options') this.menu = this.shadowRoot.querySelector('.menu') this.icon = this.shadowRoot.querySelector('.icon') this.expand = this.expand.bind(this) this.collapse = this.collapse.bind(this) this.toggle = this.toggle.bind(this) this.handleKeyDown = this.handleKeyDown.bind(this) this.handleClickOutside = this.handleClickOutside.bind(this) } static get observedAttributes() { return ['value'] } get value() { return this.getAttribute('value') } set value(val) { this.setAttribute('value', val) } expand() { if (!this.isOpen) { this.optionList.classList.remove('hide') this.optionList.animate([ { transform: window.innerWidth < 640 ? 'translateY(1.5rem)' : 'translateY(-1rem)', opacity: '0' }, { transform: 'none', opacity: '1' }, ], this.animOptions) .onfinish = () => { this.isOpen = true this.icon.classList.add('focused') } } } collapse() { if (this.isOpen) { this.optionList.animate([ { transform: 'none', opacity: '1' }, { transform: window.innerWidth < 640 ? 'translateY(1.5rem)' : 'translateY(-1rem)', opacity: '0' }, ], this.animOptions) .onfinish = () => { this.isOpen = false this.icon.classList.remove('focused') this.optionList.classList.add('hide') } } } toggle() { if (!this.isOpen) { this.expand() } else { this.collapse() } } handleKeyDown(e) { // If key is pressed on menu button if (e.target === this) { if (e.code === 'ArrowDown') { e.preventDefault() this.availableOptions[0].focus() } else if (e.code === 'Enter' || e.code === 'Space') { e.preventDefault() this.toggle() } } else { // If key is pressed over menu options if (e.code === 'ArrowUp') { e.preventDefault() if (document.activeElement.previousElementSibling) { document.activeElement.previousElementSibling.focus() } else { this.availableOptions[this.availableOptions.length - 1].focus() } } else if (e.code === 'ArrowDown') { e.preventDefault() if (document.activeElement.nextElementSibling) { document.activeElement.nextElementSibling.focus() } else { this.availableOptions[0].focus() } } else if (e.code === 'Enter' || e.code === 'Space') { e.preventDefault() e.target.click() } } } handleClickOutside(e) { if (!this.contains(e.target) && e.button !== 2) { this.collapse() } } connectedCallback() { this.setAttribute('role', 'listbox') this.setAttribute('aria-label', 'dropdown menu') const slot = this.shadowRoot.querySelector('.options slot') slot.addEventListener('slotchange', e => { this.availableOptions = e.target.assignedElements() this.containerDimensions = this.optionList.getBoundingClientRect() }); this.addEventListener('click', this.toggle) this.addEventListener('keydown', this.handleKeyDown) document.addEventListener('mousedown', this.handleClickOutside) } disconnectedCallback() { this.removeEventListener('click', this.toggle) this.removeEventListener('keydown', this.handleKeyDown) document.removeEventListener('mousedown', this.handleClickOutside) } }) // option const menuOption = document.createElement('template') menuOption.innerHTML = `
`; customElements.define('menu-option', class extends HTMLElement { constructor() { super() this.attachShadow({ mode: 'open' }).append(menuOption.content.cloneNode(true)) } connectedCallback() { this.setAttribute('role', 'option') this.setAttribute('tabindex', '0') this.addEventListener('keyup', e => { if (e.code === 'Enter' || e.code === 'Space') { e.preventDefault() this.click() } }) } }) 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) } randString(length) { let result = ''; const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; 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 = '' } = options; const notification = document.createElement('div') notification.id = this.randString(8) notification.classList.add('notification'); let composition = ``; composition += `
${icon}

${message}

`; if (pinned) { notification.classList.add('pinned'); composition += ` `; } notification.innerHTML = composition; return notification; } push(message, options = {}) { const notification = this.createNotification(message, options); this.notificationPanel.append(notification); notification.animate([ { transform: `translateY(1rem)`, opacity: '0' }, { transform: `none`, opacity: '1' }, ], this.animationOptions); return notification.id; } removeNotification(notification) { notification.animate([ { transform: `none`, opacity: '1' }, { transform: `translateY(0.5rem)`, opacity: '0' } ], this.animationOptions).onfinish = () => { notification.remove(); }; } clearAll() { Array.from(this.notificationPanel.children).forEach(child => { this.removeNotification(child); }); } connectedCallback() { 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, }); } }); 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 = 'auto'; 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' } if (popupStack) { 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: 'translateY(-1.5rem) scale(0.9)' }, ], 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 (typeof popupStack !== 'undefined') { popupStack.pop(); if (popupStack.items.length) { this.animateTo(popupStack.items[popupStack.items.length - 1].popup.shadowRoot.querySelector('.popup'), [ { transform: 'translateY(-1.5rem) scale(0.9)' }, { transform: 'none' }, ], animOptions) } else { this.resumeScrolling(); } } else { this.resumeScrolling(); } if (this.forms.length) { this.forms.forEach(form => form.reset()); } this.dispatchEvent( new CustomEvent("popupclosed", { bubbles: true, detail: { popup: this, } }) ); this.isOpen = false; }) } 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.code === '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, 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 smRadio = document.createElement('template') smRadio.innerHTML = `
` window.customElements.define('sm-radio', class extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }).append(smRadio.content.cloneNode(true)) this.radio = this.shadowRoot.querySelector('.radio'); this.reset = this.reset.bind(this) this.dispatchChangeEvent = this.dispatchChangeEvent.bind(this) this.dispatchGroupEvent = this.dispatchGroupEvent.bind(this) this.handleKeyDown = this.handleKeyDown.bind(this) this.handleClick = this.handleClick.bind(this) this.handleRadioGroup = this.handleRadioGroup.bind(this) this.uniqueId this.options } static get observedAttributes() { return ['value', 'disabled', 'checked'] } get disabled() { return this.hasAttribute('disabled') } set disabled(val) { if (val) { this.setAttribute('disabled', '') } else { this.removeAttribute('disabled') } } get checked() { return this.hasAttribute('checked') } set checked(value) { if (value) { this.setAttribute('checked', '') } else { this.removeAttribute('checked') } } set value(val) { this.setAttribute('value', val) } get value() { return this.getAttribute('value') } reset() { this.removeAttribute('checked') } dispatchChangeEvent() { this.dispatchEvent(new CustomEvent('change', this.options)) } dispatchGroupEvent() { if (this.hasAttribute('name') && this.getAttribute('name').trim() !== '') { this.dispatchEvent(new CustomEvent(`changed${this.getAttribute('name')}`, this.options)) } } handleKeyDown(e) { if (e.code === "Space") { e.preventDefault() this.handleClick() } } handleClick() { if (!this.hasAttribute('checked')) { this.setAttribute('checked', '') this.dispatchGroupEvent() } } handleRadioGroup(e) { if (e.detail.uid !== this.uniqueId) { this.reset() } } randString(length) { let result = ''; const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; for (let i = 0; i < length; i++) result += characters.charAt(Math.floor(Math.random() * characters.length)); return result; } connectedCallback() { this.uniqueId = this.randString(8) this.options = { bubbles: true, composed: true, detail: { uid: this.uniqueId, value: this.value, } } if (!this.hasAttribute('disabled')) { this.setAttribute('tabindex', '0') } this.setAttribute('role', 'radio') if (!this.hasAttribute('checked')) { this.setAttribute('aria-checked', 'false') } this.addEventListener('keydown', this.handleKeyDown) this.addEventListener('click', this.handleClick) if (this.hasAttribute('name') && this.getAttribute('name').trim() !== '') { document.addEventListener(`changed${this.getAttribute('name')}`, this.handleRadioGroup) } } attributeChangedCallback(name, oldValue, newValue) { if (oldValue !== newValue) { if (name === 'checked') { this.dispatchChangeEvent() } else if (name === 'disabled') { if (this.hasAttribute('disabled')) { this.removeAttribute('tabindex') } else { this.setAttribute('tabindex', '0') } } } } disconnectedCallback() { this.removeEventListener('keydown', this.handleKeyDown) this.removeEventListener('change', this.handleClick) } }); 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.code === "Space" && !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 smSelect = document.createElement('template') smSelect.innerHTML = `
`; customElements.define('sm-select', class extends HTMLElement { constructor() { super() this.attachShadow({ mode: 'open' }).append(smSelect.content.cloneNode(true)) this.focusIn = this.focusIn.bind(this) this.reset = this.reset.bind(this) this.open = this.open.bind(this) this.collapse = this.collapse.bind(this) this.toggle = this.toggle.bind(this) this.handleOptionsNavigation = this.handleOptionsNavigation.bind(this) this.handleOptionSelection = this.handleOptionSelection.bind(this) this.handleKeydown = this.handleKeydown.bind(this) this.handleClickOutside = this.handleClickOutside.bind(this) this.availableOptions this.previousOption this.isOpen = false; this.label = '' this.slideDown = [{ transform: `translateY(-0.5rem)`, opacity: 0 }, { transform: `translateY(0)`, opacity: 1 } ] this.slideUp = [{ transform: `translateY(0)`, opacity: 1 }, { transform: `translateY(-0.5rem)`, opacity: 0 } ] this.animationOptions = { duration: 300, fill: "forwards", easing: 'ease' } this.optionList = this.shadowRoot.querySelector('.options') this.chevron = this.shadowRoot.querySelector('.toggle') this.selection = this.shadowRoot.querySelector('.selection') this.selectedOptionText = this.shadowRoot.querySelector('.selected-option-text') } static get observedAttributes() { return ['disabled', 'label'] } get value() { return this.getAttribute('value') } set value(val) { this.setAttribute('value', val) } reset(fire = true) { if (this.availableOptions[0] && this.previousOption !== this.availableOptions[0]) { const firstElement = this.availableOptions[0]; if (this.previousOption) { this.previousOption.classList.remove('check-selected') } firstElement.classList.add('check-selected') this.value = firstElement.getAttribute('value') this.selectedOptionText.textContent = `${this.label}${firstElement.textContent}` this.previousOption = firstElement; if (fire) { this.fireEvent() } } } focusIn() { this.selection.focus() } open() { this.optionList.classList.remove('hide') this.optionList.animate(this.slideDown, this.animationOptions) this.chevron.classList.add('rotate') this.isOpen = true } collapse() { this.chevron.classList.remove('rotate') this.optionList.animate(this.slideUp, this.animationOptions) .onfinish = () => { this.optionList.classList.add('hide') this.isOpen = false } } toggle() { if (!this.isOpen && !this.hasAttribute('disabled')) { this.open() } else { this.collapse() } } fireEvent() { this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true, detail: { value: this.value } })) } handleOptionsNavigation(e) { if (e.code === 'ArrowUp') { e.preventDefault() if (document.activeElement.previousElementSibling) { document.activeElement.previousElementSibling.focus() } else { this.availableOptions[this.availableOptions.length - 1].focus() } } else if (e.code === 'ArrowDown') { e.preventDefault() if (document.activeElement.nextElementSibling) { document.activeElement.nextElementSibling.focus() } else { this.availableOptions[0].focus() } } } handleOptionSelection(e) { if (this.previousOption !== document.activeElement) { this.value = document.activeElement.getAttribute('value') this.selectedOptionText.textContent = `${this.label}${document.activeElement.textContent}`; this.fireEvent() if (this.previousOption) { this.previousOption.classList.remove('check-selected') } document.activeElement.classList.add('check-selected') this.previousOption = document.activeElement } } handleClick(e) { if (e.target === this) { this.toggle() } else { this.handleOptionSelection() this.collapse() } } handleKeydown(e) { if (e.target === this) { if (this.isOpen && e.code === 'ArrowDown') { e.preventDefault() this.availableOptions[0].focus() this.handleOptionSelection(e) } else if (e.code === 'Enter' || e.code === 'Space') { e.preventDefault() this.toggle() } } else { this.handleOptionsNavigation(e) this.handleOptionSelection(e) if (e.code === 'Enter' || e.code === 'Space') { e.preventDefault() this.collapse() } } } handleClickOutside(e) { if (this.isOpen && !this.contains(e.target)) { this.collapse() } } connectedCallback() { this.setAttribute('role', 'listbox') if (!this.hasAttribute('disabled')) { this.selection.setAttribute('tabindex', '0') } let slot = this.shadowRoot.querySelector('slot') slot.addEventListener('slotchange', e => { this.availableOptions = slot.assignedElements() this.reset(false) }); this.addEventListener('click', this.handleClick) this.addEventListener('keydown', this.handleKeydown) document.addEventListener('mousedown', this.handleClickOutside) } disconnectedCallback() { this.removeEventListener('click', this.toggle) this.removeEventListener('keydown', this.handleKeydown) document.removeEventListener('mousedown', this.handleClickOutside) } attributeChangedCallback(name) { if (name === "disabled") { if (this.hasAttribute('disabled')) { this.selection.removeAttribute('tabindex') } else { this.selection.setAttribute('tabindex', '0') } } else if (name === 'label') { this.label = this.hasAttribute('label') ? `${this.getAttribute('label')} ` : '' } } }) // option const smOption = document.createElement('template') smOption.innerHTML = `
`; customElements.define('sm-option', class extends HTMLElement { constructor() { super() this.attachShadow({ mode: 'open' }).append(smOption.content.cloneNode(true)) } connectedCallback() { this.setAttribute('role', 'option') this.setAttribute('tabindex', '0') } }) const spinner = document.createElement('template'); spinner.innerHTML = ` `; class SpinnerLoader extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }).append(spinner.content.cloneNode(true)); } } window.customElements.define('sm-spinner', SpinnerLoader); const stripSelect = document.createElement('template'); stripSelect.innerHTML = `
`; customElements.define('strip-select', class extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }).append(stripSelect.content.cloneNode(true)); this.stripSelect = this.shadowRoot.querySelector('.strip-select'); this.slottedOptions = undefined; this._value = undefined; this.scrollDistance = 0; this.scrollLeft = this.scrollLeft.bind(this); this.scrollRight = this.scrollRight.bind(this); this.fireEvent = this.fireEvent.bind(this); } get value() { return this._value; } scrollLeft() { this.stripSelect.scrollBy({ left: -this.scrollDistance, behavior: 'smooth' }); } scrollRight() { this.stripSelect.scrollBy({ left: this.scrollDistance, behavior: 'smooth' }); } fireEvent() { this.dispatchEvent( new CustomEvent("change", { bubbles: true, composed: true, detail: { value: this._value } }) ); } connectedCallback() { this.setAttribute('role', 'listbox'); const slot = this.shadowRoot.querySelector('slot'); const coverLeft = this.shadowRoot.querySelector('.cover--left'); const coverRight = this.shadowRoot.querySelector('.cover--right'); const navButtonLeft = this.shadowRoot.querySelector('.nav-button--left'); const navButtonRight = this.shadowRoot.querySelector('.nav-button--right'); slot.addEventListener('slotchange', e => { const assignedElements = slot.assignedElements(); assignedElements.forEach(elem => { if (elem.hasAttribute('selected')) { elem.setAttribute('active', ''); this._value = elem.value; } }); if (!this.hasAttribute('multiline')) { if (assignedElements.length > 0) { firstOptionObserver.observe(slot.assignedElements()[0]); lastOptionObserver.observe(slot.assignedElements()[slot.assignedElements().length - 1]); } else { navButtonLeft.classList.add('hide'); navButtonRight.classList.add('hide'); coverLeft.classList.add('hide'); coverRight.classList.add('hide'); firstOptionObserver.disconnect(); lastOptionObserver.disconnect(); } } }); const resObs = new ResizeObserver(entries => { entries.forEach(entry => { 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.scrollDistance = contentBoxSize.inlineSize * 0.6; } else { this.scrollDistance = entry.contentRect.width * 0.6; } }); }); resObs.observe(this); this.stripSelect.addEventListener('option-clicked', e => { if (this._value !== e.target.value) { this._value = e.target.value; slot.assignedElements().forEach(elem => elem.removeAttribute('active')); e.target.setAttribute('active', ''); e.target.scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center" }); this.fireEvent(); } }); const firstOptionObserver = new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.isIntersecting) { navButtonLeft.classList.add('hide'); coverLeft.classList.add('hide'); } else { navButtonLeft.classList.remove('hide'); coverLeft.classList.remove('hide'); } }); }, { threshold: 0.9, root: this }); const lastOptionObserver = new IntersectionObserver(entries => { entries.forEach(entry => { if (entry.isIntersecting) { navButtonRight.classList.add('hide'); coverRight.classList.add('hide'); } else { navButtonRight.classList.remove('hide'); coverRight.classList.remove('hide'); } }); }, { threshold: 0.9, root: this }); navButtonLeft.addEventListener('click', this.scrollLeft); navButtonRight.addEventListener('click', this.scrollRight); } disconnectedCallback() { navButtonLeft.removeEventListener('click', this.scrollLeft); navButtonRight.removeEventListener('click', this.scrollRight); } }); //Strip option const stripOption = document.createElement('template'); stripOption.innerHTML = ` `; customElements.define('strip-option', class extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }).append(stripOption.content.cloneNode(true)); this._value = undefined; this.radioButton = this.shadowRoot.querySelector('input'); this.fireEvent = this.fireEvent.bind(this); this.handleKeyDown = this.handleKeyDown.bind(this); } get value() { return this._value; } fireEvent() { this.dispatchEvent( new CustomEvent("option-clicked", { bubbles: true, composed: true, detail: { value: this._value } }) ); } handleKeyDown(e) { if (e.key === 'Enter' || e.key === 'Space') { this.fireEvent(); } } connectedCallback() { this.setAttribute('role', 'option'); this.setAttribute('tabindex', '0'); this._value = this.getAttribute('value'); this.addEventListener('click', this.fireEvent); this.addEventListener('keydown', this.handleKeyDown); } disconnectedCallback() { this.removeEventListener('click', this.fireEvent); this.removeEventListener('keydown', this.handleKeyDown); } }); const smTabHeader = document.createElement('template') smTabHeader.innerHTML = `
`; customElements.define('sm-tab-header', class extends HTMLElement { constructor() { super() this.attachShadow({ mode: 'open' }).append(smTabHeader.content.cloneNode(true)) this.prevTab this.allTabs this.activeTab this.indicator = this.shadowRoot.querySelector('.indicator'); this.tabSlot = this.shadowRoot.querySelector('slot'); this.tabHeader = this.shadowRoot.querySelector('.tab-header'); this.changeTab = this.changeTab.bind(this) this.handleClick = this.handleClick.bind(this) this.handlePanelChange = this.handlePanelChange.bind(this) this.moveIndiactor = this.moveIndiactor.bind(this) } fireEvent(index) { this.dispatchEvent( new CustomEvent(`switchedtab${this.target}`, { bubbles: true, detail: { index: parseInt(index) } }) ) } moveIndiactor(tabDimensions) { this.indicator.setAttribute('style', `width: ${tabDimensions.width}px; transform: translateX(${tabDimensions.left - this.tabHeader.getBoundingClientRect().left + this.tabHeader.scrollLeft}px)`) } changeTab(target) { if (target === this.prevTab || !target.closest('sm-tab')) return if (this.prevTab) this.prevTab.classList.remove('active') target.classList.add('active') this.tabHeader.scrollTo({ behavior: 'smooth', left: target.getBoundingClientRect().left - this.tabHeader.getBoundingClientRect().left + this.tabHeader.scrollLeft }) this.moveIndiactor(target.getBoundingClientRect()) this.prevTab = target; this.activeTab = target; } handleClick(e) { if (e.target.closest('sm-tab')) { this.changeTab(e.target) this.fireEvent(e.target.dataset.index) } } handlePanelChange(e) { this.changeTab(this.allTabs[e.detail.index]) } connectedCallback() { if (!this.hasAttribute('target') || this.getAttribute('target').value === '') return; this.target = this.getAttribute('target') this.tabSlot.addEventListener('slotchange', () => { this.allTabs = this.tabSlot.assignedElements(); this.allTabs.forEach((tab, index) => { tab.dataset.index = index }) }) this.addEventListener('click', this.handleClick) document.addEventListener(`switchedpanel${this.target}`, this.handlePanelChange) let resizeObserver = new ResizeObserver(entries => { entries.forEach((entry) => { if (this.prevTab) { let tabDimensions = this.activeTab.getBoundingClientRect(); this.moveIndiactor(tabDimensions) } }) }) resizeObserver.observe(this) let observer = new IntersectionObserver((entries) => { entries.forEach((entry) => { if (entry.isIntersecting) { this.indicator.style.transition = 'none' if (this.activeTab) { let tabDimensions = this.activeTab.getBoundingClientRect(); this.moveIndiactor(tabDimensions) } else { this.allTabs[0].classList.add('active') let tabDimensions = this.allTabs[0].getBoundingClientRect(); this.moveIndiactor(tabDimensions) this.fireEvent(0) this.prevTab = this.tabSlot.assignedElements()[0]; this.activeTab = this.prevTab; } } }) }, { threshold: 1.0 }) observer.observe(this) } disconnectedCallback() { this.removeEventListener('click', this.handleClick) document.removeEventListener(`switchedpanel${this.target}`, this.handlePanelChange) } }) // tab const smTab = document.createElement('template') smTab.innerHTML = `
`; customElements.define('sm-tab', class extends HTMLElement { constructor() { super() this.shadow = this.attachShadow({ mode: 'open' }).append(smTab.content.cloneNode(true)) } }) // tab-panels const smTabPanels = document.createElement('template') smTabPanels.innerHTML = `
Nothing to see here.
`; customElements.define('sm-tab-panels', class extends HTMLElement { constructor() { super() this.attachShadow({ mode: 'open' }).append(smTabPanels.content.cloneNode(true)) this.isTransitioning = false this.panelContainer = this.shadowRoot.querySelector('.panel-container'); this.handleTabChange = this.handleTabChange.bind(this) } handleTabChange(e) { this.isTransitioning = true this.panelContainer.scrollTo({ left: this.allPanels[e.detail.index].getBoundingClientRect().left - this.panelContainer.getBoundingClientRect().left + this.panelContainer.scrollLeft, behavior: 'smooth' }) setTimeout(() => { this.isTransitioning = false }, 300); } fireEvent(index) { this.dispatchEvent( new CustomEvent(`switchedpanel${this.id}`, { bubbles: true, detail: { index: parseInt(index) } }) ) } connectedCallback() { const slot = this.shadowRoot.querySelector('slot'); slot.addEventListener('slotchange', (e) => { this.allPanels = e.target.assignedElements() this.allPanels.forEach((panel, index) => { panel.dataset.index = index intersectionObserver.observe(panel) }) }) document.addEventListener(`switchedtab${this.id}`, this.handleTabChange) const intersectionObserver = new IntersectionObserver(entries => { entries.forEach(entry => { if (!this.isTransitioning && entry.isIntersecting) { this.fireEvent(entry.target.dataset.index) } }) }, { threshold: 0.6 }) } disconnectedCallback() { intersectionObserver.disconnect() document.removeEventListener(`switchedtab${this.id}`, this.handleTabChange) } }) const tagsInput = document.createElement('template') tagsInput.innerHTML = `

` customElements.define('tags-input', class extends HTMLElement { constructor() { super() this.attachShadow({ mode: 'open' }).append(tagsInput.content.cloneNode(true)) this.input = this.shadowRoot.querySelector('input') this.tagsWrapper = this.shadowRoot.querySelector('.tags-wrapper') this.placeholder = this.shadowRoot.querySelector('.placeholder') this.reflectedAttributes = ['placeholder', 'limit'] this.limit = undefined this.tags = new Set() this.reset = this.reset.bind(this) this.handleInput = this.handleInput.bind(this) this.handleKeydown = this.handleKeydown.bind(this) this.handleClick = this.handleClick.bind(this) this.removeTag = this.removeTag.bind(this) } static get observedAttributes() { return ['placeholder', 'limit'] } get value() { return [...this.tags].join() } get isValid() { return this.tags.size } focusIn() { this.input.focus() } reset() { this.input.value = '' this.tags.clear() while (this.input.previousElementSibling) { this.input.previousElementSibling.remove() } } handleInput(e) { const inputValueLength = e.target.value.trim().length e.target.setAttribute('size', inputValueLength ? inputValueLength : '3') if (inputValueLength) { this.placeholder.classList.add('hide') } else if (!inputValueLength && !this.tags.size) { this.placeholder.classList.remove('hide') } } handleKeydown(e) { if (e.key === ',' || e.key === '/') { e.preventDefault() } if (e.target.value.trim() !== '') { if (e.key === 'Enter' || e.key === ',' || e.key === '/' || e.code === 'Space') { const tagValue = e.target.value.trim() if (this.tags.has(tagValue)) { this.tagsWrapper.querySelector(`[data-value="${tagValue}"]`).animate([ { backgroundColor: 'initial' }, { backgroundColor: 'var(--accent-color)' }, { backgroundColor: 'initial' }, ], { duration: 300, easing: 'ease' }) } else { const tag = document.createElement('span') tag.dataset.value = tagValue tag.className = 'tag' tag.innerHTML = ` ${tagValue} ` this.input.before(tag) this.tags.add(tagValue) } e.target.value = '' e.target.setAttribute('size', '3') if (this.limit && this.limit < this.tags.size + 1) { this.input.readOnly = true return } } } else { if (e.key === 'Backspace' && this.input.previousElementSibling) { this.removeTag(this.input.previousElementSibling) } if (this.limit && this.limit > this.tags.size) { this.input.readOnly = false } } } handleClick(e) { if (e.target.closest('.tag')) { this.removeTag(e.target.closest('.tag')) } else { this.input.focus() } } removeTag(tag) { this.tags.delete(tag.dataset.value) tag.remove() if (!this.tags.size) { this.placeholder.classList.remove('hide') } } connectedCallback() { this.input.addEventListener('input', this.handleInput) this.input.addEventListener('keydown', this.handleKeydown) this.tagsWrapper.addEventListener('click', this.handleClick) } attributeChangedCallback(name, oldValue, newValue) { if (name === 'placeholder') { this.placeholder.textContent = newValue } if (name === 'limit') { this.limit = parseInt(newValue) } } disconnectedCallback() { this.input.removeEventListener('input', this.handleInput) this.input.removeEventListener('keydown', this.handleKeydown) this.tagsWrapper.removeEventListener('click', this.handleClick) } }) const smTextarea = document.createElement('template') smTextarea.innerHTML = ` `; customElements.define('sm-textarea', class extends HTMLElement { constructor() { super() this.attachShadow({ mode: 'open' }).append(smTextarea.content.cloneNode(true)) this.textarea = this.shadowRoot.querySelector('textarea') this.textareaBox = this.shadowRoot.querySelector('.textarea') this.placeholder = this.shadowRoot.querySelector('.placeholder') this.reflectedAttributes = ['disabled', 'required', 'readonly', 'rows', 'minlength', 'maxlength'] this.reset = this.reset.bind(this) this.focusIn = this.focusIn.bind(this) this.fireEvent = this.fireEvent.bind(this) this.checkInput = this.checkInput.bind(this) } static get observedAttributes() { return ['disabled', 'value', 'placeholder', 'required', 'readonly', 'rows', 'minlength', 'maxlength'] } get value() { return this.textarea.value } set value(val) { this.setAttribute('value', val) this.fireEvent() } get disabled() { return this.hasAttribute('disabled') } set disabled(val) { if (val) { this.setAttribute('disabled', '') } else { this.removeAttribute('disabled') } } get isValid() { return this.textarea.checkValidity() } reset() { this.setAttribute('value', '') } focusIn() { this.textarea.focus() } fireEvent() { let event = new Event('input', { bubbles: true, cancelable: true, composed: true }); this.dispatchEvent(event); } checkInput() { if (!this.hasAttribute('placeholder') || this.getAttribute('placeholder') === '') return; if (this.textarea.value !== '') { this.placeholder.classList.add('hide') } else { this.placeholder.classList.remove('hide') } } connectedCallback() { this.textarea.addEventListener('input', e => { this.textareaBox.dataset.value = this.textarea.value this.checkInput() }) } attributeChangedCallback(name, oldValue, newValue) { if (this.reflectedAttributes.includes(name)) { if (this.hasAttribute(name)) { this.textarea.setAttribute(name, this.getAttribute(name) ? this.getAttribute(name) : '') } else { this.textContent.removeAttribute(name) } } else if (name === 'placeholder') { this.placeholder.textContent = this.getAttribute('placeholder') } else if (name === 'value') { this.textarea.value = newValue; this.textareaBox.dataset.value = newValue this.checkInput() } } }) const textField = document.createElement('template') textField.innerHTML = `
` customElements.define('text-field', class extends HTMLElement { constructor() { super() this.attachShadow({ mode: 'open' }).append(textField.content.cloneNode(true)) this.textField = this.shadowRoot.querySelector('.text-field') this.textContainer = this.textField.children[0] this.iconsContainer = this.textField.children[1] this.editButton = this.textField.querySelector('.edit-button') this.isTextEditable = false this.isDisabled = false this.fireEvent = this.fireEvent.bind(this) this.setEditable = this.setEditable.bind(this) this.setNonEditable = this.setNonEditable.bind(this) this.toggleEditable = this.toggleEditable.bind(this) this.revert = this.revert.bind(this) } static get observedAttributes() { return ['disabled', 'value'] } get value() { return this.text } set value(val) { this.setAttribute('value', val) } set disabled(val) { this.isDisabled = val if (this.isDisabled) this.setAttribute('disabled', '') else this.removeAttribute('disabled') } fireEvent(value) { let event = new CustomEvent('change', { bubbles: true, cancelable: true, composed: true, detail: { value } }); this.dispatchEvent(event); } setEditable() { if (this.isTextEditable) return this.textContainer.contentEditable = true this.textContainer.classList.add('editable') this.textContainer.focus() document.execCommand('selectAll', false, null); this.editButton.children[0].animate(this.rotateOut, this.animOptions).onfinish = () => { this.editButton.children[0].classList.add('hide') } setTimeout(() => { this.editButton.children[1].classList.remove('hide') this.editButton.children[1].animate(this.rotateIn, this.animOptions) }, 100); this.isTextEditable = true } setNonEditable() { if (!this.isTextEditable) return this.textContainer.contentEditable = false this.textContainer.classList.remove('editable') const newValue = this.textContainer.textContent.trim() if (this.text !== newValue && newValue !== '') { this.setAttribute('value', this.textContainer.textContent) this.text = this.textContainer.textContent.trim() this.fireEvent(this.text) } else { this.value = this.text } this.editButton.children[1].animate(this.rotateOut, this.animOptions).onfinish = () => { this.editButton.children[1].classList.add('hide') } setTimeout(() => { this.editButton.children[0].classList.remove('hide') this.editButton.children[0].animate(this.rotateIn, this.animOptions) }, 100); this.isTextEditable = false } toggleEditable() { if (this.isTextEditable) this.setNonEditable() else this.setEditable() } revert() { if (this.textContainer.isContentEditable) { this.value = this.text this.setNonEditable() } } connectedCallback() { this.text if (this.hasAttribute('value')) { this.text = this.getAttribute('value') this.textContainer.textContent = this.text } if (this.hasAttribute('disabled')) this.isDisabled = true else this.isDisabled = false this.rotateOut = [ { transform: 'rotate(0)', opacity: 1 }, { transform: 'rotate(90deg)', opacity: 0 }, ] this.rotateIn = [ { transform: 'rotate(-90deg)', opacity: 0 }, { transform: 'rotate(0)', opacity: 1 }, ] this.animOptions = { duration: 300, easing: 'cubic-bezier(0.175, 0.885, 0.32, 1.275)', fill: 'forwards' } if (!this.isDisabled) { this.iconsContainer.classList.remove('hide') this.textContainer.addEventListener('dblclick', this.setEditable) this.editButton.addEventListener('click', this.toggleEditable) } } attributeChangedCallback(name, oldValue, newValue) { if (name === 'disabled') { if (this.hasAttribute('disabled')) { this.textContainer.removeEventListener('dblclick', this.setEditable) this.editButton.removeEventListener('click', this.toggleEditable) this.revert() } else { this.textContainer.addEventListener('dblclick', this.setEditable) this.editButton.addEventListener('click', this.toggleEditable) } } else if (name === 'value') { this.text = newValue this.textContainer.textContent = newValue } } disconnectedCallback() { this.textContainer.removeEventListener('dblclick', this.setEditable) this.editButton.removeEventListener('click', this.toggleEditable) } }) 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.code === 'Space') { 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);