//Button 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)) } get disabled() { return this.isDisabled } set disabled(value) { if (value && !this.isDisabled) { this.isDisabled = true this.setAttribute('disabled', '') this.button.removeAttribute('tabindex') } else if (!value && this.isDisabled) { this.isDisabled = false this.removeAttribute('disabled') } } dispatch() { if (this.isDisabled) { this.dispatchEvent(new CustomEvent('disabled', { bubbles: true, composed: true })) } else { this.dispatchEvent(new CustomEvent('clicked', { bubbles: true, composed: true })) } } connectedCallback() { this.isDisabled = false this.button = this.shadowRoot.querySelector('.button') if (this.hasAttribute('disabled') && !this.isDisabled) this.isDisabled = true this.addEventListener('click', (e) => { this.dispatch() }) } }) //Input const smInput = document.createElement('template') smInput.innerHTML = `
`; customElements.define('sm-input', class extends HTMLElement { static formAssociated = true; constructor() { super() this.attachShadow({ mode: 'open' }).append(smInput.content.cloneNode(true)) } static get observedAttributes() { return ['placeholder'] } get value() { return this.shadowRoot.querySelector('input').value } set value(val) { this.shadowRoot.querySelector('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') } get isValid() { return this.shadowRoot.querySelector('input').checkValidity() } get validity() { return this.shadowRoot.querySelector('input').validity } set disabled(value) { if (value) this.shadowRoot.querySelector('.input').classList.add('disabled') else this.shadowRoot.querySelector('.input').classList.remove('disabled') } set readOnly(value) { if (value) { this.shadowRoot.querySelector('input').setAttribute('readonly', '') this.shadowRoot.querySelector('.input').classList.add('readonly') } else { this.shadowRoot.querySelector('input').removeAttribute('readonly') this.shadowRoot.querySelector('.input').classList.remove('readonly') } } setValidity = (message) => { this.feedbackText.textContent = message } showValidity = () => { this.feedbackText.classList.remove('hide-completely') } hideValidity = () => { this.feedbackText.classList.add('hide-completely') } 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.readonly) { if (this.input.value !== '') { this.clearBtn.classList.remove('hide') } else { this.clearBtn.classList.add('hide') } } if (!this.hasAttribute('placeholder') || this.getAttribute('placeholder') === '') 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') } } connectedCallback() { this.inputParent = this.shadowRoot.querySelector('.input') this.clearBtn = this.shadowRoot.querySelector('.clear') this.label = this.shadowRoot.querySelector('.label') this.feedbackText = this.shadowRoot.querySelector('.feedback-text') this.valueChanged = false; this.readonly = false this.isNumeric = false this.min this.max this.animate = this.hasAttribute('animate') this.input = this.shadowRoot.querySelector('input') this.shadowRoot.querySelector('.label').textContent = this.getAttribute('placeholder') if (this.hasAttribute('value')) { this.input.value = this.getAttribute('value') this.checkInput() } if (this.hasAttribute('required')) { this.input.setAttribute('required', '') } if (this.hasAttribute('min')) { let minValue = this.getAttribute('min') this.input.setAttribute('min', minValue) this.min = parseInt(minValue) } if (this.hasAttribute('max')) { let maxValue = this.getAttribute('max') this.input.setAttribute('max', maxValue) this.max = parseInt(maxValue) } if (this.hasAttribute('minlength')) { const minValue = this.getAttribute('minlength') this.input.setAttribute('minlength', minValue) } if (this.hasAttribute('maxlength')) { const maxValue = this.getAttribute('maxlength') this.input.setAttribute('maxlength', maxValue) } if (this.hasAttribute('step')) { const steps = this.getAttribute('step') this.input.setAttribute('step', steps) } if (this.hasAttribute('pattern')) { this.input.setAttribute('pattern', this.getAttribute('pattern')) } if (this.hasAttribute('readonly')) { this.input.setAttribute('readonly', '') this.readonly = true } if (this.hasAttribute('disabled')) { this.inputParent.classList.add('disabled') } if (this.hasAttribute('error-text')) { this.feedbackText.textContent = this.getAttribute('error-text') } if (this.hasAttribute('type')) { if (this.getAttribute('type') === 'number') { this.input.setAttribute('inputmode', 'numeric') this.input.setAttribute('type', 'number') this.isNumeric = true } else this.input.setAttribute('type', this.getAttribute('type')) } else this.input.setAttribute('type', 'text') this.input.addEventListener('input', e => { this.checkInput(e) }) this.clearBtn.addEventListener('click', e => { this.value = '' }) } attributeChangedCallback(name, oldValue, newValue) { if (oldValue !== newValue) { if (name === 'placeholder') { this.shadowRoot.querySelector('.label').textContent = newValue; this.setAttribute('aria-label', newValue); } } } }) //textarea 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') } get value() { return this.textarea.value } set value(val) { this.textarea.value = val; this.textareaBox.dataset.value = val this.checkInput() this.fireEvent() } 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.textareaBox = this.shadowRoot.querySelector('.textarea') this.placeholder = this.shadowRoot.querySelector('.placeholder') if(this.hasAttribute('placeholder')) this.placeholder.textContent = this.getAttribute('placeholder') if (this.hasAttribute('value')) { this.textarea.value = this.getAttribute('value') this.checkInput() } if (this.hasAttribute('required')) { this.textarea.setAttribute('required', '') } if (this.hasAttribute('readonly')) { this.textarea.setAttribute('readonly', '') } if (this.hasAttribute('rows')) { this.textarea.setAttribute('rows', this.getAttribute('rows')) } this.textarea.addEventListener('input', e => { this.textareaBox.dataset.value = this.textarea.value this.checkInput() }) } }) // 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)) } }) //chcekbox 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.checkbox = this.shadowRoot.querySelector('.checkbox'); this.input = this.shadowRoot.querySelector('input') this.isChecked = false this.isDisabled = false } 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') } } set value(val) { this.val = val this.setAttribute('value', value) } get value() { return getAttribute('value') } dispatch = () => { this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true })) } handleKeyup = e => { if ((e.code === "Enter" || e.code === "Space") && this.isDisabled == false) { if (this.hasAttribute('checked')) { this.input.checked = false this.removeAttribute('checked') } else { this.input.checked = true this.setAttribute('checked', '') } } } handleChange = e => { if (this.input.checked) { this.setAttribute('checked', '') } else { this.removeAttribute('checked') } } connectedCallback() { this.val = '' this.addEventListener('keyup', this.handleKeyup) this.input.addEventListener('change', this.handleChange) } attributeChangedCallback(name, oldValue, newValue) { if (oldValue !== newValue) { if (name === 'disabled') { if (newValue === 'true') { this.isDisabled = true } else { this.isDisabled = false } } else if (name === 'checked') { if (this.hasAttribute('checked')) { this.isChecked = true this.input.checked = true } else { this.input.checked = false this.isChecked = false } this.dispatch() } } } disconnectedCallback() { this.removeEventListener('keyup', this.handleKeyup) this.removeEventListener('change', this.handleChange) } }) //switch 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 } 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') } } dispatch = () => { this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true })) } connectedCallback() { this.addEventListener('keyup', e => { if ((e.code === "Enter" || e.code === "Space") && !this.isDisabled) { 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 } } } } }) // select 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)) } static get observedAttributes() { return ['value'] } get value() { return this.getAttribute('value') } set value(val) { this.setAttribute('value', val) } collapse() { this.optionList.animate(this.slideUp, this.animationOptions) this.optionList.classList.add('hide') this.chevron.classList.remove('rotate') this.open = false } connectedCallback() { this.availableOptions this.optionList = this.shadowRoot.querySelector('.options') this.chevron = this.shadowRoot.querySelector('.toggle') let slot = this.shadowRoot.querySelector('.options slot'), selection = this.shadowRoot.querySelector('.selection'), previousOption this.open = false; this.slideDown = [{ transform: `translateY(-0.5rem)` }, { transform: `translateY(0)` } ], this.slideUp = [{ transform: `translateY(0)` }, { transform: `translateY(-0.5rem)` } ], this.animationOptions = { duration: 300, fill: "forwards", easing: 'ease' } selection.addEventListener('click', e => { if (!this.open) { this.optionList.classList.remove('hide') this.optionList.animate(this.slideDown, this.animationOptions) this.chevron.classList.add('rotate') this.open = true } else { this.collapse() } }) selection.addEventListener('keydown', e => { if (e.code === 'ArrowDown' || e.code === 'ArrowRight') { e.preventDefault() this.availableOptions[0].focus() } if (e.code === 'Enter' || e.code === 'Space') if (!this.open) { this.optionList.classList.remove('hide') this.optionList.animate(this.slideDown, this.animationOptions) this.chevron.classList.add('rotate') this.open = true } else { this.collapse() } }) this.optionList.addEventListener('keydown', e => { if (e.code === 'ArrowUp' || e.code === 'ArrowRight') { e.preventDefault() if (document.activeElement.previousElementSibling) { document.activeElement.previousElementSibling.focus() } } if (e.code === 'ArrowDown' || e.code === 'ArrowLeft') { e.preventDefault() if (document.activeElement.nextElementSibling) document.activeElement.nextElementSibling.focus() } }) this.addEventListener('optionSelected', e => { if (previousOption !== e.target) { this.setAttribute('value', e.detail.value) this.shadowRoot.querySelector('.option-text').textContent = e.detail.text; this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true, detail: { value: e.detail.value } })) if (previousOption) { previousOption.classList.remove('check-selected') } previousOption = e.target; } if (!e.detail.switching) this.collapse() e.target.classList.add('check-selected') }) slot.addEventListener('slotchange', e => { this.availableOptions = slot.assignedElements() if (this.availableOptions[0]) { let firstElement = this.availableOptions[0]; previousOption = firstElement; firstElement.classList.add('check-selected') this.setAttribute('value', firstElement.getAttribute('value')) this.shadowRoot.querySelector('.option-text').textContent = firstElement.textContent this.availableOptions.forEach((element, index) => { element.setAttribute('data-rank', index + 1); element.setAttribute('tabindex', "0"); }) } }); document.addEventListener('mousedown', e => { if (!this.contains(e.target) && this.open) { this.collapse() } }) } }) // 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)) } sendDetails(switching) { let optionSelected = new CustomEvent('optionSelected', { bubbles: true, composed: true, detail: { text: this.textContent, value: this.getAttribute('value'), switching: switching } }) this.dispatchEvent(optionSelected) } connectedCallback() { let validKey = [ 'ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight' ] this.addEventListener('click', e => { this.sendDetails() }) this.addEventListener('keyup', e => { if (e.code === 'Enter' || e.code === 'Space') { e.preventDefault() this.sendDetails(false) } if (validKey.includes(e.code)) { e.preventDefault() this.sendDetails(true) } }) if (this.hasAttribute('default')) { setTimeout(() => { this.sendDetails() }, 0); } } }) // select const smStripSelect = document.createElement('template') smStripSelect.innerHTML = `
Previous
Next
`; customElements.define('sm-strip-select', class extends HTMLElement { constructor() { super() this.attachShadow({ mode: 'open' }).append(smStripSelect.content.cloneNode(true)) } static get observedAttributes() { return ['value'] } get value() { return this.getAttribute('value') } set value(val) { this.setAttribute('value', val) } scrollLeft = () => { this.select.scrollBy({ top: 0, left: -this.scrollDistance, behavior: 'smooth' }) } scrollRight = () => { this.select.scrollBy({ top: 0, left: this.scrollDistance, behavior: 'smooth' }) } connectedCallback() { let previousOption, slot = this.shadowRoot.querySelector('slot'); this.selectContainer = this.shadowRoot.querySelector('.select-container') this.select = this.shadowRoot.querySelector('.select') this.nextArrow = this.shadowRoot.querySelector('.next-item') this.previousArrow = this.shadowRoot.querySelector('.previous-item') this.nextGradient = this.shadowRoot.querySelector('.right') this.previousGradient = this.shadowRoot.querySelector('.left') this.selectOptions this.scrollDistance = this.selectContainer.getBoundingClientRect().width const firstElementObserver = new IntersectionObserver(entries => { if (entries[0].isIntersecting) { this.previousArrow.classList.add('hide') this.previousGradient.classList.add('hide') } else { this.previousArrow.classList.remove('hide') this.previousGradient.classList.remove('hide') } }, { root: this.selectContainer, threshold: 0.95 }) const lastElementObserver = new IntersectionObserver(entries => { if (entries[0].isIntersecting) { this.nextArrow.classList.add('hide') this.nextGradient.classList.add('hide') } else { this.nextArrow.classList.remove('hide') this.nextGradient.classList.remove('hide') } }, { root: this.selectContainer, threshold: 0.95 }) const selectObserver = new IntersectionObserver(entries => { if (entries[0].isIntersecting) { this.scrollDistance = this.selectContainer.getBoundingClientRect().width } }) selectObserver.observe(this.selectContainer) this.addEventListener('optionSelected', e => { if (previousOption === e.target) return; if (previousOption) previousOption.classList.remove('active') e.target.classList.add('active') e.target.scrollIntoView({ behavior: 'smooth', inline: 'center', block: 'nearest' }) this.setAttribute('value', e.detail.value) this.dispatchEvent(new CustomEvent('change', { bubbles: true, composed: true })) previousOption = e.target; }) slot.addEventListener('slotchange', e => { this.selectOptions = slot.assignedElements() firstElementObserver.observe(this.selectOptions[0]) lastElementObserver.observe(this.selectOptions[this.selectOptions.length - 1]) if (this.selectOptions[0]) { let firstElement = this.selectOptions[0]; this.setAttribute('value', firstElement.getAttribute('value')) firstElement.classList.add('active') previousOption = firstElement; } }); this.nextArrow.addEventListener('click', this.scrollRight) this.previousArrow.addEventListener('click', this.scrollLeft) } disconnectedCallback() { this.nextArrow.removeEventListener('click', this.scrollRight) this.previousArrow.removeEventListener('click', this.scrollLeft) } }) // option const smStripOption = document.createElement('template') smStripOption.innerHTML = `
`; customElements.define('sm-strip-option', class extends HTMLElement { constructor() { super() this.attachShadow({ mode: 'open' }).append(smStripOption.content.cloneNode(true)) } sendDetails() { let optionSelected = new CustomEvent('optionSelected', { bubbles: true, composed: true, detail: { text: this.textContent, value: this.getAttribute('value') } }) this.dispatchEvent(optionSelected) } connectedCallback() { this.addEventListener('click', e => { this.sendDetails() }) this.addEventListener('keyup', e => { if (e.code === 'Enter' || e.code === 'Space') { e.preventDefault() this.sendDetails(false) } }) if (this.hasAttribute('default')) { setTimeout(() => { this.sendDetails() }, 0); } } }) //popup 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 } 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); } show = (pinned, popupStack) => { if (popupStack) this.popupStack = popupStack if (this.popupStack && !this.hasAttribute('open')) { this.popupStack.push({ popup: this, permission: pinned }) if (this.popupStack.items.length > 1) { this.popupStack.items[this.popupStack.items.length - 2].popup.classList.add('stacked') } this.dispatchEvent( new CustomEvent("popupopened", { bubbles: true, detail: { popup: this, popupStack: this.popupStack } }) ) this.setAttribute('open', '') this.pinned = pinned } this.popupContainer.classList.remove('hide') this.popup.style.transform = 'none'; document.body.style.overflow = 'hidden'; document.body.style.top= `-${window.scrollY}px` return this.popupStack } hide = () => { if (window.innerWidth < 640) this.popup.style.transform = 'translateY(100%)'; else this.popup.style.transform = 'translateY(3rem)'; this.popupContainer.classList.add('hide') this.removeAttribute('open') if (typeof this.popupStack !== 'undefined') { this.popupStack.pop() if (this.popupStack.items.length) { this.popupStack.items[this.popupStack.items.length - 1].popup.classList.remove('stacked') } else { this.resumeScrolling() } } else { this.resumeScrolling() } if (this.inputFields.length) { setTimeout(() => { this.inputFields.forEach(field => { if (field.type === 'radio' || field.tagName === 'SM-CHECKBOX') field.checked = false if (field.tagName === 'SM-INPUT' || field.tagName === 'TEXTAREA'|| field.tagName === 'SM-TEXTAREA') field.value = '' }) }, 300); } setTimeout(() => { this.dispatchEvent( new CustomEvent("popupclosed", { bubbles: true, detail: { popup: this, popupStack: this.popupStack } }) ) }, 300); } handleTouchStart = (e) => { this.touchStartY = e.changedTouches[0].clientY this.popup.style.transition = 'transform 0.1s' this.touchStartTime = e.timeStamp } handleTouchMove = (e) => { if (this.touchStartY < e.changedTouches[0].clientY) { this.offset = e.changedTouches[0].clientY - this.touchStartY; this.touchEndAnimataion = window.requestAnimationFrame(() => this.movePopup()) } /*else { this.offset = this.touchStartY - e.changedTouches[0].clientY; this.popup.style.transform = `translateY(-${this.offset}px)` }*/ } handleTouchEnd = (e) => { this.touchEndTime = e.timeStamp cancelAnimationFrame(this.touchEndAnimataion) this.touchEndY = e.changedTouches[0].clientY this.popup.style.transition = 'transform 0.3s' 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.show() return } else this.hide() } else { this.show() } } else { if (this.touchEndY > this.touchStartY) if (this.pinned) { this.show() return } else this.hide() } } movePopup = () => { this.popup.style.transform = `translateY(${this.offset}px)` } connectedCallback() { this.pinned = false this.popupStack this.popupContainer = this.shadowRoot.querySelector('.popup-container') this.popup = this.shadowRoot.querySelector('.popup') this.popupBodySlot = this.shadowRoot.querySelector('.popup-body slot') this.offset this.popupHeader = this.shadowRoot.querySelector('.popup-top') this.touchStartY = 0 this.touchEndY = 0 this.touchStartTime = 0 this.touchEndTime = 0 this.touchEndAnimataion; this.threshold = this.popup.getBoundingClientRect().height * 0.3 if (this.hasAttribute('open')) this.show() this.popupContainer.addEventListener('mousedown', e => { if (e.target === this.popupContainer && !this.pinned) { if (this.pinned) { this.show() return } else this.hide() } }) this.popupBodySlot.addEventListener('slotchange', () => { setTimeout(() => { this.threshold = this.popup.getBoundingClientRect().height * 0.3 }, 200); this.inputFields = this.querySelectorAll('sm-input', 'sm-checkbox', 'textarea', 'sm-textarea', 'radio') }) this.popupHeader.addEventListener('touchstart', (e) => { this.handleTouchStart(e) }, {passive: true}) this.popupHeader.addEventListener('touchmove', (e) => {this.handleTouchMove(e)}, {passive: true}) this.popupHeader.addEventListener('touchend', (e) => {this.handleTouchEnd(e)}, {passive: true}) } disconnectedCallback() { this.popupHeader.removeEventListener('touchstart', this.handleTouchStart, {passive: true}) this.popupHeader.removeEventListener('touchmove', this.handleTouchMove, {passive: true}) this.popupHeader.removeEventListener('touchend', this.handleTouchEnd, {passive: true}) } }) //carousel 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)) } static get observedAttributes() { return ['indicator'] } scrollLeft = () => { this.carousel.scrollBy({ top: 0, left: -this.scrollDistance, behavior: 'smooth' }) } scrollRight = () => { this.carousel.scrollBy({ top: 0, left: this.scrollDistance, behavior: 'smooth' }) } connectedCallback() { this.carousel = this.shadowRoot.querySelector('.carousel') this.carouselContainer = this.shadowRoot.querySelector('.carousel-container') this.carouselSlot = this.shadowRoot.querySelector('slot') this.nextArrow = this.shadowRoot.querySelector('.next-item') this.previousArrow = this.shadowRoot.querySelector('.previous-item') this.indicatorsContainer = this.shadowRoot.querySelector('.indicators') this.carouselItems this.indicators this.showIndicator = false this.scrollDistance = this.carouselContainer.getBoundingClientRect().width / 3 let frag = document.createDocumentFragment(); if (this.hasAttribute('indicator')) this.showIndicator = true let firstVisible = false, lastVisible = false const allElementsObserver = new IntersectionObserver(entries => { entries.forEach(entry => { if (this.showIndicator) if (entry.isIntersecting) { this.indicators[parseInt(entry.target.attributes.rank.textContent)].classList.add('active') } else this.indicators[parseInt(entry.target.attributes.rank.textContent)].classList.remove('active') if (!entry.target.previousElementSibling) if (entry.isIntersecting) { this.previousArrow.classList.remove('expand') firstVisible = true } else { this.previousArrow.classList.add('expand') firstVisible = false } if (!entry.target.nextElementSibling) if (entry.isIntersecting) { this.nextArrow.classList.remove('expand') lastVisible = true } else { this.nextArrow.classList.add('expand') lastVisible = false } if (firstVisible && lastVisible) this.indicatorsContainer.classList.add('hide') else this.indicatorsContainer.classList.remove('hide') }) }, { root: this.carouselContainer, threshold: 0.9 }) const carouselObserver = new IntersectionObserver(entries => { if (entries[0].isIntersecting) { this.scrollDistance = this.carouselContainer.getBoundingClientRect().width / 3 } }) carouselObserver.observe(this.carouselContainer) this.carouselSlot.addEventListener('slotchange', e => { this.carouselItems = this.carouselSlot.assignedElements() this.carouselItems.forEach(item => allElementsObserver.observe(item)) if (this.showIndicator) { this.indicatorsContainer.innerHTML = `` this.carouselItems.forEach((item, index) => { let dot = document.createElement('div') dot.classList.add('dot') frag.append(dot) item.setAttribute('rank', index) }) this.indicatorsContainer.append(frag) this.indicators = this.indicatorsContainer.children } }) this.addEventListener('keyup', e => { if (e.code === 'ArrowLeft') this.scrollRight() else this.scrollRight() }) this.nextArrow.addEventListener('click', this.scrollRight) this.previousArrow.addEventListener('click', this.scrollLeft) } attributeChangedCallback(name, oldValue, newValue) { if (name === 'indicator') { if (this.hasAttribute('indicator')) this.showIndicator = true else this.showIndicator = false } } disconnectedCallback() { this.nextArrow.removeEventListener('click', this.scrollRight) this.previousArrow.removeEventListener('click', this.scrollLeft) } }) //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)) } handleTouchStart = (e) => { this.notification = e.target.closest('.notification') this.touchStartX = e.changedTouches[0].clientX this.notification.style.transition = 'initial' this.touchStartTime = e.timeStamp } handleTouchMove = (e) => { e.preventDefault() if (this.touchStartX < e.changedTouches[0].clientX) { this.offset = e.changedTouches[0].clientX - this.touchStartX; this.touchEndAnimataion = requestAnimationFrame(this.movePopup) } else { this.offset = -(this.touchStartX - e.changedTouches[0].clientX); this.touchEndAnimataion = requestAnimationFrame(this.movePopup) } } handleTouchEnd = (e) => { this.notification.style.transition = 'transform 0.3s, opacity 0.3s' this.touchEndTime = e.timeStamp cancelAnimationFrame(this.touchEndAnimataion) this.touchEndX = e.changedTouches[0].clientX if (this.touchEndTime - this.touchStartTime > 200) { if (this.touchEndX - this.touchStartX > this.threshold) { this.removeNotification(this.notification) } else if (this.touchStartX - this.touchEndX > this.threshold) { this.removeNotification(this.notification, true) } else { this.resetPosition() } } else { if (this.touchEndX > this.touchStartX) { this.removeNotification(this.notification) } else { this.removeNotification(this.notification, true) } } } movePopup = () => { this.notification.style.transform = `translateX(${this.offset}px)` } resetPosition = () => { this.notification.style.transform = `translateX(0)` } push = (messageBody, type, pinned) => { let notification = document.createElement('div'), composition = `` notification.classList.add('notification') if (pinned) notification.classList.add('pinned') if (type === 'error') { composition += ` ` } else if (type === 'success') { composition += ` ` } composition += `

