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,033 lines (836 loc) 29.2 kB
/** * -------------------------------------------------------------------------- * CoreUI PRO autocomplete.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 { DefaultAllowlist, sanitizeHtml } from './util/sanitizer.js' import { defineJQueryPlugin, getNextActiveElement, getElement, getUID, isVisible, isRTL } from './util/index.js' /** * ------------------------------------------------------------------------ * Constants * ------------------------------------------------------------------------ */ const NAME = 'autocomplete' const DATA_KEY = 'coreui.autocomplete' 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 EVENT_BLUR = `blur${EVENT_KEY}` 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_INPUT = `input${EVENT_KEY}` const EVENT_KEYDOWN = `keydown${EVENT_KEY}` const EVENT_KEYUP = `keyup${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_AUTOCOMPLETE = 'autocomplete' const CLASS_NAME_BUTTONS = 'autocomplete-buttons' const CLASS_NAME_CLEANER = 'autocomplete-cleaner' const CLASS_NAME_DISABLED = 'disabled' const CLASS_NAME_DROPDOWN = 'autocomplete-dropdown' const CLASS_NAME_INDICATOR = 'autocomplete-indicator' const CLASS_NAME_INPUT = 'autocomplete-input' const CLASS_NAME_INPUT_HINT = 'autocomplete-input-hint' const CLASS_NAME_INPUT_GROUP = 'autocomplete-input-group' const CLASS_NAME_LABEL = 'label' const CLASS_NAME_OPTGROUP = 'autocomplete-optgroup' const CLASS_NAME_OPTGROUP_LABEL = 'autocomplete-optgroup-label' const CLASS_NAME_OPTION = 'autocomplete-option' const CLASS_NAME_OPTIONS = 'autocomplete-options' const CLASS_NAME_OPTIONS_EMPTY = 'autocomplete-options-empty' const CLASS_NAME_SELECTED = 'selected' const CLASS_NAME_SHOW = 'show' const SELECTOR_DATA_TOGGLE = '[data-coreui-toggle="autocomplete"]:not(.disabled)' const SELECTOR_DATA_TOGGLE_SHOWN = `.autocomplete:not(.disabled).${CLASS_NAME_SHOW}` const SELECTOR_INDICATOR = '.autocomplete-indicator' const SELECTOR_OPTGROUP = '.autocomplete-optgroup' const SELECTOR_OPTION = '.autocomplete-option' const SELECTOR_OPTIONS = '.autocomplete-options' const SELECTOR_OPTIONS_EMPTY = '.autocomplete-options-empty' const SELECTOR_VISIBLE_ITEMS = '.autocomplete-options .autocomplete-option:not(.disabled):not(:disabled)' const Default = { allowList: DefaultAllowlist, allowOnlyDefinedOptions: false, ariaCleanerLabel: 'Clear selection', ariaIndicatorLabel: 'Toggle visibility of options menu', cleaner: false, clearSearchOnSelect: true, container: false, disabled: false, highlightOptionsOnSearch: false, id: null, indicator: false, invalid: false, name: null, options: false, optionsGroupsTemplate: null, optionsMaxHeight: 'auto', optionsTemplate: null, placeholder: null, required: false, sanitize: true, sanitizeFn: null, search: null, searchNoResultsLabel: false, showHints: false, valid: false, value: null } const DefaultType = { allowList: 'object', allowOnlyDefinedOptions: 'boolean', ariaCleanerLabel: 'string', ariaIndicatorLabel: 'string', cleaner: 'boolean', clearSearchOnSelect: 'boolean', container: '(string|element|boolean)', disabled: 'boolean', highlightOptionsOnSearch: 'boolean', id: '(string|null)', indicator: 'boolean', invalid: 'boolean', name: '(string|null)', options: '(array|null)', optionsGroupsTemplate: '(function|null)', optionsMaxHeight: '(number|string)', optionsTemplate: '(function|null)', placeholder: '(string|null)', required: 'boolean', sanitize: 'boolean', sanitizeFn: '(null|function)', search: '(array|string|null)', searchNoResultsLabel: ('boolean|string'), showHints: 'boolean', valid: 'boolean', value: '(number|string|null)' } /** * ------------------------------------------------------------------------ * Class Definition * ------------------------------------------------------------------------ */ class Autocomplete extends BaseComponent { constructor(element, config) { super(element, config) this._uniqueId = this._config.id ?? getUID(`${this.constructor.NAME}`) this._indicatorElement = null this._inputElement = null this._inputHintElement = null this._togglerElement = null this._optionsElement = null this._menu = null this._selected = [] this._options = this._getOptionsFromConfig() this._popper = null this._search = '' this._createAutocomplete() 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 } if ( !this._config.searchNoResultsLabel && this._flattenOptions().filter(option => option.label.toLowerCase().includes(this._search.toLowerCase())).length === 0) { return } EventHandler.trigger(this._element, EVENT_SHOW) this._element.classList.add(CLASS_NAME_SHOW) this._inputElement.setAttribute('aria-expanded', 'true') if (this._config.container) { this._menu.style.minWidth = `${this._element.offsetWidth}px` this._menu.classList.add(CLASS_NAME_SHOW) } EventHandler.trigger(this._element, EVENT_SHOWN) this._createPopper() } hide() { EventHandler.trigger(this._element, EVENT_HIDE) if (this._popper) { this._popper.destroy() } this._element.classList.remove(CLASS_NAME_SHOW) this._inputElement.setAttribute('aria-expanded', 'false') if (this._config.container) { this._menu.classList.remove(CLASS_NAME_SHOW) } if (this._inputHintElement) { this._inputHintElement.value = '' } EventHandler.trigger(this._element, EVENT_HIDDEN) } dispose() { if (this._popper) { this._popper.destroy() } super.dispose() } clear() { this.deselectAll() this.search('') this._filterOptionsList() this._inputElement.value = '' EventHandler.trigger(this._element, EVENT_CHANGED, { value: this._selected }) } search(label) { this._search = label.length > 0 ? label.toLowerCase() : '' if (!this._isExternalSearch()) { this._filterOptionsList() } EventHandler.trigger(this._element, EVENT_INPUT, { value: label }) } update(config) { if (config.value) { this.deselectAll() } this._config = { ...this._config, ...this._configAfterMerge(config) } this._options = this._getOptionsFromConfig() this._optionsElement.innerHTML = '' this._createOptions(this._optionsElement, this._options) } deselectAll(options = this._selected) { if (this._selected.length === 0) { return } for (const option of options) { if (option.disabled) { continue } if (Array.isArray(option.options)) { this.deselectAll(option.options) continue } this._deselectOption(option.value) this._updateCleaner() } } // Helpers _flattenOptions(options = this._options, flat = []) { for (const opt of options) { if (opt && Array.isArray(opt.options)) { this._flattenOptions(opt.options, flat) continue } flat.push(opt) } return flat } _getClassNames() { return this._element.classList.value.split(' ') } _highlightOption(label) { const regex = new RegExp(this._search, 'gi') return label.replace(regex, string => `<strong>${string}</strong>`) } _isExternalSearch() { return Array.isArray(this._config.search) && this._config.search.includes('external') } _isGlobalSearch() { return Array.isArray(this._config.search) && this._config.search.includes('global') } _isVisible(element) { const style = window.getComputedStyle(element) return (style.display !== 'none') } _isShown() { return this._element.classList.contains(CLASS_NAME_SHOW) } // Private _addEventListeners() { EventHandler.on(this._element, EVENT_CLICK, event => { if (!this._config.disabled && !event.target.closest(SELECTOR_INDICATOR)) { this.show() } }) EventHandler.on(this._element, EVENT_KEYDOWN, event => { if (event.key === ESCAPE_KEY) { this.hide() if (this._config.allowOnlyDefinedOptions && this._selected.length === 0) { this.search('') this._inputElement.value = '' } return } if (this._isGlobalSearch() && (event.key.length === 1 || event.key === BACKSPACE_KEY || event.key === DELETE_KEY)) { this._inputElement.focus() } }) EventHandler.on(this._menu, EVENT_KEYDOWN, event => { if (this._isGlobalSearch() && (event.key.length === 1 || event.key === BACKSPACE_KEY || event.key === DELETE_KEY)) { this._inputElement.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() this.toggle() }) EventHandler.on(this._inputElement, EVENT_BLUR, () => { const options = this._flattenOptions().filter(option => option.label.toLowerCase().startsWith(this._inputElement.value.toLowerCase())) if (this._config.allowOnlyDefinedOptions && this._selected.length === 0 && options.length === 0) { this.clear() } }) EventHandler.on(this._inputElement, EVENT_KEYDOWN, event => { if (!this._isShown() && event.key !== TAB_KEY) { this.show() } if (event.key === ARROW_DOWN_KEY && this._inputElement.value.length === this._inputElement.selectionStart) { this._selectMenuItem(event) return } if (event.key === TAB_KEY && this._config.showHints && this._inputElement.value.length > 0) { if (this._inputHintElement.value) { event.preventDefault() event.stopPropagation() } const options = this._flattenOptions().filter(option => option.label.toLowerCase().startsWith(this._inputElement.value.toLowerCase())) if (options.length > 0) { this._selectOption(options[0]) } } if (event.key === ENTER_KEY) { event.preventDefault() event.stopPropagation() if (this._inputElement.value.length === 0) { return } const options = this._flattenOptions().filter(option => option.label.toLowerCase() === this._inputElement.value.toLowerCase()) if (options.length > 0) { this._selectOption(options[0]) } if (options.length === 0 && !this._config.allowOnlyDefinedOptions) { EventHandler.trigger(this._element, EVENT_CHANGED, { value: this._inputElement.value }) this.hide() if (this._config.clearSearchOnSelect) { this.search('') } } } }) EventHandler.on(this._inputElement, EVENT_KEYUP, event => { if (event.key.length === 1 || event.key === BACKSPACE_KEY || event.key === DELETE_KEY) { const { value } = event.target this.search(value) if (this._config.showHints) { const options = value ? this._flattenOptions().filter(option => option.label.toLowerCase().startsWith(value.toLowerCase())) : [] this._inputHintElement.value = options.length > 0 ? `${value}${options[0].label.slice(value.length)}` : '' } if (this._selected.length > 0) { this.deselectAll() EventHandler.trigger(this._element, EVENT_CHANGED, { value: this._selected }) } } }) EventHandler.on(this._optionsElement, EVENT_CLICK, event => { event.preventDefault() event.stopPropagation() this._onOptionsClick(event.target) }) EventHandler.on(this._cleanerElement, EVENT_CLICK, event => { if (!this._config.disabled) { event.preventDefault() event.stopPropagation() this.clear() } }) EventHandler.on(this._cleanerElement, EVENT_KEYDOWN, event => { if (!this._config.disabled && event.key === ENTER_KEY) { event.preventDefault() event.stopPropagation() this.clear() } }) 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) } }) } _getOptionsFromConfig(options = this._config.options) { if (!options || !Array.isArray(options)) { return [] } const _options = [] for (const option of options) { if (option.options && Array.isArray(option.options)) { const customGroupProperties = { ...option } delete customGroupProperties.label delete customGroupProperties.options _options.push({ ...customGroupProperties, label: option.label, options: this._getOptionsFromConfig(option.options) }) continue } const label = typeof option === 'string' ? option : option.label const value = option.value ?? (typeof option === 'string' ? option : option.label) const isSelected = option.selected || (this._config.value && this._config.value === value) const customProperties = typeof option === 'object' ? { ...option } : {} delete customProperties.label delete customProperties.value delete customProperties.selected delete customProperties.disabled _options.push({ ...customProperties, label, value: String(value), ...isSelected && { selected: true }, ...option.disabled && { disabled: true } }) if (isSelected) { this._selected.push({ label: option.label, value: String(value) }) } } return _options } _createAutocomplete() { this._element.classList.add(CLASS_NAME_AUTOCOMPLETE) this._element.classList.toggle('is-invalid', this._config.invalid) this._element.classList.toggle('is-valid', this._config.valid) if (this._config.disabled) { this._element.classList.add(CLASS_NAME_DISABLED) } for (const className of this._getClassNames()) { this._element.classList.add(className) } this._createInputGroup() this._createButtons() this._createOptionsContainer() this._updateOptionsList() } _createInputGroup() { const togglerEl = document.createElement('div') togglerEl.classList.add(CLASS_NAME_INPUT_GROUP) this._togglerElement = togglerEl if (!this._config.search && !this._config.disabled) { togglerEl.tabIndex = -1 } if (!this._config.disabled && this._config.showHints) { const inputHintEl = document.createElement('input') inputHintEl.classList.add(CLASS_NAME_INPUT, CLASS_NAME_INPUT_HINT) inputHintEl.setAttribute('name', (this._config.name || `${this._uniqueId}-hint`).toString()) inputHintEl.autocomplete = 'off' inputHintEl.readOnly = true inputHintEl.tabIndex = -1 inputHintEl.setAttribute('aria-hidden', true) togglerEl.append(inputHintEl) this._inputHintElement = inputHintEl } const inputEl = document.createElement('input') inputEl.classList.add(CLASS_NAME_INPUT) inputEl.id = this._uniqueId inputEl.setAttribute('name', (this._config.name || this._uniqueId).toString()) inputEl.autocomplete = 'off' inputEl.placeholder = this._config.placeholder ?? '' inputEl.role = 'combobox' inputEl.setAttribute('aria-autocomplete', 'list') inputEl.setAttribute('aria-expanded', 'false') inputEl.setAttribute('aria-haspopup', 'listbox') if (this._config.disabled) { inputEl.setAttribute('disabled', true) inputEl.tabIndex = -1 } if (this._config.required) { inputEl.setAttribute('required', true) } togglerEl.append(inputEl) this._inputElement = inputEl this._element.append(togglerEl) } _createButtons() { if (!this._config.cleaner && !this._config.indicator) { return } const buttons = document.createElement('div') buttons.classList.add(CLASS_NAME_BUTTONS) if (!this._config.disabled && this._config.cleaner) { 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._cleanerElement = cleaner } if (this._config.indicator) { const indicator = document.createElement('button') indicator.type = 'button' indicator.classList.add(CLASS_NAME_INDICATOR) indicator.setAttribute('aria-label', this._config.ariaIndicatorLabel) if (this._config.disabled) { indicator.tabIndex = -1 } buttons.append(indicator) this._indicatorElement = indicator this._indicatorElement = indicator } this._togglerElement.append(buttons) this._updateCleaner() } _createPopper() { if (typeof Popper === 'undefined') { throw new TypeError('CoreUI\'s Auto Complete component 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) } _createOptionsContainer() { const dropdownDiv = document.createElement('div') dropdownDiv.classList.add(CLASS_NAME_DROPDOWN) dropdownDiv.role = 'listbox' dropdownDiv.setAttribute('aria-labelledby', this._uniqueId) dropdownDiv.setAttribute('id', `${this._uniqueId}-listbox`) 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) { this._inputElement.setAttribute('aria-owns', `${this._uniqueId}-listbox`) dropdownDiv.id = `${this._uniqueId}-listbox` container.append(dropdownDiv) } else { this._element.append(dropdownDiv) } this._createOptions(optionsDiv, this._options) this._optionsElement = optionsDiv this._menu = dropdownDiv } _createOptions(parentElement, options) { for (const option of options) { if (Array.isArray(option.options)) { const optgroup = document.createElement('div') optgroup.classList.add(CLASS_NAME_OPTGROUP) optgroup.setAttribute('role', 'group') const optgrouplabel = document.createElement('div') if (this._config.optionsGroupsTemplate && typeof this._config.optionsGroupsTemplate === 'function') { optgrouplabel.innerHTML = this._config.sanitize ? sanitizeHtml(this._config.optionsGroupsTemplate(option), this._config.allowList, this._config.sanitizeFn) : this._config.optionsGroupsTemplate(option) } else { optgrouplabel.textContent = option.label } optgrouplabel.classList.add(CLASS_NAME_OPTGROUP_LABEL) optgroup.append(optgrouplabel) this._createOptions(optgroup, option.options) parentElement.append(optgroup) continue } const optionDiv = document.createElement('div') optionDiv.classList.add(CLASS_NAME_OPTION) if (option.disabled) { optionDiv.classList.add(CLASS_NAME_DISABLED) optionDiv.setAttribute('aria-disabled', 'true') } optionDiv.dataset.value = option.value optionDiv.tabIndex = 0 if (this._isExternalSearch() && this._config.highlightOptionsOnSearch && this._search) { optionDiv.innerHTML = this._highlightOption(option.label) } else if (this._config.optionsTemplate && typeof this._config.optionsTemplate === 'function') { optionDiv.innerHTML = this._config.sanitize ? sanitizeHtml(this._config.optionsTemplate(option), this._config.allowList, this._config.sanitizeFn) : this._config.optionsTemplate(option) } else { optionDiv.textContent = option.label } parentElement.append(optionDiv) } } _onOptionsClick(element) { if (element.classList.contains(CLASS_NAME_LABEL)) { return } if (!element.classList.contains(CLASS_NAME_OPTION)) { element = element.closest(SELECTOR_OPTION) if (!element) { return } } const { value } = element.dataset const foundOption = this._findOptionByValue(value) if (foundOption) { this._selectOption(foundOption) this._inputElement.focus() } } _findOptionByValue(value, options = this._options) { for (const option of options) { if (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(option) { this.deselectAll() if (this._selected.filter(selectedOption => selectedOption.value === option.value).length === 0) { this._selected.push(option) } const foundOption = SelectorEngine.findOne(`[data-value="${option.value}"]`, this._optionsElement) if (foundOption) { foundOption.classList.add(CLASS_NAME_SELECTED) foundOption.setAttribute('aria-selected', true) } EventHandler.trigger(this._element, EVENT_CHANGED, { value: option }) this._inputElement.value = option.label if (this._config.showHints) { this._inputHintElement.value = '' } this.hide() if (this._config.clearSearchOnSelect) { this.search('') } this._updateCleaner() } _deselectOption(value) { this._selected = this._selected.filter(option => option.value !== value) const option = SelectorEngine.findOne(`[data-value="${value}"]`, this._optionsElement) if (option) { option.classList.remove(CLASS_NAME_SELECTED) option.setAttribute('aria-selected', false) } // EventHandler.trigger(this._element, EVENT_CHANGED, { // value: this._selected // }) } _updateCleaner() { if (!this._config.cleaner || this._cleanerElement === null) { return } if (this._selected.length > 0) { this._cleanerElement.style.removeProperty('display') return } this._cleanerElement.style.display = 'none' } _updateOptionsList(options = this._options) { for (const option of options) { if (Array.isArray(option.options)) { this._updateOptionsList(option.options) continue } if (option.selected) { this._selectOption(option) } } } _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 { if (this._config.highlightOptionsOnSearch && !this._config.optionsTemplate) { option.innerHTML = this._highlightOption(option.textContent) } 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) { if (this._config.searchNoResultsLabel) { 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) } return } this.hide() } } _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.options === 'string') { config.options = config.options.split(/,\s*/).map(String) } if (typeof config.search === 'string') { config.search = config.search.split(/,\s*/).map(String) } return config } // Static static autocompleteInterface(element, config) { const data = Autocomplete.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 () { Autocomplete.autocompleteInterface(this, config) }) } static clearMenus(event) { if (event.button === RIGHT_MOUSE_BUTTON || (event.type === 'keyup' && event.key !== TAB_KEY)) { return } const openToggles = SelectorEngine.find(SELECTOR_DATA_TOGGLE_SHOWN) for (const toggle of openToggles) { const context = Autocomplete.getInstance(toggle) if (!context) { continue } const composedPath = event.composedPath() if ( composedPath.includes(context._element) ) { continue } const relatedTarget = { relatedTarget: context._element } if (event.type === 'click') { relatedTarget.clickEvent = event } context.hide() context.search('') if (context._config.allowOnlyDefinedOptions && context._selected.length === 0) { context._inputElement.value = '' } } } } /** * Data API implementation */ EventHandler.on(window, EVENT_LOAD_DATA_API, () => { for (const autocomplete of SelectorEngine.find(SELECTOR_DATA_TOGGLE)) { Autocomplete.autocompleteInterface(autocomplete) } }) EventHandler.on(document, EVENT_CLICK_DATA_API, Autocomplete.clearMenus) EventHandler.on(document, EVENT_KEYUP_DATA_API, Autocomplete.clearMenus) /** * jQuery */ defineJQueryPlugin(Autocomplete) export default Autocomplete