UNPKG

slim-select

Version:

Slim advanced select dropdown

420 lines (355 loc) 12.6 kB
import { generateID } from './helpers' export class Option { id: string value: string text: string html: string defaultSelected: boolean selected: boolean display: boolean disabled: boolean placeholder: boolean class: string style: string data: { [key: string]: string } mandatory: boolean constructor(option: Partial<Option>) { this.id = !option.id || option.id === '' ? generateID() : option.id this.value = option.value === undefined ? option.text || '' : option.value || '' this.text = option.text || '' this.html = option.html || '' this.defaultSelected = option.defaultSelected !== undefined ? option.defaultSelected : false this.selected = option.selected !== undefined ? option.selected : false this.display = option.display !== undefined ? option.display : true this.disabled = option.disabled !== undefined ? option.disabled : false this.mandatory = option.mandatory !== undefined ? option.mandatory : false this.placeholder = option.placeholder !== undefined ? option.placeholder : false this.class = option.class || '' this.style = option.style || '' this.data = option.data || {} } } export class Optgroup { public id: string public label: string public selectAll: boolean public selectAllText: string public closable: 'off' | 'open' | 'close' public options: Partial<Option>[] constructor(optgroup: Partial<Optgroup>) { this.id = !optgroup.id || optgroup.id === '' ? generateID() : optgroup.id this.label = optgroup.label || '' this.selectAll = optgroup.selectAll === undefined ? false : optgroup.selectAll this.selectAllText = optgroup.selectAllText || 'Select All' this.closable = optgroup.closable || 'off' // If options exist, loop through options and create new option class // and set the options to the optgroup options field this.options = [] if (optgroup.options) { for (const o of optgroup.options) { this.options.push(new Option(o)) } } } } export default class Store { private selectType: 'single' | 'multiple' = 'single' // Main data set, never null private data: (Option | Optgroup)[] = [] private selectedOrder: string[] = [] constructor(type: 'single' | 'multiple', data: (Partial<Option> | Partial<Optgroup>)[]) { this.selectType = type this.setData(data) } // Validate DataArrayPartial public validateDataArray(data: (Partial<Option> | Partial<Optgroup>)[]): Error | null { if (!Array.isArray(data)) { return new Error('Data must be an array') } // Loop through each data object for (let dataObj of data) { if (!dataObj) continue // Optgroup if (dataObj instanceof Optgroup || 'label' in dataObj) { if (!('label' in dataObj)) { return new Error('Optgroup must have a label') } if ('options' in dataObj && dataObj.options) { for (let option of dataObj.options) { const validationError = this.validateOption(option) if (validationError) { return validationError } } } } else if (dataObj instanceof Option || 'text' in dataObj) { const validationError = this.validateOption(dataObj) if (validationError) { return validationError } } else { return new Error('Data object must be a valid optgroup or option') } } return null } // Validate Option public validateOption(option: Partial<Option>): Error | null { if (!('text' in option)) { return new Error('Option must have a text') } return null } public partialToFullData(data: (Partial<Option> | Partial<Optgroup>)[]): (Option | Optgroup)[] { let dataFinal: (Option | Optgroup)[] = [] data.forEach((dataObj) => { if (!dataObj) return // Optgroup if (dataObj instanceof Optgroup || 'label' in dataObj) { let optOptions: Option[] = [] if ('options' in dataObj && dataObj.options) { dataObj.options.forEach((option: Partial<Option>) => { optOptions.push(new Option(option)) }) } if (optOptions.length > 0) { dataFinal.push(new Optgroup(dataObj as Partial<Optgroup>)) } } // Option if (dataObj instanceof Option || 'text' in dataObj) { dataFinal.push(new Option(dataObj as Partial<Option>)) } }) return dataFinal } public setData(data: (Partial<Option> | Partial<Optgroup>)[], preserveSelected: boolean = false) { // Convert new data to full data array const newData = this.partialToFullData(data) if (preserveSelected) { // Get currently selected options before updating data const selectedOptions = this.getSelectedOptions() // Check which selected options are missing from new data const missingSelected: (Option | Optgroup)[] = [] selectedOptions.forEach((selectedOption) => { let found = false // Check if this selected option exists in new data for (const newItem of newData) { if (newItem instanceof Option && newItem.id === selectedOption.id) { found = true break } if (newItem instanceof Optgroup) { for (const opt of newItem.options) { if (opt.id === selectedOption.id) { found = true break } } } } if (!found) { missingSelected.push(selectedOption) } }) // Add missing selected options to the beginning of the data this.data = [...missingSelected, ...newData] } else { this.data = newData } // Run this.data through setSelected by value // to set the selected property and clean any wrong selected if (this.selectType === 'single') { this.setSelectedBy('id', this.getSelected()) } } // Get data will return all the data public getData(): Option[] | Optgroup[] { return this.filter(null, true) as Option[] | Optgroup[] } // Get data options will return the data as a // flat array of just options public getDataOptions(): Option[] { return this.filter(null, false) as Option[] } public addOption(option: Partial<Option>, addToStart: boolean = false) { if (addToStart) { let data = [new Option(option)] as (Option | Optgroup)[] this.setData(data.concat(this.getData())) } else { this.setData(this.getData().concat(new Option(option))) } } // Pass in an array of id that will loop through // each option and set the selected property to true // but also clean selected by determining selectType public setSelectedBy(selectedType: 'id' | 'value', selectedValues: string[]) { let firstOption: Partial<Option> | null = null let hasSelected = false const selectedObjects: Partial<Option>[] = [] for (let dataObj of this.data) { // Optgroup if (dataObj instanceof Optgroup) { for (let option of dataObj.options as Partial<Option>[]) { if (!firstOption) { firstOption = option } let optionValue = option[selectedType] || '' option.selected = hasSelected ? false : selectedValues.includes(optionValue) // If the option is selected, set hasSelected to true // for single based selects if (option.selected) { selectedObjects.push(option) if (this.selectType === 'single') { hasSelected = true } } } } // Option if (dataObj instanceof Option) { if (!firstOption) { firstOption = dataObj } dataObj.selected = hasSelected ? false : selectedValues.includes(dataObj[selectedType]) // If the option is selected, set hasSelected to true // for single based selects if (dataObj.selected) { selectedObjects.push(dataObj) if (this.selectType === 'single') { hasSelected = true } } } } // If no options are selected, select the first option if (this.selectType === 'single' && firstOption && !hasSelected) { firstOption.selected = true selectedObjects.push(firstOption) } // Put together a list of selected ids in the order of the selected values const selectedIds = selectedValues.map((value) => { return selectedObjects.find((option) => option[selectedType] === value)?.id || '' }) this.selectedOrder = selectedIds } public getSelected(): string[] { return this.getSelectedOptions().map((option) => option.id) } public getSelectedValues(): string[] { return this.getSelectedOptions().map((option) => option.value) } public getSelectedOptions(): Option[] { return this.filter((opt: Option) => { return opt.selected }, false) as Option[] } public getOptgroupByID(id: string): Optgroup | null { // Loop through each data object // and if optgroup is found, return it for (let dataObj of this.data) { if (dataObj instanceof Optgroup && dataObj.id === id) { return dataObj } } return null } public getOptionByID(id: string): Option | null { let options = this.filter((opt: Option) => { return opt.id === id }, false) as Option[] return options.length ? options[0] : null } public getSelectType(): string { return this.selectType } public getFirstOption(): Option | null { let option: Option | null = null for (let dataObj of this.data) { if (dataObj instanceof Optgroup) { option = dataObj.options[0] as Option } else if (dataObj instanceof Option) { option = dataObj } if (option) { break } } return option } // Take in search string and return filtered list of values public search(search: string, searchFilter: (opt: Option, search: string) => boolean): (Option | Optgroup)[] { search = search.trim() // If search is empty, return all data if (search === '') { return this.getData() } // Run filter with search function return this.filter((opt: Option): boolean => { return searchFilter(opt, search) }, true) } // Filter takes in a function that will be used to filter the data // This will also keep optgroups of sub options meet the filter requirements public filter(filter: { (opt: Option): boolean } | null, includeOptgroup: boolean): (Option | Optgroup)[] { const dataSearch: (Option | Optgroup)[] = [] this.data.forEach((dataObj: Option | Optgroup) => { // Optgroup if (dataObj instanceof Optgroup) { let optOptions: Option[] = [] let options = dataObj.options as Option[] options.forEach((option: Option) => { if (!filter || filter(option as Option)) { // If you dont want to include optgroups // just push to the dataSearch array if (!includeOptgroup) { dataSearch.push(new Option(option)) } else { optOptions.push(new Option(option)) } } }) // If we pushed any options to the optOptions array // push the optgroup to the dataSearch array if (optOptions.length > 0) { // Create new optgroup with the new options let optgroup = new Optgroup(dataObj) optgroup.options = optOptions // Push optgroup to dataSearch dataSearch.push(optgroup) } } // Option if (dataObj instanceof Option) { if (!filter || filter(dataObj)) { dataSearch.push(new Option(dataObj)) } } }) return dataSearch } // Take in an array of options and reoder them based upon the selected order public selectedOrderOptions(options: Option[]): Option[] { const newOrder: Option[] = [] this.selectedOrder.forEach((id) => { const option = options.find((opt) => opt.id === id) if (option) { newOrder.push(option) } }) // add any remaining options that were not in the selected order options.forEach((option) => { let isIn = false newOrder.forEach((selectedOption) => { if (option.id === selectedOption.id) { isIn = true return } }) if (!isIn) { newOrder.push(option) } }) return newOrder } }