UNPKG

@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
/** * -------------------------------------------------------------------------- * 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 ? '' : ','}&nbsp;</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