UNPKG

slim-select

Version:

Slim advanced select dropdown

429 lines (362 loc) 12.3 kB
import { kebabCase } from './helpers' import { DataArray, DataArrayPartial, Optgroup, OptgroupOptional, Option } from './store' export default class Select { public select: HTMLSelectElement // Mutation observer fields public onValueChange?: (value: Option[]) => void public onClassChange?: (classes: string[]) => void public onDisabledChange?: (disabled: boolean) => void public onOptionsChange?: (data: DataArrayPartial) => void // Change observers public listen: boolean = false private observer: MutationObserver | null = null constructor(select: HTMLSelectElement) { this.select = select this.valueChange = this.valueChange.bind(this) // Add change event listener this.select.addEventListener('change', this.valueChange, { // allow bubbling of event passive: true }) // Initiate mutation observer this.observer = new MutationObserver(this.observeCall.bind(this)) // Start listening for changes this.changeListen(true) } public enable(): void { this.select.disabled = false } public disable(): void { this.select.disabled = true } public hideUI(): void { this.select.tabIndex = -1 this.select.style.display = 'none' this.select.setAttribute('aria-hidden', 'true') } public showUI(): void { this.select.removeAttribute('tabindex') this.select.style.display = '' this.select.removeAttribute('aria-hidden') } public changeListen(listen: boolean) { this.listen = listen // Start listening for changes if (listen) { if (this.observer) { this.observer.observe(this.select, { subtree: true, // subtree for optgroups options childList: true, // children changes attributes: true // attributes changes }) } } // Stop listening for changes if (!listen) { if (this.observer) { this.observer.disconnect() } } } // This function get triggers when the select value changes // and will call the onValueChange function if it exists public valueChange(ev: Event): boolean { if (this.listen && this.onValueChange) { this.onValueChange(this.getSelectedOptions()) } // Allow bubbling back to other change event listeners return true } private observeCall(mutations: MutationRecord[]): void { // If we are not listening, do nothing. if (!this.listen) { return } let classChanged = false let disabledChanged = false let optgroupOptionChanged = false // Loop through mutations and check various things for (const m of mutations) { // Check if its the select if (m.target === this.select) { // Check if disabled has changed if (m.attributeName === 'disabled') { disabledChanged = true } // Check if class has changed if (m.attributeName === 'class') { classChanged = true } if (m.type === 'childList') { for (const n of m.addedNodes) { if (n.nodeName === 'OPTION' && (<HTMLOptionElement>n).value === this.select.value) { // we added a new option that's now the select value this.select.dispatchEvent(new Event('change')) break } } // options changed, so we need the optionsChange event to fire optgroupOptionChanged = true } } // Check if its an optgroup or option if (m.target.nodeName === 'OPTGROUP' || m.target.nodeName === 'OPTION') { optgroupOptionChanged = true } } // If class has changed then call the class change function if (classChanged && this.onClassChange) { this.onClassChange(this.select.className.split(' ')) } // If disabled has changed then call the disabled change function if (disabledChanged && this.onDisabledChange) { this.changeListen(false) this.onDisabledChange(this.select.disabled) this.changeListen(true) } // If optgroup or option has changed then call the select change function if (optgroupOptionChanged && this.onOptionsChange) { this.changeListen(false) this.onOptionsChange(this.getData()) this.changeListen(true) } } // From the select element pull optgroup and options into data public getData(): DataArrayPartial { let data = [] as DataArrayPartial // Loop through nodes and get data const nodes = this.select.childNodes as any as HTMLOptGroupElement[] | HTMLOptionElement[] for (const n of nodes) { // Optgroup if (n.nodeName === 'OPTGROUP') { data.push(this.getDataFromOptgroup(n as HTMLOptGroupElement)) } // Option if (n.nodeName === 'OPTION') { data.push(this.getDataFromOption(n as HTMLOptionElement)) } } return data } public getDataFromOptgroup(optgroup: HTMLOptGroupElement): OptgroupOptional { let data = { id: optgroup.id, label: optgroup.label, selectAll: optgroup.dataset ? optgroup.dataset.selectall === 'true' : false, selectAllText: optgroup.dataset ? optgroup.dataset.selectalltext : 'Select all', closable: optgroup.dataset ? optgroup.dataset.closable : 'off', options: [] } as OptgroupOptional const options = optgroup.childNodes as any as HTMLOptionElement[] for (const o of options) { if (o.nodeName === 'OPTION') { data.options!.push(this.getDataFromOption(o as HTMLOptionElement)) } } return data } // From passed in option pull pieces of usable information public getDataFromOption(option: HTMLOptionElement): Option { return { id: option.id, value: option.value, text: option.text, html: option.dataset && option.dataset.html ? option.dataset.html : '', selected: option.selected, display: option.style.display !== 'none', disabled: option.disabled, mandatory: option.dataset ? option.dataset.mandatory === 'true' : false, placeholder: option.dataset.placeholder === 'true', class: option.className, style: option.style.cssText, data: option.dataset } as Option } public getSelectedOptions(): Option[] { let options = [] // Loop through options and set selected const opts = this.select.childNodes as any as (HTMLOptGroupElement | HTMLOptionElement)[] for (const o of opts) { if (o.nodeName === 'OPTGROUP') { const optgroupOptions = o.childNodes as any as HTMLOptionElement[] for (const oo of optgroupOptions) { if (oo.nodeName === 'OPTION') { const option = oo as HTMLOptionElement if (option.selected) { options.push(this.getDataFromOption(option)) } } } } if (o.nodeName === 'OPTION') { const option = o as HTMLOptionElement if (option.selected) { options.push(this.getDataFromOption(option)) } } } return options } public getSelectedValues(): string[] { return this.getSelectedOptions().map((option) => option.value) } public setSelected(ids: string[]): void { // Stop listening to changes this.changeListen(false) // Loop through options and set selected const options = this.select.childNodes as any as (HTMLOptGroupElement | HTMLOptionElement)[] for (const o of options) { if (o.nodeName === 'OPTGROUP') { const optgroup = o as HTMLOptGroupElement const optgroupOptions = optgroup.childNodes as any as HTMLOptionElement[] for (const oo of optgroupOptions) { if (oo.nodeName === 'OPTION') { const option = oo as HTMLOptionElement option.selected = ids.includes(option.id) } } } if (o.nodeName === 'OPTION') { const option = o as HTMLOptionElement option.selected = ids.includes(option.id) } } // Stop listening to changes this.changeListen(true) } // Set selected options by value instead of id // This is useful when the id is not known // and only the value is known // but the value is not unique and can be duplicated public setSelectedByValue(values: string[]): void { // Stop listening to changes this.changeListen(false) // Loop through options and set selected const options = this.select.childNodes as any as (HTMLOptGroupElement | HTMLOptionElement)[] for (const o of options) { if (o.nodeName === 'OPTGROUP') { const optgroup = o as HTMLOptGroupElement const optgroupOptions = optgroup.childNodes as any as HTMLOptionElement[] for (const oo of optgroupOptions) { if (oo.nodeName === 'OPTION') { const option = oo as HTMLOptionElement option.selected = values.includes(option.value) } } } if (o.nodeName === 'OPTION') { const option = o as HTMLOptionElement option.selected = values.includes(option.value) } } // Stop listening to changes this.changeListen(true) } public updateSelect(id?: string, style?: string, classes?: string[]): void { // Stop listening to changes this.changeListen(false) // Update id, only if the id isnt already set if (id) { this.select.dataset.id = id } // Update style if (style) { this.select.style.cssText = style } // Update classes if (classes) { this.select.className = '' classes.forEach((c) => { if (c.trim() !== '') { this.select.classList.add(c.trim()) } }) } // Start listening to changes this.changeListen(true) } public updateOptions(data: DataArray): void { // Stop listening to changes this.changeListen(false) // Clear out select this.select.innerHTML = '' for (const d of data) { if (d instanceof Optgroup) { this.select.appendChild(this.createOptgroup(d)) } if (d instanceof Option) { this.select.appendChild(this.createOption(d)) } } // Trigger change event on original select this.select.dispatchEvent(new Event('change', { bubbles: true })) // Start listening to changes this.changeListen(true) } public createOptgroup(optgroup: Optgroup): HTMLOptGroupElement { const optgroupEl = document.createElement('optgroup') optgroupEl.id = optgroup.id optgroupEl.label = optgroup.label if (optgroup.selectAll) { optgroupEl.dataset.selectAll = 'true' } if (optgroup.closable !== 'off') { optgroupEl.dataset.closable = optgroup.closable } if (optgroup.options) { for (const o of optgroup.options) { optgroupEl.appendChild(this.createOption(o)) } } return optgroupEl } public createOption(info: Option): HTMLOptionElement { const optionEl = document.createElement('option') optionEl.id = info.id optionEl.value = info.value optionEl.textContent = info.text if (info.html !== '') { optionEl.setAttribute('data-html', info.html) } if (info.selected) { optionEl.selected = info.selected } if (info.disabled) { optionEl.disabled = true } if (!info.display) { optionEl.style.display = 'none' } if (info.placeholder) { optionEl.setAttribute('data-placeholder', 'true') } if (info.mandatory) { optionEl.setAttribute('data-mandatory', 'true') } if (info.class) { info.class.split(' ').forEach((optionClass: string) => { optionEl.classList.add(optionClass) }) } if (info.data && typeof info.data === 'object') { Object.keys(info.data).forEach((key) => { optionEl.setAttribute('data-' + kebabCase(key), info.data[key]) }) } return optionEl } public destroy() { this.changeListen(false) // Remove event change listener this.select.removeEventListener('change', this.valueChange) // Disconnect observer and null if (this.observer) { this.observer.disconnect() this.observer = null } // Remove dataset id from original select delete this.select.dataset.id // Show the original select this.showUI() } }