${messageBody}

Close ` notification.innerHTML = composition this.notificationPanel.prepend(notification) if (window.innerWidth > 640) { notification.animate([{ transform: `translateX(1rem)`, opacity: '0' }, { transform: 'translateX(0)', opacity: '1' } ], this.animationOptions).onfinish = () => { notification.setAttribute('style', `transform: none;`); } } else { notification.setAttribute('style', `transform: translateY(0); opacity: 1`) } notification.addEventListener('touchstart', this.handleTouchStart) notification.addEventListener('touchmove', this.handleTouchMove) notification.addEventListener('touchend', this.handleTouchEnd) } removeNotification = (notification, toLeft) => { if (!this.offset) this.offset = 0; if (toLeft) notification.animate([{ transform: `translateX(${this.offset}px)`, opacity: '1' }, { transform: `translateX(-100%)`, opacity: '0' } ], this.animationOptions).onfinish = () => { notification.remove() } else { notification.animate([{ transform: `translateX(${this.offset}px)`, opacity: '1' }, { transform: `translateX(100%)`, opacity: '0' } ], this.animationOptions).onfinish = () => { notification.remove() } } } clearAll = () => { Array.from(this.notificationPanel.children).forEach(child => { this.removeNotification(child) }) } connectedCallback() { this.notificationPanel = this.shadowRoot.querySelector('.notification-panel') this.animationOptions = { duration: 300, fill: "forwards", easing: "ease" } this.fontSize = Number(window.getComputedStyle(document.body).getPropertyValue('font-size').match(/\d+/)[0]) this.notification this.offset this.touchStartX = 0 this.touchEndX = 0 this.touchStartTime = 0 this.touchEndTime = 0 this.threshold = this.notificationPanel.getBoundingClientRect().width * 0.3 this.touchEndAnimataion; 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) { if (!mutation.addedNodes[0].classList.contains('pinned')) setTimeout(() => { this.removeNotification(mutation.addedNodes[0]) }, 5000); if (window.innerWidth > 640) this.notificationPanel.style.padding = '1.5rem 0 3rem 1.5rem'; else this.notificationPanel.style.padding = '1rem 1rem 2rem 1rem'; } else if (mutation.removedNodes.length && !this.notificationPanel.children.length) { this.notificationPanel.style.padding = 0; } } }) }) observer.observe(this.notificationPanel, { attributes: true, childList: true, subtree: true }) } }) // sm-menu 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)) } static get observedAttributes() { return ['value'] } get value() { return this.getAttribute('value') } set value(val) { this.setAttribute('value', val) } expand = () => { if (!this.open) { this.optionList.classList.remove('hide') this.optionList.classList.add('no-transformations') this.open = true this.icon.classList.add('focused') this.availableOptions.forEach(option => { option.setAttribute('tabindex', '0') }) } } collapse() { if (this.open) { this.open = false this.icon.classList.remove('focused') this.optionList.classList.add('hide') this.optionList.classList.remove('no-transformations') this.availableOptions.forEach(option => { option.removeAttribute('tabindex') }) } } connectedCallback() { this.availableOptions this.containerDimensions this.optionList = this.shadowRoot.querySelector('.options') let slot = this.shadowRoot.querySelector('.options slot'), menu = this.shadowRoot.querySelector('.menu') this.icon = this.shadowRoot.querySelector('.icon') this.open = false; menu.addEventListener('click', e => { if (!this.open) { this.expand() } else { this.collapse() } }) menu.addEventListener('keydown', e => { if (e.code === 'ArrowDown' || e.code === 'ArrowRight') { e.preventDefault() this.availableOptions[0].focus() } if (e.code === 'Enter' || e.code === 'Space') { e.preventDefault() if (!this.open) { this.expand() } else { this.collapse() } } }) this.optionList.addEventListener('keydown', e => { if (e.code === 'ArrowUp' || e.code === 'ArrowRight') { e.preventDefault() if (document.activeElement.previousElementSibling) { document.activeElement.previousElementSibling.focus() } } if (e.code === 'ArrowDown' || e.code === 'ArrowLeft') { e.preventDefault() if (document.activeElement.nextElementSibling) document.activeElement.nextElementSibling.focus() } }) this.optionList.addEventListener('click', e => { this.collapse() }) slot.addEventListener('slotchange', e => { this.availableOptions = slot.assignedElements() this.containerDimensions = this.optionList.getBoundingClientRect() }); window.addEventListener('mousedown', e => { if (!this.contains(e.target) && e.button !== 2) { this.collapse() } }) } }) // option const smMenuOption = document.createElement('template') smMenuOption.innerHTML = `
`; customElements.define('sm-menu-option', class extends HTMLElement { constructor() { super() this.attachShadow({ mode: 'open' }).append(smMenuOption.content.cloneNode(true)) } connectedCallback() { this.addEventListener('keyup', e => { if (e.code === 'Enter' || e.code === 'Space') { e.preventDefault() this.click() } }) } }) // tab-header 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.indicator = this.shadowRoot.querySelector('.indicator'); this.tabSlot = this.shadowRoot.querySelector('slot'); this.tabHeader = this.shadowRoot.querySelector('.tab-header'); } sendDetails(element) { this.dispatchEvent( new CustomEvent("switchtab", { bubbles: true, detail: { target: this.target, rank: parseInt(element.getAttribute('rank')) } }) ) } moveIndiactor(tabDimensions) { //if(this.isTab) this.indicator.setAttribute('style', `width: ${tabDimensions.width}px; transform: translateX(${tabDimensions.left - this.tabHeader.getBoundingClientRect().left + this.tabHeader.scrollLeft}px)`) //else //this.indicator.setAttribute('style', `width: calc(${tabDimensions.width}px - 1.6rem); transform: translateX(calc(${ tabDimensions.left - this.tabHeader.getBoundingClientRect().left + this.tabHeader.scrollLeft}px + 0.8rem)`) } connectedCallback() { if (!this.hasAttribute('target') || this.getAttribute('target').value === '') return; this.prevTab this.allTabs this.activeTab this.isTab = false this.target = this.getAttribute('target') if (this.hasAttribute('variant') && this.getAttribute('variant') === 'tab') { this.isTab = true } this.tabSlot.addEventListener('slotchange', () => { this.tabSlot.assignedElements().forEach((tab, index) => { tab.setAttribute('rank', index) }) }) this.allTabs = this.tabSlot.assignedElements(); this.tabSlot.addEventListener('click', e => { if (e.target === this.prevTab || !e.target.closest('sm-tab')) return if (this.prevTab) this.prevTab.classList.remove('active') e.target.classList.add('active') e.target.scrollIntoView({ behavior: 'smooth', block: 'nearest', inline: 'center' }) this.moveIndiactor(e.target.getBoundingClientRect()) this.sendDetails(e.target) this.prevTab = e.target; this.activeTab = e.target; }) 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.sendDetails(this.allTabs[0]) this.prevTab = this.tabSlot.assignedElements()[0]; this.activeTab = this.prevTab; } } }) }, { threshold: 1.0 }) observer.observe(this) } }) // 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.panelSlot = this.shadowRoot.querySelector('slot'); } connectedCallback() { //animations let flyInLeft = [{ opacity: 0, transform: 'translateX(-1rem)' }, { opacity: 1, transform: 'none' } ], flyInRight = [{ opacity: 0, transform: 'translateX(1rem)' }, { opacity: 1, transform: 'none' } ], flyOutLeft = [{ opacity: 1, transform: 'none' }, { opacity: 0, transform: 'translateX(-1rem)' } ], flyOutRight = [{ opacity: 1, transform: 'none' }, { opacity: 0, transform: 'translateX(1rem)' } ], animationOptions = { duration: 300, fill: 'forwards', easing: 'ease' } this.prevPanel this.allPanels this.previousRank this.panelSlot.addEventListener('slotchange', () => { this.panelSlot.assignedElements().forEach((panel) => { panel.classList.add('hide-completely') }) }) this.allPanels = this.panelSlot.assignedElements() this._targetBodyFlyRight = (targetBody) => { targetBody.classList.remove('hide-completely') targetBody.animate(flyInRight, animationOptions) } this._targetBodyFlyLeft = (targetBody) => { targetBody.classList.remove('hide-completely') targetBody.animate(flyInLeft, animationOptions) } document.addEventListener('switchtab', e => { if (e.detail.target !== this.id) return if (this.prevPanel) { let targetBody = this.allPanels[e.detail.rank], currentBody = this.prevPanel; if (this.previousRank < e.detail.rank) { if (currentBody && !targetBody) currentBody.animate(flyOutLeft, animationOptions).onfinish = () => { currentBody.classList.add('hide-completely') } else if (targetBody && !currentBody) { this._targetBodyFlyRight(targetBody) } else if (currentBody && targetBody) { currentBody.animate(flyOutLeft, animationOptions).onfinish = () => { currentBody.classList.add('hide-completely') this._targetBodyFlyRight(targetBody) } } } else { if (currentBody && !targetBody) currentBody.animate(flyOutRight, animationOptions).onfinish = () => { currentBody.classList.add('hide-completely') } else if (targetBody && !currentBody) { this._targetBodyFlyLeft(targetBody) } else if (currentBody && targetBody) { currentBody.animate(flyOutRight, animationOptions).onfinish = () => { currentBody.classList.add('hide-completely') this._targetBodyFlyLeft(targetBody) } } } } else { this.allPanels[e.detail.rank].classList.remove('hide-completely') } this.previousRank = e.detail.rank this.prevPanel = this.allPanels[e.detail.rank]; }) } })