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.selectOption = this.selectOption.bind(this)
this.debounce = this.debounce.bind(this)
this.availableOptions = []
this.previousOption
this.isOpen = false;
this.label = ''
this.defaultSelected = ''
this.isUnderViewport = false
this.animationOptions = {
duration: 300,
fill: "forwards",
easing: 'ease'
}
this.optionList = this.shadowRoot.querySelector('.options')
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) {
const selectedOption = this.shadowRoot.querySelector('slot').assignedElements().find(option => option.getAttribute('value') === val)
if (selectedOption) {
this.setAttribute('value', val)
this.selectOption(selectedOption)
} else {
console.warn(`There is no option with ${val} as value`)
}
}
debounce(callback, wait) {
let timeoutId = null;
return (...args) => {
window.clearTimeout(timeoutId);
timeoutId = window.setTimeout(() => {
callback.apply(null, args);
}, wait);
};
}
reset(fire = true) {
if (this.availableOptions[0] && this.previousOption !== this.availableOptions[0]) {
const selectedOption = this.availableOptions.find(option => option.hasAttribute('selected')) || this.availableOptions[0];
this.value = selectedOption.getAttribute('value')
if (fire) {
this.fireEvent()
}
}
}
selectOption(selectedOption) {
if (this.previousOption !== selectedOption) {
this.querySelectorAll('[selected]').forEach(option => option.removeAttribute('selected'))
this.selectedOptionText.textContent = `${this.label}${selectedOption.textContent}`;
selectedOption.setAttribute('selected', '')
this.previousOption = selectedOption
}
}
focusIn() {
this.selection.focus()
}
open() {
this.availableOptions.forEach(option => option.setAttribute('tabindex', 0))
this.optionList.classList.remove('hidden')
this.isUnderViewport = this.getBoundingClientRect().bottom + this.optionList.getBoundingClientRect().height > window.innerHeight;
if (this.isUnderViewport) {
this.setAttribute('isUnder', '')
} else {
this.removeAttribute('isUnder')
}
this.optionList.animate([
{
transform: `translateY(${this.isUnderViewport ? '' : '-'}0.5rem)`,
opacity: 0
},
{
transform: `translateY(0)`,
opacity: 1
}
], this.animationOptions)
this.setAttribute('open', '');
this.style.zIndex = 1000;
(this.availableOptions.find(option => option.hasAttribute('selected')) || this.availableOptions[0]).focus()
document.addEventListener('mousedown', this.handleClickOutside)
this.isOpen = true
}
collapse() {
this.removeAttribute('open')
this.optionList.animate([
{
transform: `translateY(0)`,
opacity: 1
},
{
transform: `translateY(${this.isUnderViewport ? '' : '-'}0.5rem)`,
opacity: 0
},
], this.animationOptions)
.onfinish = () => {
this.availableOptions.forEach(option => option.removeAttribute('tabindex'))
document.removeEventListener('mousedown', this.handleClickOutside)
this.optionList.classList.add('hidden')
this.isOpen = false
this.style.zIndex = 'auto';
}
}
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.key === 'ArrowUp') {
e.preventDefault()
if (document.activeElement.previousElementSibling) {
document.activeElement.previousElementSibling.focus()
} else {
this.availableOptions[this.availableOptions.length - 1].focus()
}
} else if (e.key === '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.fireEvent()
}
}
handleClick(e) {
if (e.target === this) {
this.toggle()
}
else {
this.handleOptionSelection()
this.collapse()
}
}
handleKeydown(e) {
if (e.target === this) {
if (this.isOpen && e.key === 'ArrowDown') {
e.preventDefault();
(this.availableOptions.find(option => option.hasAttribute('selected')) || this.availableOptions[0]).focus()
this.handleOptionSelection(e)
} else if (e.key === ' ') {
e.preventDefault()
this.toggle()
}
} else {
this.handleOptionsNavigation(e)
this.handleOptionSelection(e)
if (['Enter', ' ', 'Escape', 'Tab'].includes(e.key)) {
e.preventDefault()
this.collapse()
this.focusIn()
}
}
}
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', this.debounce(e => {
this.availableOptions = slot.assignedElements()
this.reset(false)
this.defaultSelected = this.value
}, 100));
new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const offsetLeft = this.selection.getBoundingClientRect().left
if (offsetLeft < window.innerWidth / 2) {
this.setAttribute('align-select', 'left')
} else {
this.setAttribute('align-select', 'right')
}
}
})
}).observe(this)
this.addEventListener('click', this.handleClick)
this.addEventListener('keydown', this.handleKeydown)
}
disconnectedCallback() {
this.removeEventListener('click', this.handleClick)
this.removeEventListener('click', this.toggle)
this.removeEventListener('keydown', this.handleKeydown)
}
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')
}
})