@coreui/coreui-pro
Version:
The most popular front-end framework for developing responsive, mobile-first projects on the web rewritten by the CoreUI Team
1,124 lines (907 loc) • 30.9 kB
JavaScript
/**
* --------------------------------------------------------------------------
* CoreUI PRO multi-select.js
* License (https://coreui.io/pro/license/)
* --------------------------------------------------------------------------
*/
import * as Popper from '@popperjs/core'
import BaseComponent from './base-component.js'
import Data from './dom/data.js'
import EventHandler from './dom/event-handler.js'
import SelectorEngine from './dom/selector-engine.js'
import {
defineJQueryPlugin,
getNextActiveElement,
getElement,
isVisible,
isRTL
} from './util/index.js'
/**
* ------------------------------------------------------------------------
* Constants
* ------------------------------------------------------------------------
*/
const NAME = 'multi-select'
const DATA_KEY = 'coreui.multi-select'
const EVENT_KEY = `.${DATA_KEY}`
const DATA_API_KEY = '.data-api'
const ARROW_UP_KEY = 'ArrowUp'
const ARROW_DOWN_KEY = 'ArrowDown'
const BACKSPACE_KEY = 'Backspace'
const DELETE_KEY = 'Delete'
const ENTER_KEY = 'Enter'
const ESCAPE_KEY = 'Escape'
const TAB_KEY = 'Tab'
const RIGHT_MOUSE_BUTTON = 2 // MouseEvent.button value for the secondary button, usually the right button
const SELECTOR_CLEANER = '.form-multi-select-cleaner'
const SELECTOR_OPTGROUP = '.form-multi-select-optgroup'
const SELECTOR_OPTION = '.form-multi-select-option'
const SELECTOR_OPTIONS = '.form-multi-select-options'
const SELECTOR_OPTIONS_EMPTY = '.form-multi-select-options-empty'
const SELECTOR_SEARCH = '.form-multi-select-search'
const SELECTOR_SELECT = '.form-multi-select'
const SELECTOR_SELECTION = '.form-multi-select-selection'
const SELECTOR_VISIBLE_ITEMS = '.form-multi-select-options .form-multi-select-option:not(.disabled):not(:disabled)'
const EVENT_CHANGED = `changed${EVENT_KEY}`
const EVENT_CLICK = `click${EVENT_KEY}`
const EVENT_HIDE = `hide${EVENT_KEY}`
const EVENT_HIDDEN = `hidden${EVENT_KEY}`
const EVENT_KEYDOWN = `keydown${EVENT_KEY}`
const EVENT_KEYUP = `keyup${EVENT_KEY}`
const EVENT_SEARCH = `search${EVENT_KEY}`
const EVENT_SHOW = `show${EVENT_KEY}`
const EVENT_SHOWN = `shown${EVENT_KEY}`
const EVENT_CLICK_DATA_API = `click${EVENT_KEY}${DATA_API_KEY}`
const EVENT_KEYUP_DATA_API = `keyup${EVENT_KEY}${DATA_API_KEY}`
const EVENT_LOAD_DATA_API = `load${EVENT_KEY}${DATA_API_KEY}`
const CLASS_NAME_CLEANER = 'form-multi-select-cleaner'
const CLASS_NAME_DISABLED = 'disabled'
const CLASS_NAME_INPUT_GROUP = 'form-multi-select-input-group'
const CLASS_NAME_LABEL = 'label'
const CLASS_NAME_SELECT = 'form-multi-select'
const CLASS_NAME_SELECT_DROPDOWN = 'form-multi-select-dropdown'
const CLASS_NAME_SELECT_ALL = 'form-multi-select-all'
const CLASS_NAME_OPTGROUP = 'form-multi-select-optgroup'
const CLASS_NAME_OPTGROUP_LABEL = 'form-multi-select-optgroup-label'
const CLASS_NAME_OPTION = 'form-multi-select-option'
const CLASS_NAME_OPTION_WITH_CHECKBOX = 'form-multi-select-option-with-checkbox'
const CLASS_NAME_OPTIONS = 'form-multi-select-options'
const CLASS_NAME_OPTIONS_EMPTY = 'form-multi-select-options-empty'
const CLASS_NAME_SEARCH = 'form-multi-select-search'
const CLASS_NAME_SELECTED = 'form-multi-selected'
const CLASS_NAME_SELECTION = 'form-multi-select-selection'
const CLASS_NAME_SELECTION_TAGS = 'form-multi-select-selection-tags'
const CLASS_NAME_SHOW = 'show'
const CLASS_NAME_TAG = 'form-multi-select-tag'
const CLASS_NAME_TAG_DELETE = 'form-multi-select-tag-delete'
const Default = {
ariaCleanerLabel: 'Clear all selections',
cleaner: true,
container: false,
disabled: false,
invalid: false,
multiple: true,
name: null,
options: false,
optionsMaxHeight: 'auto',
optionsStyle: 'checkbox',
placeholder: 'Select...',
required: false,
search: false,
searchNoResultsLabel: 'No results found',
selectAll: true,
selectAllLabel: 'Select all options',
selectionType: 'tags',
selectionTypeCounterText: 'item(s) selected',
valid: false,
value: null
}
const DefaultType = {
ariaCleanerLabel: 'string',
cleaner: 'boolean',
container: '(string|element|boolean)',
disabled: 'boolean',
invalid: 'boolean',
multiple: 'boolean',
name: '(string|null)',
options: '(boolean|array)',
optionsMaxHeight: '(number|string)',
optionsStyle: 'string',
placeholder: 'string',
required: 'boolean',
search: '(boolean|string)',
searchNoResultsLabel: 'string',
selectAll: 'boolean',
selectAllLabel: 'string',
selectionType: 'string',
selectionTypeCounterText: 'string',
valid: 'boolean',
value: '(string|array|null)'
}
/**
* ------------------------------------------------------------------------
* Class Definition
* ------------------------------------------------------------------------
*/
class MultiSelect extends BaseComponent {
constructor(element, config) {
super(element, config)
this._configureNativeSelect()
this._indicatorElement = null
this._selectAllElement = null
this._selectionElement = null
this._selectionCleanerElement = null
this._searchElement = null
this._togglerElement = null
this._optionsElement = null
this._clone = null
this._menu = null
this._selected = []
this._options = this._getOptions()
this._popper = null
this._search = ''
if (this._config.options.length > 0) {
this._createNativeOptions(this._element, this._config.options)
}
this._createSelect()
this._addEventListeners()
Data.set(this._element, DATA_KEY, this)
}
// Getters
static get Default() {
return Default
}
static get DefaultType() {
return DefaultType
}
static get NAME() {
return NAME
}
// Public
toggle() {
return this._isShown() ? this.hide() : this.show()
}
show() {
if (this._config.disabled || this._isShown()) {
return
}
EventHandler.trigger(this._element, EVENT_SHOW)
this._clone.classList.add(CLASS_NAME_SHOW)
this._clone.setAttribute('aria-expanded', true)
if (this._config.container) {
this._menu.style.minWidth = `${this._clone.offsetWidth}px`
this._menu.classList.add(CLASS_NAME_SHOW)
}
EventHandler.trigger(this._element, EVENT_SHOWN)
this._createPopper()
if (this._config.search) {
SelectorEngine.findOne(SELECTOR_SEARCH, this._clone).focus()
}
}
hide() {
EventHandler.trigger(this._element, EVENT_HIDE)
if (this._popper) {
this._popper.destroy()
}
if (this._config.search) {
this._searchElement.value = ''
}
this._onSearchChange(this._searchElement)
this._clone.classList.remove(CLASS_NAME_SHOW)
this._clone.setAttribute('aria-expanded', 'false')
if (this._config.container) {
this._menu.classList.remove(CLASS_NAME_SHOW)
}
EventHandler.trigger(this._element, EVENT_HIDDEN)
}
dispose() {
if (this._popper) {
this._popper.destroy()
}
super.dispose()
}
search(text) {
this._search = text.length > 0 ? text.toLowerCase() : text
this._filterOptionsList()
EventHandler.trigger(this._element, EVENT_SEARCH)
}
update(config) {
if (config.value) {
this.deselectAll()
}
this._config = { ...this._config, ...this._configAfterMerge(config) }
this._selected = []
this._options = this._getOptions()
this._menu.remove()
this._clone.remove()
this._element.innerHTML = ''
this._createNativeOptions(this._element, this._options)
this._createSelect()
this._addEventListeners()
}
selectAll(options = this._options) {
for (const option of options) {
if (option.disabled) {
continue
}
if (option.label) {
this.selectAll(option.options)
continue
}
this._selectOption(option.value, option.text)
}
}
deselectAll(options = this._options) {
for (const option of options) {
if (option.disabled) {
continue
}
if (option.label) {
this.deselectAll(option.options)
continue
}
this._deselectOption(option.value)
}
}
getValue() {
return this._selected
}
// Private
_addEventListeners() {
EventHandler.on(this._clone, EVENT_CLICK, () => {
if (!this._config.disabled) {
this.show()
}
})
EventHandler.on(this._clone, EVENT_KEYDOWN, event => {
if (event.key === ESCAPE_KEY) {
this.hide()
return
}
if (this._config.search === 'global' && (event.key.length === 1 || event.key === BACKSPACE_KEY || event.key === DELETE_KEY)) {
this._searchElement.focus()
}
})
EventHandler.on(this._menu, EVENT_KEYDOWN, event => {
if (this._config.search === 'global' && (event.key.length === 1 || event.key === BACKSPACE_KEY || event.key === DELETE_KEY)) {
this._searchElement.focus()
}
})
EventHandler.on(this._togglerElement, EVENT_KEYDOWN, event => {
if (!this._isShown() && (event.key === ENTER_KEY || event.key === ARROW_DOWN_KEY)) {
event.preventDefault()
this.show()
return
}
if (this._isShown() && event.key === ARROW_DOWN_KEY) {
event.preventDefault()
this._selectMenuItem(event)
}
})
EventHandler.on(this._indicatorElement, EVENT_CLICK, event => {
event.preventDefault()
event.stopPropagation()
this.toggle()
})
EventHandler.on(this._searchElement, EVENT_KEYUP, () => {
this._onSearchChange(this._searchElement)
})
EventHandler.on(this._searchElement, EVENT_KEYDOWN, event => {
if (!this._isShown()) {
this.show()
}
if (event.key === ARROW_DOWN_KEY && this._searchElement.value.length === this._searchElement.selectionStart) {
this._selectMenuItem(event)
return
}
if ((event.key === BACKSPACE_KEY || event.key === DELETE_KEY) && event.target.value.length === 0) {
this._deselectLastOption()
}
this._searchElement.focus()
})
EventHandler.on(this._selectAllElement, EVENT_CLICK, event => {
event.preventDefault()
event.stopPropagation()
this.selectAll()
})
EventHandler.on(this._optionsElement, EVENT_CLICK, event => {
event.preventDefault()
event.stopPropagation()
this._onOptionsClick(event.target)
})
EventHandler.on(this._selectionCleanerElement, EVENT_CLICK, event => {
if (!this._config.disabled) {
event.preventDefault()
event.stopPropagation()
this.deselectAll()
}
})
EventHandler.on(this._optionsElement, EVENT_KEYDOWN, event => {
if (event.key === ENTER_KEY) {
this._onOptionsClick(event.target)
}
if ([ARROW_UP_KEY, ARROW_DOWN_KEY].includes(event.key)) {
event.preventDefault()
this._selectMenuItem(event)
}
})
}
_getClassNames() {
return this._element.classList.value.split(' ')
}
_getOptions() {
if (this._config.options) {
return this._getOptionsFromConfig()
}
return this._getOptionsFromElement()
}
_getOptionsFromConfig(options = this._config.options) {
const _options = []
for (const option of options) {
if (option.options && Array.isArray(option.options)) {
_options.push({
label: option.label,
options: this._getOptionsFromConfig(option.options)
})
continue
}
const value = String(option.value)
const isSelected = option.selected || (this._config.value && this._config.value.includes(value))
_options.push({
...option,
value,
...isSelected && { selected: true },
...option.disabled && { disabled: true }
})
if (isSelected) {
this._selected.push({
value: String(option.value),
text: option.text
})
}
}
return _options
}
_getOptionsFromElement(node = this._element) {
const nodes = Array.from(node.childNodes).filter(element => element.nodeName === 'OPTION' || element.nodeName === 'OPTGROUP')
const options = []
for (const node of nodes) {
if (node.nodeName === 'OPTION' && node.value) {
const value = String(node.value)
const text = node.innerHTML
const isSelected = node.selected || (this._config.value && this._config.value.includes(node.value))
options.push({
value,
text,
selected: isSelected,
disabled: node.disabled
})
if (node.selected || isSelected) {
this._selected.push({
value,
text: node.innerHTML,
...node.disabled && { disabled: true }
})
}
}
if (node.nodeName === 'OPTGROUP') {
options.push({
label: node.label,
options: this._getOptionsFromElement(node)
})
}
}
return options
}
_configureNativeSelect() {
this._element.classList.add(CLASS_NAME_SELECT)
if (this._config.multiple) {
this._element.setAttribute('multiple', true)
}
if (this._config.required) {
this._element.setAttribute('required', true)
}
}
_createNativeOptions(parentElement, options) {
for (const option of options) {
if ((typeof option.options === 'undefined')) {
const opt = document.createElement('OPTION')
opt.value = option.value
if (option.disabled === true) {
opt.setAttribute('disabled', 'disabled')
}
if (option.selected === true) {
opt.setAttribute('selected', 'selected')
}
opt.innerHTML = option.text
parentElement.append(opt)
} else {
const optgroup = document.createElement('optgroup')
optgroup.label = option.label
this._createNativeOptions(optgroup, option.options)
parentElement.append(optgroup)
}
}
}
_hideNativeSelect() {
this._element.tabIndex = '-1'
this._element.style.display = 'none'
}
_createSelect() {
const multiSelectEl = document.createElement('div')
multiSelectEl.classList.add(CLASS_NAME_SELECT)
multiSelectEl.classList.toggle('is-invalid', this._config.invalid)
multiSelectEl.classList.toggle('is-valid', this._config.valid)
multiSelectEl.setAttribute('aria-expanded', 'false')
if (this._config.disabled) {
this._element.classList.add(CLASS_NAME_DISABLED)
}
for (const className of this._getClassNames()) {
multiSelectEl.classList.add(className)
}
this._clone = multiSelectEl
this._element.parentNode.insertBefore(multiSelectEl, this._element.nextSibling)
this._createSelection()
this._createButtons()
if (this._config.search) {
this._createSearchInput()
this._updateSearch()
}
if (this._config.name || this._element.id || this._element.name) {
this._element.setAttribute('name', (this._config.name || this._element.name || `multi-select-${this._element.id}`))
}
this._createOptionsContainer()
this._hideNativeSelect()
this._updateOptionsList()
}
_createSelection() {
const togglerEl = document.createElement('div')
togglerEl.classList.add(CLASS_NAME_INPUT_GROUP)
this._togglerElement = togglerEl
if (!this._config.search && !this._config.disabled) {
togglerEl.tabIndex = 0
}
const selectionEl = document.createElement('div')
selectionEl.classList.add(CLASS_NAME_SELECTION)
if (this._config.multiple && this._config.selectionType === 'tags') {
selectionEl.classList.add(CLASS_NAME_SELECTION_TAGS)
}
togglerEl.append(selectionEl)
this._clone.append(togglerEl)
this._updateSelection()
this._selectionElement = selectionEl
}
_createButtons() {
const buttons = document.createElement('div')
buttons.classList.add('form-multi-select-buttons')
if (!this._config.disabled && this._config.cleaner && this._config.multiple) {
const cleaner = document.createElement('button')
cleaner.type = 'button'
cleaner.classList.add(CLASS_NAME_CLEANER)
cleaner.style.display = 'none'
cleaner.setAttribute('aria-label', this._config.ariaCleanerLabel)
buttons.append(cleaner)
this._selectionCleanerElement = cleaner
}
const indicator = document.createElement('button')
indicator.type = 'button'
indicator.classList.add('form-multi-select-indicator')
if (this._config.disabled) {
indicator.tabIndex = -1
}
buttons.append(indicator)
this._indicatorElement = indicator
this._togglerElement.append(buttons)
this._updateSelectionCleaner()
}
_createPopper() {
if (typeof Popper === 'undefined') {
throw new TypeError('CoreUI\'s multi select require Popper (https://popper.js.org)')
}
const popperConfig = {
modifiers: [{
name: 'preventOverflow',
options: {
boundary: 'clippingParents'
}
},
{
name: 'offset',
options: {
offset: [0, 2]
}
}],
placement: isRTL() ? 'bottom-end' : 'bottom-start'
}
this._popper = Popper.createPopper(this._togglerElement, this._menu, popperConfig)
}
_createSearchInput() {
const input = document.createElement('input')
input.classList.add(CLASS_NAME_SEARCH)
if (this._config.disabled) {
input.disabled = true
}
this._searchElement = input
this._updateSearchSize()
this._selectionElement.append(input)
}
_createOptionsContainer() {
const dropdownDiv = document.createElement('div')
dropdownDiv.classList.add(CLASS_NAME_SELECT_DROPDOWN)
if (this._config.selectAll && this._config.multiple) {
const selectAllButton = document.createElement('button')
selectAllButton.type = 'button'
selectAllButton.classList.add(CLASS_NAME_SELECT_ALL)
selectAllButton.innerHTML = this._config.selectAllLabel
this._selectAllElement = selectAllButton
dropdownDiv.append(selectAllButton)
}
const optionsDiv = document.createElement('div')
optionsDiv.classList.add(CLASS_NAME_OPTIONS)
if (this._config.optionsMaxHeight !== 'auto') {
optionsDiv.style.maxHeight = `${this._config.optionsMaxHeight}px`
optionsDiv.style.overflow = 'auto'
}
dropdownDiv.append(optionsDiv)
const { container } = this._config
if (container) {
container.append(dropdownDiv)
} else {
this._clone.append(dropdownDiv)
}
this._createOptions(optionsDiv, this._options)
this._optionsElement = optionsDiv
this._menu = dropdownDiv
}
_createOptions(parentElement, options) {
for (const option of options) {
if (typeof option.value !== 'undefined') {
const optionDiv = document.createElement('div')
optionDiv.classList.add(CLASS_NAME_OPTION)
if (option.disabled) {
optionDiv.classList.add(CLASS_NAME_DISABLED)
}
if (this._config.optionsStyle === 'checkbox') {
optionDiv.classList.add(CLASS_NAME_OPTION_WITH_CHECKBOX)
}
optionDiv.dataset.value = String(option.value)
optionDiv.tabIndex = 0
optionDiv.innerHTML = option.text
parentElement.append(optionDiv)
}
if (typeof option.label !== 'undefined') {
const optgroup = document.createElement('div')
optgroup.classList.add(CLASS_NAME_OPTGROUP)
const optgrouplabel = document.createElement('div')
optgrouplabel.innerHTML = option.label
optgrouplabel.classList.add(CLASS_NAME_OPTGROUP_LABEL)
optgroup.append(optgrouplabel)
this._createOptions(optgroup, option.options)
parentElement.append(optgroup)
}
}
}
_createTag(value, text, disabled) {
const tag = document.createElement('div')
tag.classList.add(CLASS_NAME_TAG)
tag.dataset.value = value
tag.innerHTML = text
if (!this._config.disabled && disabled !== true) {
const closeBtn = document.createElement('button')
closeBtn.type = 'button'
closeBtn.classList.add(CLASS_NAME_TAG_DELETE)
closeBtn.setAttribute('aria-label', 'Close')
EventHandler.on(closeBtn, EVENT_CLICK, event => {
event.preventDefault()
event.stopPropagation()
tag.remove()
this._deselectOption(value)
})
tag.append(closeBtn)
}
return tag
}
_onOptionsClick(element) {
if (!element.classList.contains(CLASS_NAME_OPTION) || element.classList.contains(CLASS_NAME_LABEL)) {
return
}
const value = String(element.dataset.value)
const { text } = this._findOptionByValue(value)
if (this._config.multiple && element.classList.contains(CLASS_NAME_SELECTED)) {
this._deselectOption(value)
} else if (this._config.multiple && !element.classList.contains(CLASS_NAME_SELECTED)) {
this._selectOption(value, text)
} else if (!this._config.multiple) {
this._selectOption(value, text)
}
if (!this._config.multiple) {
this.hide()
this.search('')
this._searchElement.value = null
}
}
_findOptionByValue(value, options = this._options) {
for (const option of options) {
if (String(option.value) === value) {
return option
}
if (option.options && Array.isArray(option.options)) {
const found = this._findOptionByValue(value, option.options)
if (found) {
return found
}
}
}
return null
}
_selectOption(value, text) {
if (!this._config.multiple) {
this.deselectAll()
}
if (this._selected.filter(option => option.value === String(value)).length === 0) {
this._selected.push({
value: String(value),
text
})
}
const nativeOption = SelectorEngine.findOne(`option[value="${value}"]`, this._element)
if (nativeOption) {
nativeOption.selected = true
}
const option = SelectorEngine.findOne(`[data-value="${value}"]`, this._optionsElement)
if (option) {
option.classList.add(CLASS_NAME_SELECTED)
}
EventHandler.trigger(this._element, EVENT_CHANGED, {
value: this._selected
})
this._updateSelection()
this._updateSelectionCleaner()
this._updateSearch()
this._updateSearchSize()
}
_deselectOption(value) {
this._selected = this._selected.filter(option => option.value !== String(value))
SelectorEngine.findOne(`option[value="${value}"]`, this._element).selected = false
const option = SelectorEngine.findOne(`[data-value="${value}"]`, this._optionsElement)
if (option) {
option.classList.remove(CLASS_NAME_SELECTED)
}
EventHandler.trigger(this._element, EVENT_CHANGED, {
value: this._selected
})
this._updateSelection()
this._updateSelectionCleaner()
this._updateSearch()
this._updateSearchSize()
}
_deselectLastOption() {
if (this._selected.length > 0) {
const last = this._selected.findLast(option => option.disabled !== true)
if (last) {
this._deselectOption(last.value)
}
}
}
_updateSelection() {
const selection = SelectorEngine.findOne(SELECTOR_SELECTION, this._clone)
const search = SelectorEngine.findOne(SELECTOR_SEARCH, this._clone)
if (this._selected.length === 0 && !this._config.search) {
selection.innerHTML = `<span class="form-multi-select-placeholder">${this._config.placeholder}</span>`
return
}
if (this._config.multiple && this._config.selectionType === 'counter' && !this._config.search) {
selection.innerHTML = `${this._selected.length} ${this._config.selectionTypeCounterText}`
}
if (this._config.multiple && this._config.selectionType === 'tags') {
selection.innerHTML = ''
for (const option of this._selected) {
selection.append(this._createTag(option.value, option.text, option.disabled))
}
}
if (this._config.multiple && this._config.selectionType === 'text') {
selection.innerHTML = this._selected.map((option, index) => `<span>${option.text}${index === this._selected.length - 1 ? '' : ','} </span>`).join('')
}
if (!this._config.multiple && this._selected.length > 0 && !this._config.search) {
selection.innerHTML = this._selected[0].text
}
if (search) {
selection.append(search)
}
if (this._popper) {
this._popper.update()
}
}
_updateSelectionCleaner() {
if (!this._config.cleaner || this._selectionCleanerElement === null) {
return
}
const selectionCleaner = SelectorEngine.findOne(SELECTOR_CLEANER, this._clone)
if (this._selected.length > 0) {
selectionCleaner.style.removeProperty('display')
return
}
selectionCleaner.style.display = 'none'
}
_updateSearch() {
if (!this._config.search) {
return
}
// Select single
if (!this._config.multiple && this._selected.length > 0) {
this._searchElement.placeholder = this._selected[0].text
return
}
if (!this._config.multiple && this._selected.length === 0) {
this._searchElement.placeholder = this._config.placeholder
return
}
// Select multiple
if (this._config.multiple && this._selected.length > 0 && this._config.selectionType !== 'counter') {
this._searchElement.removeAttribute('placeholder')
return
}
if (this._config.multiple && this._selected.length === 0) {
this._searchElement.placeholder = this._config.placeholder
return
}
if (this._config.multiple && this._config.selectionType === 'counter') {
this._searchElement.placeholder = `${this._selected.length} ${this._config.selectionTypeCounterText}`
}
}
_updateSearchSize(size = 2) {
if (!this._searchElement || !this._config.multiple) {
return
}
if (this._selected.length > 0 && (this._config.selectionType === 'tags' || this._config.selectionType === 'text')) {
this._searchElement.size = size
return
}
if (this._selected.length === 0 && (this._config.selectionType === 'tags' || this._config.selectionType === 'text')) {
this._searchElement.removeAttribute('size')
}
}
_onSearchChange(element) {
if (element) {
this.search(element.value)
this._updateSearchSize(element.value.length + 1)
}
}
_updateOptionsList(options = this._options) {
for (const option of options) {
if (option.label) {
this._updateOptionsList(option.options)
continue
}
if (option.selected) {
this._selectOption(option.value, option.text)
}
}
}
_isVisible(element) {
const style = window.getComputedStyle(element)
return (style.display !== 'none')
}
_isShown() {
return this._clone.classList.contains(CLASS_NAME_SHOW)
}
_filterOptionsList() {
const options = SelectorEngine.find(SELECTOR_OPTION, this._menu)
let visibleOptions = 0
for (const option of options) {
// eslint-disable-next-line unicorn/prefer-includes
if (option.textContent.toLowerCase().indexOf(this._search) === -1) {
option.style.display = 'none'
} else {
option.style.removeProperty('display')
visibleOptions++
}
const optgroup = option.closest(SELECTOR_OPTGROUP)
if (optgroup) {
// eslint-disable-next-line unicorn/prefer-array-some
if (SelectorEngine.children(optgroup, SELECTOR_OPTION).filter(element => this._isVisible(element)).length > 0) {
optgroup.style.removeProperty('display')
} else {
optgroup.style.display = 'none'
}
}
}
if (visibleOptions > 0) {
if (SelectorEngine.findOne(SELECTOR_OPTIONS_EMPTY, this._menu)) {
SelectorEngine.findOne(SELECTOR_OPTIONS_EMPTY, this._menu).remove()
}
return
}
if (visibleOptions === 0) {
const placeholder = document.createElement('div')
placeholder.classList.add(CLASS_NAME_OPTIONS_EMPTY)
placeholder.innerHTML = this._config.searchNoResultsLabel
if (!SelectorEngine.findOne(SELECTOR_OPTIONS_EMPTY, this._menu)) {
SelectorEngine.findOne(SELECTOR_OPTIONS, this._menu).append(placeholder)
}
}
}
_selectMenuItem({ key, target }) {
const items = SelectorEngine.find(SELECTOR_VISIBLE_ITEMS, this._menu).filter(element => isVisible(element))
if (!items.length) {
return
}
// if target isn't included in items (e.g. when expanding the dropdown)
// allow cycling to get the last item in case key equals ARROW_UP_KEY
getNextActiveElement(items, target, key === ARROW_DOWN_KEY, !items.includes(target)).focus()
}
_configAfterMerge(config) {
if (config.container === true) {
config.container = document.body
}
if (typeof config.container === 'object' || typeof config.container === 'string') {
config.container = getElement(config.container)
}
if (typeof config.value === 'number') {
config.value = [String(config.value)]
}
if (typeof config.value === 'string') {
config.value = config.value.split(/,\s*/).map(String)
}
return config
}
// Static
static multiSelectInterface(element, config) {
const data = MultiSelect.getOrCreateInstance(element, config)
if (typeof config === 'string') {
if (typeof data[config] === 'undefined') {
throw new TypeError(`No method named "${config}"`)
}
data[config]()
}
}
static jQueryInterface(config) {
return this.each(function () {
MultiSelect.multiSelectInterface(this, config)
})
}
static clearMenus(event) {
if (event && (event.button === RIGHT_MOUSE_BUTTON ||
(event.type === 'keyup' && event.key !== TAB_KEY))) {
return
}
const selects = SelectorEngine.find(SELECTOR_SELECT)
for (let i = 0, len = selects.length; i < len; i++) {
const context = Data.get(selects[i], DATA_KEY)
const relatedTarget = {
relatedTarget: selects[i]
}
if (event && event.type === 'click') {
relatedTarget.clickEvent = event
}
if (!context) {
continue
}
if (!context._clone.classList.contains(CLASS_NAME_SHOW)) {
continue
}
if (context._clone.contains(event.target)) {
continue
}
context.hide()
EventHandler.trigger(context._element, EVENT_HIDDEN)
}
}
}
/**
* Data API implementation
*/
EventHandler.on(window, EVENT_LOAD_DATA_API, () => {
for (const ms of SelectorEngine.find(SELECTOR_SELECT)) {
if (ms.tabIndex !== -1) {
MultiSelect.multiSelectInterface(ms)
}
}
})
EventHandler.on(document, EVENT_CLICK_DATA_API, MultiSelect.clearMenus)
EventHandler.on(document, EVENT_KEYUP_DATA_API, MultiSelect.clearMenus)
/**
* jQuery
*/
defineJQueryPlugin(MultiSelect)
export default MultiSelect