{"version":3,"file":"choices.js","sources":["webpack:///webpack/universalModuleDefinition","webpack:///webpack/bootstrap b28c19019179aae11f77","webpack:///assets/scripts/src/choices.js","webpack:///./~/fuse.js/src/fuse.js","webpack:///./~/classnames/index.js","webpack:///assets/scripts/src/store/index.js","webpack:///./~/redux/lib/index.js","webpack:///./~/redux/lib/createStore.js","webpack:///./~/lodash/isPlainObject.js","webpack:///./~/lodash/_baseGetTag.js","webpack:///./~/lodash/_Symbol.js","webpack:///./~/lodash/_root.js","webpack:///./~/lodash/_freeGlobal.js","webpack:///./~/lodash/_getRawTag.js","webpack:///./~/lodash/_objectToString.js","webpack:///./~/lodash/_getPrototype.js","webpack:///./~/lodash/_overArg.js","webpack:///./~/lodash/isObjectLike.js","webpack:///./~/symbol-observable/index.js","webpack:///./~/symbol-observable/lib/index.js","webpack:///(webpack)/buildin/module.js","webpack:///./~/symbol-observable/lib/ponyfill.js","webpack:///./~/redux/lib/combineReducers.js","webpack:///./~/redux/lib/utils/warning.js","webpack:///./~/redux/lib/bindActionCreators.js","webpack:///./~/redux/lib/applyMiddleware.js","webpack:///./~/redux/lib/compose.js","webpack:///assets/scripts/src/reducers/index.js","webpack:///assets/scripts/src/reducers/items.js","webpack:///assets/scripts/src/reducers/groups.js","webpack:///assets/scripts/src/reducers/choices.js","webpack:///assets/scripts/src/reducers/general.js","webpack:///assets/scripts/src/actions/index.js","webpack:///assets/scripts/src/lib/utils.js","webpack:///assets/scripts/src/lib/polyfills.js"],"sourcesContent":["(function webpackUniversalModuleDefinition(root, factory) {\n\tif(typeof exports === 'object' && typeof module === 'object')\n\t\tmodule.exports = factory();\n\telse if(typeof define === 'function' && define.amd)\n\t\tdefine([], factory);\n\telse if(typeof exports === 'object')\n\t\texports[\"Choices\"] = factory();\n\telse\n\t\troot[\"Choices\"] = factory();\n})(this, function() {\nreturn \n\n\n// WEBPACK FOOTER //\n// webpack/universalModuleDefinition"," \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId])\n \t\t\treturn installedModules[moduleId].exports;\n\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\texports: {},\n \t\t\tid: moduleId,\n \t\t\tloaded: false\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.loaded = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"/assets/scripts/dist/\";\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(0);\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap b28c19019179aae11f77","import Fuse from 'fuse.js';\nimport classNames from 'classnames';\nimport Store from './store/index.js';\nimport {\n setIsLoading,\n addItem,\n removeItem,\n highlightItem,\n addChoice,\n filterChoices,\n activateChoices,\n addGroup,\n clearAll,\n clearChoices,\n}\nfrom './actions/index';\nimport {\n isScrolledIntoView,\n getAdjacentEl,\n wrap,\n getType,\n isType,\n isElement,\n strToEl,\n stripHTML,\n extend,\n getWidthOfInput,\n sortByAlpha,\n sortByScore,\n generateId,\n triggerEvent,\n findAncestorByAttrName\n}\nfrom './lib/utils.js';\nimport './lib/polyfills.js';\n\n/**\n * Choices\n */\nclass Choices {\n constructor(element = '[data-choice]', userConfig = {}) {\n // If there are multiple elements, create a new instance\n // for each element besides the first one (as that already has an instance)\n if (isType('String', element)) {\n const elements = document.querySelectorAll(element);\n if (elements.length > 1) {\n for (let i = 1; i < elements.length; i++) {\n const el = elements[i];\n new Choices(el, userConfig);\n }\n }\n }\n\n const defaultConfig = {\n silent: false,\n items: [],\n choices: [],\n renderChoiceLimit: -1,\n maxItemCount: -1,\n addItems: true,\n removeItems: true,\n removeItemButton: false,\n editItems: false,\n duplicateItems: true,\n delimiter: ',',\n paste: true,\n searchEnabled: true,\n searchChoices: true,\n searchFloor: 1,\n searchResultLimit: 4,\n searchFields: ['label', 'value'],\n position: 'auto',\n resetScrollPosition: true,\n regexFilter: null,\n shouldSort: true,\n shouldSortItems: false,\n sortFilter: sortByAlpha,\n placeholder: true,\n placeholderValue: null,\n searchPlaceholderValue: null,\n prependValue: null,\n appendValue: null,\n renderSelectedChoices: 'auto',\n loadingText: 'Loading...',\n noResultsText: 'No results found',\n noChoicesText: 'No choices to choose from',\n itemSelectText: 'Press to select',\n addItemText: (value) => {\n return `Press Enter to add \"${stripHTML(value)}\"`;\n },\n maxItemText: (maxItemCount) => {\n return `Only ${maxItemCount} values can be added.`;\n },\n itemComparer: (choice, item) => {\n return choice === item;\n },\n uniqueItemText: 'Only unique values can be added.',\n classNames: {\n containerOuter: 'choices',\n containerInner: 'choices__inner',\n input: 'choices__input',\n inputCloned: 'choices__input--cloned',\n list: 'choices__list',\n listItems: 'choices__list--multiple',\n listSingle: 'choices__list--single',\n listDropdown: 'choices__list--dropdown',\n item: 'choices__item',\n itemSelectable: 'choices__item--selectable',\n itemDisabled: 'choices__item--disabled',\n itemChoice: 'choices__item--choice',\n placeholder: 'choices__placeholder',\n group: 'choices__group',\n groupHeading: 'choices__heading',\n button: 'choices__button',\n activeState: 'is-active',\n focusState: 'is-focused',\n openState: 'is-open',\n disabledState: 'is-disabled',\n highlightedState: 'is-highlighted',\n hiddenState: 'is-hidden',\n flippedState: 'is-flipped',\n loadingState: 'is-loading',\n noResults: 'has-no-results',\n noChoices: 'has-no-choices'\n },\n fuseOptions: {\n include: 'score'\n },\n callbackOnInit: null,\n callbackOnCreateTemplates: null\n };\n\n this.idNames = {\n itemChoice: 'item-choice'\n };\n\n // Merge options with user options\n this.config = extend(defaultConfig, userConfig);\n\n if (this.config.renderSelectedChoices !== 'auto' && this.config.renderSelectedChoices !== 'always') {\n if (!this.config.silent) {\n console.warn(\n 'renderSelectedChoices: Possible values are \\'auto\\' and \\'always\\'. Falling back to \\'auto\\'.'\n );\n }\n this.config.renderSelectedChoices = 'auto';\n }\n\n // Create data store\n this.store = new Store(this.render);\n\n // State tracking\n this.initialised = false;\n this.currentState = {};\n this.prevState = {};\n this.currentValue = '';\n\n // Retrieve triggering element (i.e. element with 'data-choice' trigger)\n this.element = element;\n this.passedElement = isType('String', element) ? document.querySelector(element) : element;\n\n if (!this.passedElement) {\n if (!this.config.silent) {\n console.error('Passed element not found');\n }\n return;\n }\n\n this.isTextElement = this.passedElement.type === 'text';\n this.isSelectOneElement = this.passedElement.type === 'select-one';\n this.isSelectMultipleElement = this.passedElement.type === 'select-multiple';\n this.isSelectElement = this.isSelectOneElement || this.isSelectMultipleElement;\n this.isValidElementType = this.isTextElement || this.isSelectElement;\n this.isIe11 = !!(navigator.userAgent.match(/Trident/) && navigator.userAgent.match(/rv[ :]11/));\n this.isScrollingOnIe = false;\n\n\n if (this.config.shouldSortItems === true && this.isSelectOneElement) {\n if (!this.config.silent) {\n console.warn('shouldSortElements: Type of passed element is \\'select-one\\', falling back to false.');\n }\n }\n\n this.highlightPosition = 0;\n this.canSearch = this.config.searchEnabled;\n\n this.placeholder = false;\n if (!this.isSelectOneElement) {\n this.placeholder = this.config.placeholder ?\n (this.config.placeholderValue || this.passedElement.getAttribute('placeholder')) :\n false;\n }\n\n // Assign preset choices from passed object\n this.presetChoices = this.config.choices;\n\n // Assign preset items from passed object first\n this.presetItems = this.config.items;\n\n // Then add any values passed from attribute\n if (this.passedElement.value) {\n this.presetItems = this.presetItems.concat(\n this.passedElement.value.split(this.config.delimiter)\n );\n }\n\n // Set unique base Id\n this.baseId = generateId(this.passedElement, 'choices-');\n\n // Bind methods\n this.render = this.render.bind(this);\n\n // Bind event handlers\n this._onFocus = this._onFocus.bind(this);\n this._onBlur = this._onBlur.bind(this);\n this._onKeyUp = this._onKeyUp.bind(this);\n this._onKeyDown = this._onKeyDown.bind(this);\n this._onClick = this._onClick.bind(this);\n this._onTouchMove = this._onTouchMove.bind(this);\n this._onTouchEnd = this._onTouchEnd.bind(this);\n this._onMouseDown = this._onMouseDown.bind(this);\n this._onMouseOver = this._onMouseOver.bind(this);\n this._onPaste = this._onPaste.bind(this);\n this._onInput = this._onInput.bind(this);\n\n // Monitor touch taps/scrolls\n this.wasTap = true;\n\n // Cutting the mustard\n const cuttingTheMustard = 'classList' in document.documentElement;\n if (!cuttingTheMustard && !this.config.silent) {\n console.error('Choices: Your browser doesn\\'t support Choices');\n }\n\n const canInit = isElement(this.passedElement) && this.isValidElementType;\n\n if (canInit) {\n // If element has already been initialised with Choices\n if (this.passedElement.getAttribute('data-choice') === 'active') {\n return;\n }\n\n // Let's go\n this.init();\n } else if (!this.config.silent) {\n console.error('Incompatible input passed');\n }\n }\n\n /*========================================\n = Public functions =\n ========================================*/\n\n /**\n * Initialise Choices\n * @return\n * @public\n */\n init() {\n if (this.initialised === true) {\n return;\n }\n\n const callback = this.config.callbackOnInit;\n\n // Set initialise flag\n this.initialised = true;\n // Create required elements\n this._createTemplates();\n // Generate input markup\n this._createInput();\n // Subscribe store to render method\n this.store.subscribe(this.render);\n // Render any items\n this.render();\n // Trigger event listeners\n this._addEventListeners();\n\n // Run callback if it is a function\n if (callback) {\n if (isType('Function', callback)) {\n callback.call(this);\n }\n }\n }\n\n /**\n * Destroy Choices and nullify values\n * @return\n * @public\n */\n destroy() {\n if (this.initialised === false) {\n return;\n }\n\n // Remove all event listeners\n this._removeEventListeners();\n\n // Reinstate passed element\n this.passedElement.classList.remove(this.config.classNames.input, this.config.classNames.hiddenState);\n this.passedElement.removeAttribute('tabindex');\n // Recover original styles if any\n const origStyle = this.passedElement.getAttribute('data-choice-orig-style');\n if (Boolean(origStyle)) {\n this.passedElement.removeAttribute('data-choice-orig-style');\n this.passedElement.setAttribute('style', origStyle);\n } else {\n this.passedElement.removeAttribute('style');\n }\n this.passedElement.removeAttribute('aria-hidden');\n this.passedElement.removeAttribute('data-choice');\n\n // Re-assign values - this is weird, I know\n this.passedElement.value = this.passedElement.value;\n\n // Move passed element back to original position\n this.containerOuter.parentNode.insertBefore(this.passedElement, this.containerOuter);\n // Remove added elements\n this.containerOuter.parentNode.removeChild(this.containerOuter);\n\n // Clear data store\n this.clearStore();\n\n // Nullify instance-specific data\n this.config.templates = null;\n\n // Uninitialise\n this.initialised = false;\n }\n\n /**\n * Render group choices into a DOM fragment and append to choice list\n * @param {Array} groups Groups to add to list\n * @param {Array} choices Choices to add to groups\n * @param {DocumentFragment} fragment Fragment to add groups and options to (optional)\n * @return {DocumentFragment} Populated options fragment\n * @private\n */\n renderGroups(groups, choices, fragment) {\n const groupFragment = fragment || document.createDocumentFragment();\n const filter = this.config.sortFilter;\n\n // If sorting is enabled, filter groups\n if (this.config.shouldSort) {\n groups.sort(filter);\n }\n\n groups.forEach((group) => {\n // Grab options that are children of this group\n const groupChoices = choices.filter((choice) => {\n if (this.isSelectOneElement) {\n return choice.groupId === group.id;\n }\n return choice.groupId === group.id && !choice.selected;\n });\n\n if (groupChoices.length >= 1) {\n const dropdownGroup = this._getTemplate('choiceGroup', group);\n groupFragment.appendChild(dropdownGroup);\n this.renderChoices(groupChoices, groupFragment, true);\n }\n });\n\n return groupFragment;\n }\n\n /**\n * Render choices into a DOM fragment and append to choice list\n * @param {Array} choices Choices to add to list\n * @param {DocumentFragment} fragment Fragment to add choices to (optional)\n * @return {DocumentFragment} Populated choices fragment\n * @private\n */\n renderChoices(choices, fragment, withinGroup = false) {\n // Create a fragment to store our list items (so we don't have to update the DOM for each item)\n const choicesFragment = fragment || document.createDocumentFragment();\n const { renderSelectedChoices, searchResultLimit, renderChoiceLimit } = this.config;\n const filter = this.isSearching ? sortByScore : this.config.sortFilter;\n const appendChoice = (choice) => {\n const shouldRender = renderSelectedChoices === 'auto' ?\n (this.isSelectOneElement || !choice.selected) :\n true;\n if (shouldRender) {\n const dropdownItem = this._getTemplate('choice', choice);\n choicesFragment.appendChild(dropdownItem);\n }\n };\n\n let rendererableChoices = choices;\n\n if (renderSelectedChoices === 'auto' && !this.isSelectOneElement) {\n rendererableChoices = choices.filter(choice => !choice.selected);\n }\n\n // Split array into placeholders and \"normal\" choices\n const { placeholderChoices, normalChoices } = rendererableChoices.reduce((acc, choice) => {\n if (choice.placeholder) {\n acc.placeholderChoices.push(choice);\n } else {\n acc.normalChoices.push(choice);\n }\n return acc;\n }, { placeholderChoices: [], normalChoices: [] });\n\n // If sorting is enabled or the user is searching, filter choices\n if (this.config.shouldSort || this.isSearching) {\n normalChoices.sort(filter);\n }\n\n let choiceLimit = rendererableChoices.length;\n\n // Prepend placeholeder\n const sortedChoices = [...placeholderChoices, ...normalChoices];\n\n if (this.isSearching) {\n choiceLimit = searchResultLimit;\n } else if (renderChoiceLimit > 0 && !withinGroup) {\n choiceLimit = renderChoiceLimit;\n }\n\n // Add each choice to dropdown within range\n for (let i = 0; i < choiceLimit; i++) {\n if (sortedChoices[i]) {\n appendChoice(sortedChoices[i]);\n }\n };\n\n return choicesFragment;\n }\n\n /**\n * Render items into a DOM fragment and append to items list\n * @param {Array} items Items to add to list\n * @param {DocumentFragment} [fragment] Fragment to add items to (optional)\n * @return\n * @private\n */\n renderItems(items, fragment = null) {\n // Create fragment to add elements to\n const itemListFragment = fragment || document.createDocumentFragment();\n\n // If sorting is enabled, filter items\n if (this.config.shouldSortItems && !this.isSelectOneElement) {\n items.sort(this.config.sortFilter);\n }\n\n if (this.isTextElement) {\n // Simplify store data to just values\n const itemsFiltered = this.store.getItemsReducedToValues(items);\n const itemsFilteredString = itemsFiltered.join(this.config.delimiter);\n // Update the value of the hidden input\n this.passedElement.setAttribute('value', itemsFilteredString);\n this.passedElement.value = itemsFilteredString;\n } else {\n const selectedOptionsFragment = document.createDocumentFragment();\n\n // Add each list item to list\n items.forEach((item) => {\n // Create a standard select option\n const option = this._getTemplate('option', item);\n // Append it to fragment\n selectedOptionsFragment.appendChild(option);\n });\n\n // Update selected choices\n this.passedElement.innerHTML = '';\n this.passedElement.appendChild(selectedOptionsFragment);\n }\n\n // Add each list item to list\n items.forEach((item) => {\n // Create new list element\n const listItem = this._getTemplate('item', item);\n // Append it to list\n itemListFragment.appendChild(listItem);\n });\n\n return itemListFragment;\n }\n\n /**\n * Render DOM with values\n * @return\n * @private\n */\n render() {\n if(this.store.isLoading()) {\n return;\n }\n\n this.currentState = this.store.getState();\n\n // Only render if our state has actually changed\n if (this.currentState !== this.prevState) {\n // Choices\n if (\n this.currentState.choices !== this.prevState.choices ||\n this.currentState.groups !== this.prevState.groups ||\n this.currentState.items !== this.prevState.items\n ) {\n if (this.isSelectElement) {\n // Get active groups/choices\n const activeGroups = this.store.getGroupsFilteredByActive();\n const activeChoices = this.store.getChoicesFilteredByActive();\n\n let choiceListFragment = document.createDocumentFragment();\n\n // Clear choices\n this.choiceList.innerHTML = '';\n\n // Scroll back to top of choices list\n if (this.config.resetScrollPosition) {\n this.choiceList.scrollTop = 0;\n }\n\n // If we have grouped options\n if (activeGroups.length >= 1 && this.isSearching !== true) {\n choiceListFragment = this.renderGroups(activeGroups, activeChoices, choiceListFragment);\n } else if (activeChoices.length >= 1) {\n choiceListFragment = this.renderChoices(activeChoices, choiceListFragment);\n }\n\n const activeItems = this.store.getItemsFilteredByActive();\n const canAddItem = this._canAddItem(activeItems, this.input.value);\n\n // If we have choices to show\n if (choiceListFragment.childNodes && choiceListFragment.childNodes.length > 0) {\n // ...and we can select them\n if (canAddItem.response) {\n // ...append them and highlight the first choice\n this.choiceList.appendChild(choiceListFragment);\n this._highlightChoice();\n } else {\n // ...otherwise show a notice\n this.choiceList.appendChild(this._getTemplate('notice', canAddItem.notice));\n }\n } else {\n // Otherwise show a notice\n let dropdownItem;\n let notice;\n\n if (this.isSearching) {\n notice = isType('Function', this.config.noResultsText) ?\n this.config.noResultsText() :\n this.config.noResultsText;\n\n dropdownItem = this._getTemplate('notice', notice, 'no-results');\n } else {\n notice = isType('Function', this.config.noChoicesText) ?\n this.config.noChoicesText() :\n this.config.noChoicesText;\n\n dropdownItem = this._getTemplate('notice', notice, 'no-choices');\n }\n\n this.choiceList.appendChild(dropdownItem);\n }\n }\n }\n\n // Items\n if (this.currentState.items !== this.prevState.items) {\n // Get active items (items that can be selected)\n const activeItems = this.store.getItemsFilteredByActive();\n\n // Clear list\n this.itemList.innerHTML = '';\n\n if (activeItems && activeItems) {\n // Create a fragment to store our list items\n // (so we don't have to update the DOM for each item)\n const itemListFragment = this.renderItems(activeItems);\n\n // If we have items to add\n if (itemListFragment.childNodes) {\n // Update list\n this.itemList.appendChild(itemListFragment);\n }\n }\n }\n\n this.prevState = this.currentState;\n }\n }\n\n /**\n * Select item (a selected item can be deleted)\n * @param {Element} item Element to select\n * @param {Boolean} [runEvent=true] Whether to trigger 'highlightItem' event\n * @return {Object} Class instance\n * @public\n */\n highlightItem(item, runEvent = true) {\n if (!item) {\n return this;\n }\n\n const id = item.id;\n const groupId = item.groupId;\n const group = groupId >= 0 ? this.store.getGroupById(groupId) : null;\n\n this.store.dispatch(\n highlightItem(id, true)\n );\n\n if (runEvent) {\n if (group && group.value) {\n triggerEvent(this.passedElement, 'highlightItem', {\n id,\n value: item.value,\n label: item.label,\n groupValue: group.value\n });\n } else {\n triggerEvent(this.passedElement, 'highlightItem', {\n id,\n value: item.value,\n label: item.label,\n });\n }\n }\n\n return this;\n }\n\n /**\n * Deselect item\n * @param {Element} item Element to de-select\n * @return {Object} Class instance\n * @public\n */\n unhighlightItem(item) {\n if (!item) {\n return this;\n }\n\n const id = item.id;\n const groupId = item.groupId;\n const group = groupId >= 0 ? this.store.getGroupById(groupId) : null;\n\n this.store.dispatch(\n highlightItem(id, false)\n );\n\n if (group && group.value) {\n triggerEvent(this.passedElement, 'unhighlightItem', {\n id,\n value: item.value,\n label: item.label,\n groupValue: group.value\n });\n } else {\n triggerEvent(this.passedElement, 'unhighlightItem', {\n id,\n value: item.value,\n label: item.label,\n });\n }\n\n return this;\n }\n\n /**\n * Highlight items within store\n * @return {Object} Class instance\n * @public\n */\n highlightAll() {\n const items = this.store.getItems();\n items.forEach((item) => {\n this.highlightItem(item);\n });\n\n return this;\n }\n\n /**\n * Deselect items within store\n * @return {Object} Class instance\n * @public\n */\n unhighlightAll() {\n const items = this.store.getItems();\n items.forEach((item) => {\n this.unhighlightItem(item);\n });\n\n return this;\n }\n\n /**\n * Remove an item from the store by its value\n * @param {String} value Value to search for\n * @return {Object} Class instance\n * @public\n */\n removeItemsByValue(value) {\n if (!value || !isType('String', value)) {\n return this;\n }\n\n const items = this.store.getItemsFilteredByActive();\n\n items.forEach((item) => {\n if (item.value === value) {\n this._removeItem(item);\n }\n });\n\n return this;\n }\n\n /**\n * Remove all items from store array\n * @note Removed items are soft deleted\n * @param {Number} excludedId Optionally exclude item by ID\n * @return {Object} Class instance\n * @public\n */\n removeActiveItems(excludedId) {\n const items = this.store.getItemsFilteredByActive();\n\n items.forEach((item) => {\n if (item.active && excludedId !== item.id) {\n this._removeItem(item);\n }\n });\n\n return this;\n }\n\n /**\n * Remove all selected items from store\n * @note Removed items are soft deleted\n * @return {Object} Class instance\n * @public\n */\n removeHighlightedItems(runEvent = false) {\n const items = this.store.getItemsFilteredByActive();\n\n items.forEach((item) => {\n if (item.highlighted && item.active) {\n this._removeItem(item);\n // If this action was performed by the user\n // trigger the event\n if (runEvent) {\n this._triggerChange(item.value);\n }\n }\n });\n\n return this;\n }\n\n /**\n * Show dropdown to user by adding active state class\n * @return {Object} Class instance\n * @public\n */\n showDropdown(focusInput = false) {\n const body = document.body;\n const html = document.documentElement;\n const winHeight = Math.max(\n body.scrollHeight,\n body.offsetHeight,\n html.clientHeight,\n html.scrollHeight,\n html.offsetHeight\n );\n\n this.containerOuter.classList.add(this.config.classNames.openState);\n this.containerOuter.setAttribute('aria-expanded', 'true');\n this.dropdown.classList.add(this.config.classNames.activeState);\n this.dropdown.setAttribute('aria-expanded', 'true');\n\n const dimensions = this.dropdown.getBoundingClientRect();\n const dropdownPos = Math.ceil(dimensions.top + window.scrollY + this.dropdown.offsetHeight);\n\n // If flip is enabled and the dropdown bottom position is greater than the window height flip the dropdown.\n let shouldFlip = false;\n if (this.config.position === 'auto') {\n shouldFlip = dropdownPos >= winHeight;\n } else if (this.config.position === 'top') {\n shouldFlip = true;\n }\n\n if (shouldFlip) {\n this.containerOuter.classList.add(this.config.classNames.flippedState);\n }\n\n // Optionally focus the input if we have a search input\n if (focusInput && this.canSearch && document.activeElement !== this.input) {\n this.input.focus();\n }\n\n triggerEvent(this.passedElement, 'showDropdown', {});\n\n return this;\n }\n\n /**\n * Hide dropdown from user\n * @return {Object} Class instance\n * @public\n */\n hideDropdown(blurInput = false) {\n // A dropdown flips if it does not have space within the page\n const isFlipped = this.containerOuter.classList.contains(this.config.classNames.flippedState);\n\n this.containerOuter.classList.remove(this.config.classNames.openState);\n this.containerOuter.setAttribute('aria-expanded', 'false');\n this.dropdown.classList.remove(this.config.classNames.activeState);\n this.dropdown.setAttribute('aria-expanded', 'false');\n\n if (isFlipped) {\n this.containerOuter.classList.remove(this.config.classNames.flippedState);\n }\n\n // Optionally blur the input if we have a search input\n if (blurInput && this.canSearch && document.activeElement === this.input) {\n this.input.blur();\n }\n\n triggerEvent(this.passedElement, 'hideDropdown', {});\n\n return this;\n }\n\n /**\n * Determine whether to hide or show dropdown based on its current state\n * @return {Object} Class instance\n * @public\n */\n toggleDropdown() {\n const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState);\n if (hasActiveDropdown) {\n this.hideDropdown();\n } else {\n this.showDropdown(true);\n }\n\n return this;\n }\n\n /**\n * Get value(s) of input (i.e. inputted items (text) or selected choices (select))\n * @param {Boolean} valueOnly Get only values of selected items, otherwise return selected items\n * @return {Array/String} selected value (select-one) or array of selected items (inputs & select-multiple)\n * @public\n */\n getValue(valueOnly = false) {\n const items = this.store.getItemsFilteredByActive();\n const selectedItems = [];\n\n items.forEach((item) => {\n if (this.isTextElement) {\n selectedItems.push(valueOnly ? item.value : item);\n } else if (item.active) {\n selectedItems.push(valueOnly ? item.value : item);\n }\n });\n\n if (this.isSelectOneElement) {\n return selectedItems[0];\n }\n\n return selectedItems;\n }\n\n /**\n * Set value of input. If the input is a select box, a choice will be created and selected otherwise\n * an item will created directly.\n * @param {Array} args Array of value objects or value strings\n * @return {Object} Class instance\n * @public\n */\n setValue(args) {\n if (this.initialised === true) {\n // Convert args to an iterable array\n const values = [...args],\n handleValue = (item) => {\n const itemType = getType(item);\n if (itemType === 'Object') {\n if (!item.value) {\n return;\n }\n\n // If we are dealing with a select input, we need to create an option first\n // that is then selected. For text inputs we can just add items normally.\n if (!this.isTextElement) {\n this._addChoice(\n item.value,\n item.label,\n true,\n false,\n -1,\n item.customProperties,\n item.placeholder\n );\n } else {\n this._addItem(\n item.value,\n item.label,\n item.id,\n undefined,\n item.customProperties,\n item.placeholder\n );\n }\n } else if (itemType === 'String') {\n if (!this.isTextElement) {\n this._addChoice(\n item,\n item,\n true,\n false,\n -1,\n null\n );\n } else {\n this._addItem(item);\n }\n }\n };\n\n if (values.length > 1) {\n values.forEach((value) => {\n handleValue(value);\n });\n } else {\n handleValue(values[0]);\n }\n }\n return this;\n }\n\n /**\n * Select value of select box via the value of an existing choice\n * @param {Array/String} value An array of strings of a single string\n * @return {Object} Class instance\n * @public\n */\n setValueByChoice(value) {\n if (!this.isTextElement) {\n const choices = this.store.getChoices();\n // If only one value has been passed, convert to array\n const choiceValue = isType('Array', value) ? value : [value];\n\n // Loop through each value and\n choiceValue.forEach((val) => {\n const foundChoice = choices.find((choice) => {\n // Check 'value' property exists and the choice isn't already selected\n return this.config.itemComparer(choice.value, val);\n });\n\n if (foundChoice) {\n if (!foundChoice.selected) {\n this._addItem(\n foundChoice.value,\n foundChoice.label,\n foundChoice.id,\n foundChoice.groupId,\n foundChoice.customProperties,\n foundChoice.placeholder,\n foundChoice.keyCode\n );\n } else if (!this.config.silent) {\n console.warn('Attempting to select choice already selected');\n }\n } else if (!this.config.silent) {\n console.warn('Attempting to select choice that does not exist');\n }\n });\n }\n return this;\n }\n\n /**\n * Direct populate choices\n * @param {Array} choices - Choices to insert\n * @param {String} value - Name of 'value' property\n * @param {String} label - Name of 'label' property\n * @param {Boolean} replaceChoices Whether existing choices should be removed\n * @return {Object} Class instance\n * @public\n */\n setChoices(choices, value, label, replaceChoices = false) {\n if (this.initialised === true) {\n if (this.isSelectElement) {\n if (!isType('Array', choices) || !value) {\n return this;\n }\n\n // Clear choices if needed\n if (replaceChoices) {\n this._clearChoices();\n }\n\n this._setLoading(true);\n\n // Add choices if passed\n if (choices && choices.length) {\n this.containerOuter.classList.remove(this.config.classNames.loadingState);\n choices.forEach((result) => {\n if (result.choices) {\n this._addGroup(\n result,\n (result.id || null),\n value,\n label\n );\n } else {\n this._addChoice(\n result[value],\n result[label],\n result.selected,\n result.disabled,\n undefined,\n result.customProperties,\n result.placeholder\n );\n }\n });\n }\n\n this._setLoading(false);\n\n }\n }\n return this;\n }\n\n /**\n * Clear items,choices and groups\n * @note Hard delete\n * @return {Object} Class instance\n * @public\n */\n clearStore() {\n this.store.dispatch(\n clearAll()\n );\n return this;\n }\n\n /**\n * Set value of input to blank\n * @return {Object} Class instance\n * @public\n */\n clearInput() {\n if (this.input.value){\n this.input.value = '';\n }\n if (!this.isSelectOneElement) {\n this._setInputWidth();\n }\n if (!this.isTextElement && this.config.searchEnabled) {\n this.isSearching = false;\n this.store.dispatch(\n activateChoices(true)\n );\n }\n return this;\n }\n\n /**\n * Enable interaction with Choices\n * @return {Object} Class instance\n */\n enable() {\n if (this.initialised) {\n this.passedElement.disabled = false;\n const isDisabled = this.containerOuter.classList.contains(this.config.classNames.disabledState);\n if (isDisabled) {\n this._addEventListeners();\n this.passedElement.removeAttribute('disabled');\n this.input.removeAttribute('disabled');\n this.containerOuter.classList.remove(this.config.classNames.disabledState);\n this.containerOuter.removeAttribute('aria-disabled');\n if (this.isSelectOneElement) {\n this.containerOuter.setAttribute('tabindex', '0');\n }\n }\n }\n return this;\n }\n\n /**\n * Disable interaction with Choices\n * @return {Object} Class instance\n * @public\n */\n disable() {\n if (this.initialised) {\n this.passedElement.disabled = true;\n const isEnabled = !this.containerOuter.classList.contains(this.config.classNames.disabledState);\n if (isEnabled) {\n this._removeEventListeners();\n this.passedElement.setAttribute('disabled', '');\n this.input.setAttribute('disabled', '');\n this.containerOuter.classList.add(this.config.classNames.disabledState);\n this.containerOuter.setAttribute('aria-disabled', 'true');\n if (this.isSelectOneElement) {\n this.containerOuter.setAttribute('tabindex', '-1');\n }\n }\n }\n return this;\n }\n\n /**\n * Populate options via ajax callback\n * @param {Function} fn Function that actually makes an AJAX request\n * @return {Object} Class instance\n * @public\n */\n ajax(fn) {\n if (this.initialised === true) {\n if (this.isSelectElement) {\n // Show loading text\n requestAnimationFrame(() => {\n this._handleLoadingState(true);\n });\n // Run callback\n fn(this._ajaxCallback());\n }\n }\n return this;\n }\n\n /*===== End of Public functions ======*/\n\n /*=============================================\n = Private functions =\n =============================================*/\n\n /**\n * Call change callback\n * @param {String} value - last added/deleted/selected value\n * @return\n * @private\n */\n _triggerChange(value) {\n if (!value) {\n return;\n }\n\n triggerEvent(this.passedElement, 'change', {\n value\n });\n }\n\n /**\n * Process enter/click of an item button\n * @param {Array} activeItems The currently active items\n * @param {Element} element Button being interacted with\n * @return\n * @private\n */\n _handleButtonAction(activeItems, element) {\n if (!activeItems || !element) {\n return;\n }\n\n // If we are clicking on a button\n if (this.config.removeItems && this.config.removeItemButton) {\n const itemId = element.parentNode.getAttribute('data-id');\n const itemToRemove = activeItems.find((item) => item.id === parseInt(itemId, 10));\n\n // Remove item associated with button\n this._removeItem(itemToRemove);\n this._triggerChange(itemToRemove.value);\n\n if (this.isSelectOneElement) {\n this._selectPlaceholderChoice();\n }\n }\n }\n\n /**\n * Select placeholder choice\n */\n _selectPlaceholderChoice() {\n const placeholderChoice = this.store.getPlaceholderChoice();\n\n if (placeholderChoice) {\n this._addItem(\n placeholderChoice.value,\n placeholderChoice.label,\n placeholderChoice.id,\n placeholderChoice.groupId,\n null,\n placeholderChoice.placeholder\n );\n this._triggerChange(placeholderChoice.value);\n }\n }\n\n /**\n * Process click of an item\n * @param {Array} activeItems The currently active items\n * @param {Element} element Item being interacted with\n * @param {Boolean} hasShiftKey Whether the user has the shift key active\n * @return\n * @private\n */\n _handleItemAction(activeItems, element, hasShiftKey = false) {\n if (!activeItems || !element) {\n return;\n }\n\n // If we are clicking on an item\n if (this.config.removeItems && !this.isSelectOneElement) {\n const passedId = element.getAttribute('data-id');\n\n // We only want to select one item with a click\n // so we deselect any items that aren't the target\n // unless shift is being pressed\n activeItems.forEach((item) => {\n if (item.id === parseInt(passedId, 10) && !item.highlighted) {\n this.highlightItem(item);\n } else if (!hasShiftKey) {\n if (item.highlighted) {\n this.unhighlightItem(item);\n }\n }\n });\n\n // Focus input as without focus, a user cannot do anything with a\n // highlighted item\n if (document.activeElement !== this.input) {\n this.input.focus();\n }\n }\n }\n\n /**\n * Process click of a choice\n * @param {Array} activeItems The currently active items\n * @param {Element} element Choice being interacted with\n * @return\n */\n _handleChoiceAction(activeItems, element) {\n if (!activeItems || !element) {\n return;\n }\n\n // If we are clicking on an option\n const id = element.getAttribute('data-id');\n const choice = this.store.getChoiceById(id);\n const passedKeyCode = activeItems[0] && activeItems[0].keyCode ? activeItems[0].keyCode : null;\n const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState);\n\n // Update choice keyCode\n choice.keyCode = passedKeyCode;\n\n triggerEvent(this.passedElement, 'choice', {\n choice,\n });\n\n if (choice && !choice.selected && !choice.disabled) {\n const canAddItem = this._canAddItem(activeItems, choice.value);\n\n if (canAddItem.response) {\n this._addItem(\n choice.value,\n choice.label,\n choice.id,\n choice.groupId,\n choice.customProperties,\n choice.placeholder,\n choice.keyCode\n );\n this._triggerChange(choice.value);\n }\n }\n\n this.clearInput();\n\n // We wont to close the dropdown if we are dealing with a single select box\n if (hasActiveDropdown && this.isSelectOneElement) {\n this.hideDropdown();\n this.containerOuter.focus();\n }\n }\n\n /**\n * Process back space event\n * @param {Array} activeItems items\n * @return\n * @private\n */\n _handleBackspace(activeItems) {\n if (this.config.removeItems && activeItems) {\n const lastItem = activeItems[activeItems.length - 1];\n const hasHighlightedItems = activeItems.some(item => item.highlighted);\n\n // If editing the last item is allowed and there are not other selected items,\n // we can edit the item value. Otherwise if we can remove items, remove all selected items\n if (this.config.editItems && !hasHighlightedItems && lastItem) {\n this.input.value = lastItem.value;\n this._setInputWidth();\n this._removeItem(lastItem);\n this._triggerChange(lastItem.value);\n } else {\n if (!hasHighlightedItems) {\n this.highlightItem(lastItem, false);\n }\n this.removeHighlightedItems(true);\n }\n }\n }\n\n /**\n * Validates whether an item can be added by a user\n * @param {Array} activeItems The currently active items\n * @param {String} value Value of item to add\n * @return {Object} Response: Whether user can add item\n * Notice: Notice show in dropdown\n */\n _canAddItem(activeItems, value) {\n let canAddItem = true;\n let notice = isType('Function', this.config.addItemText) ?\n this.config.addItemText(value) :\n this.config.addItemText;\n\n if (this.isSelectMultipleElement || this.isTextElement) {\n if (this.config.maxItemCount > 0 && this.config.maxItemCount <= activeItems.length) {\n // If there is a max entry limit and we have reached that limit\n // don't update\n canAddItem = false;\n notice = isType('Function', this.config.maxItemText) ?\n this.config.maxItemText(this.config.maxItemCount) :\n this.config.maxItemText;\n }\n }\n\n if (this.isTextElement && this.config.addItems && canAddItem) {\n // If a user has supplied a regular expression filter\n if (this.config.regexFilter) {\n // Determine whether we can update based on whether\n // our regular expression passes\n canAddItem = this._regexFilter(value);\n }\n }\n\n // If no duplicates are allowed, and the value already exists\n // in the array\n const isUnique = !activeItems.some((item) => {\n if (isType('String', value)) {\n return item.value === value.trim();\n }\n\n return item.value === value;\n });\n\n if (\n !isUnique &&\n !this.config.duplicateItems &&\n !this.isSelectOneElement &&\n canAddItem\n ) {\n canAddItem = false;\n notice = isType('Function', this.config.uniqueItemText) ?\n this.config.uniqueItemText(value) :\n this.config.uniqueItemText;\n }\n\n return {\n response: canAddItem,\n notice,\n };\n }\n\n /**\n * Apply or remove a loading state to the component.\n * @param {Boolean} setLoading default value set to 'true'.\n * @return\n * @private\n */\n _handleLoadingState(setLoading = true) {\n let placeholderItem = this.itemList.querySelector(`.${this.config.classNames.placeholder}`);\n if (setLoading) {\n this.containerOuter.classList.add(this.config.classNames.loadingState);\n this.containerOuter.setAttribute('aria-busy', 'true');\n if (this.isSelectOneElement) {\n if (!placeholderItem) {\n placeholderItem = this._getTemplate('placeholder', this.config.loadingText);\n this.itemList.appendChild(placeholderItem);\n } else {\n placeholderItem.innerHTML = this.config.loadingText;\n }\n } else {\n this.input.placeholder = this.config.loadingText;\n }\n } else {\n // Remove loading states/text\n this.containerOuter.classList.remove(this.config.classNames.loadingState);\n\n if (this.isSelectOneElement) {\n placeholderItem.innerHTML = (this.placeholder || '');\n } else {\n this.input.placeholder = (this.placeholder || '');\n }\n }\n }\n\n /**\n * Retrieve the callback used to populate component's choices in an async way.\n * @returns {Function} The callback as a function.\n * @private\n */\n _ajaxCallback() {\n return (results, value, label) => {\n if (!results || !value) {\n return;\n }\n\n const parsedResults = isType('Object', results) ? [results] : results;\n\n if (parsedResults && isType('Array', parsedResults) && parsedResults.length) {\n // Remove loading states/text\n this._handleLoadingState(false);\n // Add each result as a choice\n\n this._setLoading(true);\n\n parsedResults.forEach((result) => {\n if (result.choices) {\n const groupId = (result.id || null);\n this._addGroup(\n result,\n groupId,\n value,\n label\n );\n } else {\n this._addChoice(\n result[value],\n result[label],\n result.selected,\n result.disabled,\n undefined,\n result.customProperties,\n result.placeholder\n );\n }\n });\n\n this._setLoading(false);\n\n if (this.isSelectOneElement) {\n this._selectPlaceholderChoice();\n }\n } else {\n // No results, remove loading state\n this._handleLoadingState(false);\n }\n\n this.containerOuter.removeAttribute('aria-busy');\n };\n }\n\n /**\n * Filter choices based on search value\n * @param {String} value Value to filter by\n * @return\n * @private\n */\n _searchChoices(value) {\n const newValue = isType('String', value) ? value.trim() : value;\n const currentValue = isType('String', this.currentValue) ? this.currentValue.trim() : this.currentValue;\n\n // If new value matches the desired length and is not the same as the current value with a space\n if (newValue.length >= 1 && newValue !== `${currentValue} `) {\n const haystack = this.store.getSearchableChoices();\n const needle = newValue;\n const keys = isType('Array', this.config.searchFields) ? this.config.searchFields : [this.config.searchFields];\n const options = Object.assign(this.config.fuseOptions, { keys });\n const fuse = new Fuse(haystack, options);\n const results = fuse.search(needle);\n\n this.currentValue = newValue;\n this.highlightPosition = 0;\n this.isSearching = true;\n this.store.dispatch(\n filterChoices(results)\n );\n\n return results.length;\n }\n\n return 0;\n }\n\n /**\n * Determine the action when a user is searching\n * @param {String} value Value entered by user\n * @return\n * @private\n */\n _handleSearch(value) {\n if (!value) {\n return;\n }\n\n const choices = this.store.getChoices();\n const hasUnactiveChoices = choices.some(option => !option.active);\n\n // Run callback if it is a function\n if (this.input === document.activeElement) {\n // Check that we have a value to search and the input was an alphanumeric character\n if (value && value.length >= this.config.searchFloor) {\n let resultCount = 0;\n // Check flag to filter search input\n if (this.config.searchChoices) {\n // Filter available choices\n resultCount = this._searchChoices(value);\n }\n // Trigger search event\n triggerEvent(this.passedElement, 'search', {\n value,\n resultCount\n });\n } else if (hasUnactiveChoices) {\n // Otherwise reset choices to active\n this.isSearching = false;\n this.store.dispatch(\n activateChoices(true)\n );\n }\n }\n }\n\n /**\n * Trigger event listeners\n * @return\n * @private\n */\n _addEventListeners() {\n document.addEventListener('keyup', this._onKeyUp);\n document.addEventListener('keydown', this._onKeyDown);\n document.addEventListener('click', this._onClick);\n document.addEventListener('touchmove', this._onTouchMove);\n document.addEventListener('touchend', this._onTouchEnd);\n document.addEventListener('mousedown', this._onMouseDown);\n document.addEventListener('mouseover', this._onMouseOver);\n\n if (this.isSelectOneElement) {\n this.containerOuter.addEventListener('focus', this._onFocus);\n this.containerOuter.addEventListener('blur', this._onBlur);\n }\n\n this.input.addEventListener('input', this._onInput);\n this.input.addEventListener('paste', this._onPaste);\n this.input.addEventListener('focus', this._onFocus);\n this.input.addEventListener('blur', this._onBlur);\n }\n\n /**\n * Remove event listeners\n * @return\n * @private\n */\n _removeEventListeners() {\n document.removeEventListener('keyup', this._onKeyUp);\n document.removeEventListener('keydown', this._onKeyDown);\n document.removeEventListener('click', this._onClick);\n document.removeEventListener('touchmove', this._onTouchMove);\n document.removeEventListener('touchend', this._onTouchEnd);\n document.removeEventListener('mousedown', this._onMouseDown);\n document.removeEventListener('mouseover', this._onMouseOver);\n\n if (this.isSelectOneElement) {\n this.containerOuter.removeEventListener('focus', this._onFocus);\n this.containerOuter.removeEventListener('blur', this._onBlur);\n }\n\n this.input.removeEventListener('input', this._onInput);\n this.input.removeEventListener('paste', this._onPaste);\n this.input.removeEventListener('focus', this._onFocus);\n this.input.removeEventListener('blur', this._onBlur);\n }\n\n /**\n * Set the correct input width based on placeholder\n * value or input value\n * @return\n */\n _setInputWidth() {\n if (this.placeholder) {\n // If there is a placeholder, we only want to set the width of the input when it is a greater\n // length than 75% of the placeholder. This stops the input jumping around.\n if (this.input.value && this.input.value.length >= (this.placeholder.length / 1.25)) {\n this.input.style.width = getWidthOfInput(this.input);\n }\n } else {\n // If there is no placeholder, resize input to contents\n this.input.style.width = getWidthOfInput(this.input);\n }\n }\n\n /**\n * Key down event\n * @param {Object} e Event\n * @return\n */\n _onKeyDown(e) {\n if (e.target !== this.input && !this.containerOuter.contains(e.target)) {\n return;\n }\n\n const target = e.target;\n const activeItems = this.store.getItemsFilteredByActive();\n const hasFocusedInput = this.input === document.activeElement;\n const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState);\n const hasItems = this.itemList && this.itemList.children;\n const keyString = String.fromCharCode(e.keyCode);\n\n const backKey = 46;\n const deleteKey = 8;\n const enterKey = 13;\n const aKey = 65;\n const escapeKey = 27;\n const upKey = 38;\n const downKey = 40;\n const pageUpKey = 33;\n const pageDownKey = 34;\n const ctrlDownKey = e.ctrlKey || e.metaKey;\n\n // If a user is typing and the dropdown is not active\n if (!this.isTextElement && /[a-zA-Z0-9-_ ]/.test(keyString) && !hasActiveDropdown) {\n this.showDropdown(true);\n }\n\n this.canSearch = this.config.searchEnabled;\n\n const onAKey = () => {\n // If CTRL + A or CMD + A have been pressed and there are items to select\n if (ctrlDownKey && hasItems) {\n this.canSearch = false;\n if (this.config.removeItems && !this.input.value && this.input === document.activeElement) {\n // Highlight items\n this.highlightAll();\n }\n }\n };\n\n const onEnterKey = () => {\n // If enter key is pressed and the input has a value\n if (this.isTextElement && target.value) {\n const value = this.input.value;\n const canAddItem = this._canAddItem(activeItems, value);\n\n // All is good, add\n if (canAddItem.response) {\n if (hasActiveDropdown) {\n this.hideDropdown();\n }\n this._addItem(value);\n this._triggerChange(value);\n this.clearInput();\n }\n }\n\n if (target.hasAttribute('data-button')) {\n this._handleButtonAction(activeItems, target);\n e.preventDefault();\n }\n\n if (hasActiveDropdown) {\n e.preventDefault();\n const highlighted = this.dropdown.querySelector(`.${this.config.classNames.highlightedState}`);\n\n // If we have a highlighted choice\n if (highlighted) {\n // add enter keyCode value\n if (activeItems[0]) {\n activeItems[0].keyCode = enterKey;\n }\n this._handleChoiceAction(activeItems, highlighted);\n }\n\n } else if (this.isSelectOneElement) {\n // Open single select dropdown if it's not active\n if (!hasActiveDropdown) {\n this.showDropdown(true);\n e.preventDefault();\n }\n }\n };\n\n const onEscapeKey = () => {\n if (hasActiveDropdown) {\n this.toggleDropdown();\n this.containerOuter.focus();\n }\n };\n\n const onDirectionKey = () => {\n // If up or down key is pressed, traverse through options\n if (hasActiveDropdown || this.isSelectOneElement) {\n // Show dropdown if focus\n if (!hasActiveDropdown) {\n this.showDropdown(true);\n }\n\n this.canSearch = false;\n\n const directionInt = e.keyCode === downKey || e.keyCode === pageDownKey ? 1 : -1;\n const skipKey = e.metaKey || e.keyCode === pageDownKey || e.keyCode === pageUpKey;\n\n let nextEl;\n if (skipKey) {\n if (directionInt > 0) {\n nextEl = Array.from(this.dropdown.querySelectorAll('[data-choice-selectable]')).pop();\n } else {\n nextEl = this.dropdown.querySelector('[data-choice-selectable]');\n }\n } else {\n const currentEl = this.dropdown.querySelector(`.${this.config.classNames.highlightedState}`);\n if (currentEl) {\n nextEl = getAdjacentEl(currentEl, '[data-choice-selectable]', directionInt);\n } else {\n nextEl = this.dropdown.querySelector('[data-choice-selectable]');\n }\n }\n\n if (nextEl) {\n // We prevent default to stop the cursor moving\n // when pressing the arrow\n if (!isScrolledIntoView(nextEl, this.choiceList, directionInt)) {\n this._scrollToChoice(nextEl, directionInt);\n }\n this._highlightChoice(nextEl);\n }\n\n // Prevent default to maintain cursor position whilst\n // traversing dropdown options\n e.preventDefault();\n }\n };\n\n const onDeleteKey = () => {\n // If backspace or delete key is pressed and the input has no value\n if (hasFocusedInput && !e.target.value && !this.isSelectOneElement) {\n this._handleBackspace(activeItems);\n e.preventDefault();\n }\n };\n\n // Map keys to key actions\n const keyDownActions = {\n [aKey]: onAKey,\n [enterKey]: onEnterKey,\n [escapeKey]: onEscapeKey,\n [upKey]: onDirectionKey,\n [pageUpKey]: onDirectionKey,\n [downKey]: onDirectionKey,\n [pageDownKey]: onDirectionKey,\n [deleteKey]: onDeleteKey,\n [backKey]: onDeleteKey,\n };\n\n // If keycode has a function, run it\n if (keyDownActions[e.keyCode]) {\n keyDownActions[e.keyCode]();\n }\n }\n\n /**\n * Key up event\n * @param {Object} e Event\n * @return\n * @private\n */\n _onKeyUp(e) {\n if (e.target !== this.input) {\n return;\n }\n\n const value = this.input.value;\n const activeItems = this.store.getItemsFilteredByActive();\n const canAddItem = this._canAddItem(activeItems, value);\n\n // We are typing into a text input and have a value, we want to show a dropdown\n // notice. Otherwise hide the dropdown\n if (this.isTextElement) {\n const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState);\n if (value) {\n\n if (canAddItem.notice) {\n const dropdownItem = this._getTemplate('notice', canAddItem.notice);\n this.dropdown.innerHTML = dropdownItem.outerHTML;\n }\n\n if (canAddItem.response === true) {\n if (!hasActiveDropdown) {\n this.showDropdown();\n }\n } else if (!canAddItem.notice && hasActiveDropdown) {\n this.hideDropdown();\n }\n } else if (hasActiveDropdown) {\n this.hideDropdown();\n }\n } else {\n const backKey = 46;\n const deleteKey = 8;\n\n // If user has removed value...\n if ((e.keyCode === backKey || e.keyCode === deleteKey) && !e.target.value) {\n // ...and it is a multiple select input, activate choices (if searching)\n if (!this.isTextElement && this.isSearching) {\n this.isSearching = false;\n this.store.dispatch(\n activateChoices(true)\n );\n }\n } else if (this.canSearch && canAddItem.response) {\n this._handleSearch(this.input.value);\n }\n }\n // Re-establish canSearch value from changes in _onKeyDown\n this.canSearch = this.config.searchEnabled;\n }\n\n /**\n * Input event\n * @return\n * @private\n */\n _onInput() {\n if (!this.isSelectOneElement) {\n this._setInputWidth();\n }\n }\n\n /**\n * Touch move event\n * @return\n * @private\n */\n _onTouchMove() {\n if (this.wasTap === true) {\n this.wasTap = false;\n }\n }\n\n /**\n * Touch end event\n * @param {Object} e Event\n * @return\n * @private\n */\n _onTouchEnd(e) {\n const target = e.target || e.touches[0].target;\n const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState);\n\n // If a user tapped within our container...\n if (this.wasTap === true && this.containerOuter.contains(target)) {\n // ...and we aren't dealing with a single select box, show dropdown/focus input\n if ((target === this.containerOuter || target === this.containerInner) && !this.isSelectOneElement) {\n if (this.isTextElement) {\n // If text element, we only want to focus the input (if it isn't already)\n if (document.activeElement !== this.input) {\n this.input.focus();\n }\n } else {\n if (!hasActiveDropdown) {\n // If a select box, we want to show the dropdown\n this.showDropdown(true);\n }\n }\n }\n // Prevents focus event firing\n e.stopPropagation();\n }\n\n this.wasTap = true;\n }\n\n /**\n * Mouse down event\n * @param {Object} e Event\n * @return\n * @private\n */\n _onMouseDown(e) {\n const target = e.target;\n\n // If we have our mouse down on the scrollbar and are on IE11...\n if (target === this.choiceList && this.isIe11) {\n this.isScrollingOnIe = true;\n }\n\n if (this.containerOuter.contains(target) && target !== this.input) {\n let foundTarget;\n const activeItems = this.store.getItemsFilteredByActive();\n const hasShiftKey = e.shiftKey;\n\n if (foundTarget = findAncestorByAttrName(target, 'data-button')) {\n this._handleButtonAction(activeItems, foundTarget);\n } else if (foundTarget = findAncestorByAttrName(target, 'data-item')) {\n this._handleItemAction(activeItems, foundTarget, hasShiftKey);\n } else if (foundTarget = findAncestorByAttrName(target, 'data-choice')) {\n this._handleChoiceAction(activeItems, foundTarget);\n }\n\n e.preventDefault();\n }\n }\n\n /**\n * Click event\n * @param {Object} e Event\n * @return\n * @private\n */\n _onClick(e) {\n const target = e.target;\n const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState);\n const activeItems = this.store.getItemsFilteredByActive();\n\n // If target is something that concerns us\n if (this.containerOuter.contains(target)) {\n // Handle button delete\n if (target.hasAttribute('data-button')) {\n this._handleButtonAction(activeItems, target);\n }\n\n if (!hasActiveDropdown) {\n if (this.isTextElement) {\n if (document.activeElement !== this.input) {\n this.input.focus();\n }\n } else {\n if (this.canSearch) {\n this.showDropdown(true);\n } else {\n this.showDropdown();\n this.containerOuter.focus();\n }\n }\n } else if (this.isSelectOneElement && target !== this.input && !this.dropdown.contains(target)) {\n this.hideDropdown(true);\n }\n } else {\n const hasHighlightedItems = activeItems.some(item => item.highlighted);\n\n // De-select any highlighted items\n if (hasHighlightedItems) {\n this.unhighlightAll();\n }\n\n // Remove focus state\n this.containerOuter.classList.remove(this.config.classNames.focusState);\n\n // Close all other dropdowns\n if (hasActiveDropdown) {\n this.hideDropdown();\n }\n }\n }\n\n /**\n * Mouse over (hover) event\n * @param {Object} e Event\n * @return\n * @private\n */\n _onMouseOver(e) {\n // If the dropdown is either the target or one of its children is the target\n if (e.target === this.dropdown || this.dropdown.contains(e.target)) {\n if (e.target.hasAttribute('data-choice')) this._highlightChoice(e.target);\n }\n }\n\n /**\n * Paste event\n * @param {Object} e Event\n * @return\n * @private\n */\n _onPaste(e) {\n // Disable pasting into the input if option has been set\n if (e.target === this.input && !this.config.paste) {\n e.preventDefault();\n }\n }\n\n /**\n * Focus event\n * @param {Object} e Event\n * @return\n * @private\n */\n _onFocus(e) {\n const target = e.target;\n // If target is something that concerns us\n if (this.containerOuter.contains(target)) {\n const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState);\n const focusActions = {\n text: () => {\n if (target === this.input) {\n this.containerOuter.classList.add(this.config.classNames.focusState);\n }\n },\n 'select-one': () => {\n this.containerOuter.classList.add(this.config.classNames.focusState);\n if (target === this.input) {\n // Show dropdown if it isn't already showing\n if (!hasActiveDropdown) {\n this.showDropdown();\n }\n }\n },\n 'select-multiple': () => {\n if (target === this.input) {\n // If element is a select box, the focused element is the container and the dropdown\n // isn't already open, focus and show dropdown\n this.containerOuter.classList.add(this.config.classNames.focusState);\n\n if (!hasActiveDropdown) {\n this.showDropdown(true);\n }\n }\n },\n };\n\n focusActions[this.passedElement.type]();\n }\n }\n\n /**\n * Blur event\n * @param {Object} e Event\n * @return\n * @private\n */\n _onBlur(e) {\n const target = e.target;\n // If target is something that concerns us\n if (this.containerOuter.contains(target) && !this.isScrollingOnIe) {\n const activeItems = this.store.getItemsFilteredByActive();\n const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState);\n const hasHighlightedItems = activeItems.some(item => item.highlighted);\n const blurActions = {\n text: () => {\n if (target === this.input) {\n // Remove the focus state\n this.containerOuter.classList.remove(this.config.classNames.focusState);\n // De-select any highlighted items\n if (hasHighlightedItems) {\n this.unhighlightAll();\n }\n // Hide dropdown if it is showing\n if (hasActiveDropdown) {\n this.hideDropdown();\n }\n }\n },\n 'select-one': () => {\n this.containerOuter.classList.remove(this.config.classNames.focusState);\n if (target === this.containerOuter) {\n // Hide dropdown if it is showing\n if (hasActiveDropdown && !this.canSearch) {\n this.hideDropdown();\n }\n }\n if (target === this.input && hasActiveDropdown) {\n // Hide dropdown if it is showing\n this.hideDropdown();\n }\n },\n 'select-multiple': () => {\n if (target === this.input) {\n // Remove the focus state\n this.containerOuter.classList.remove(this.config.classNames.focusState);\n // Hide dropdown if it is showing\n if (hasActiveDropdown) {\n this.hideDropdown();\n }\n // De-select any highlighted items\n if (hasHighlightedItems) {\n this.unhighlightAll();\n }\n }\n },\n };\n\n blurActions[this.passedElement.type]();\n } else {\n // On IE11, clicking the scollbar blurs our input and thus\n // closes the dropdown. To stop this, we refocus our input\n // if we know we are on IE *and* are scrolling.\n this.isScrollingOnIe = false;\n this.input.focus();\n }\n }\n\n /**\n * Tests value against a regular expression\n * @param {string} value Value to test\n * @return {Boolean} Whether test passed/failed\n * @private\n */\n _regexFilter(value) {\n if (!value) {\n return false;\n }\n\n const regex = this.config.regexFilter;\n const expression = new RegExp(regex.source, 'i');\n return expression.test(value);\n }\n\n /**\n * Scroll to an option element\n * @param {HTMLElement} choice Option to scroll to\n * @param {Number} direction Whether option is above or below\n * @return\n * @private\n */\n _scrollToChoice(choice, direction) {\n if (!choice) {\n return;\n }\n\n const dropdownHeight = this.choiceList.offsetHeight;\n const choiceHeight = choice.offsetHeight;\n // Distance from bottom of element to top of parent\n const choicePos = choice.offsetTop + choiceHeight;\n // Scroll position of dropdown\n const containerScrollPos = this.choiceList.scrollTop + dropdownHeight;\n // Difference between the choice and scroll position\n const endPoint = direction > 0 ? ((this.choiceList.scrollTop + choicePos) - containerScrollPos) : choice.offsetTop;\n\n const animateScroll = () => {\n const strength = 4;\n const choiceListScrollTop = this.choiceList.scrollTop;\n let continueAnimation = false;\n let easing;\n let distance;\n\n if (direction > 0) {\n easing = (endPoint - choiceListScrollTop) / strength;\n distance = easing > 1 ? easing : 1;\n\n this.choiceList.scrollTop = choiceListScrollTop + distance;\n if (choiceListScrollTop < endPoint) {\n continueAnimation = true;\n }\n } else {\n easing = (choiceListScrollTop - endPoint) / strength;\n distance = easing > 1 ? easing : 1;\n\n this.choiceList.scrollTop = choiceListScrollTop - distance;\n if (choiceListScrollTop > endPoint) {\n continueAnimation = true;\n }\n }\n\n if (continueAnimation) {\n requestAnimationFrame((time) => {\n animateScroll(time, endPoint, direction);\n });\n }\n };\n\n requestAnimationFrame((time) => {\n animateScroll(time, endPoint, direction);\n });\n }\n\n /**\n * Highlight choice\n * @param {HTMLElement} [el] Element to highlight\n * @return\n * @private\n */\n _highlightChoice(el = null) {\n // Highlight first element in dropdown\n const choices = Array.from(this.dropdown.querySelectorAll('[data-choice-selectable]'));\n let passedEl = el;\n\n if (choices && choices.length) {\n const highlightedChoices = Array.from(this.dropdown.querySelectorAll(`.${this.config.classNames.highlightedState}`));\n\n // Remove any highlighted choices\n highlightedChoices.forEach((choice) => {\n choice.classList.remove(this.config.classNames.highlightedState);\n choice.setAttribute('aria-selected', 'false');\n });\n\n if (passedEl) {\n this.highlightPosition = choices.indexOf(passedEl);\n } else {\n // Highlight choice based on last known highlight location\n if (choices.length > this.highlightPosition) {\n // If we have an option to highlight\n passedEl = choices[this.highlightPosition];\n } else {\n // Otherwise highlight the option before\n passedEl = choices[choices.length - 1];\n }\n\n if (!passedEl) {\n passedEl = choices[0];\n }\n }\n\n // Highlight given option, and set accessiblity attributes\n passedEl.classList.add(this.config.classNames.highlightedState);\n passedEl.setAttribute('aria-selected', 'true');\n this.containerOuter.setAttribute('aria-activedescendant', passedEl.id);\n }\n }\n\n /**\n * Add item to store with correct value\n * @param {String} value Value to add to store\n * @param {String} [label] Label to add to store\n * @param {Number} [choiceId=-1] ID of the associated choice that was selected\n * @param {Number} [groupId=-1] ID of group choice is within. Negative number indicates no group\n * @param {Object} [customProperties] Object containing user defined properties\n * @return {Object} Class instance\n * @public\n */\n _addItem(value, label = null, choiceId = -1, groupId = -1, customProperties = null, placeholder = false, keyCode = null) {\n let passedValue = isType('String', value) ? value.trim() : value;\n let passedKeyCode = keyCode;\n const items = this.store.getItems();\n const passedLabel = label || passedValue;\n const passedOptionId = parseInt(choiceId, 10) || -1;\n\n // Get group if group ID passed\n const group = groupId >= 0 ? this.store.getGroupById(groupId) : null;\n\n // Generate unique id\n const id = items ? items.length + 1 : 1;\n\n // If a prepended value has been passed, prepend it\n if (this.config.prependValue) {\n passedValue = this.config.prependValue + passedValue.toString();\n }\n\n // If an appended value has been passed, append it\n if (this.config.appendValue) {\n passedValue += this.config.appendValue.toString();\n }\n\n this.store.dispatch(\n addItem(\n passedValue,\n passedLabel,\n id,\n passedOptionId,\n groupId,\n customProperties,\n placeholder,\n passedKeyCode\n )\n );\n\n if (this.isSelectOneElement) {\n this.removeActiveItems(id);\n }\n\n // Trigger change event\n if (group && group.value) {\n triggerEvent(this.passedElement, 'addItem', {\n id,\n value: passedValue,\n label: passedLabel,\n groupValue: group.value,\n keyCode: passedKeyCode\n });\n } else {\n triggerEvent(this.passedElement, 'addItem', {\n id,\n value: passedValue,\n label: passedLabel,\n keyCode: passedKeyCode\n });\n }\n\n return this;\n }\n\n /**\n * Remove item from store\n * @param {Object} item Item to remove\n * @return {Object} Class instance\n * @public\n */\n _removeItem(item) {\n if (!item || !isType('Object', item)) {\n return this;\n }\n\n const id = item.id;\n const value = item.value;\n const label = item.label;\n const choiceId = item.choiceId;\n const groupId = item.groupId;\n const group = groupId >= 0 ? this.store.getGroupById(groupId) : null;\n\n this.store.dispatch(\n removeItem(id, choiceId)\n );\n\n if (group && group.value) {\n triggerEvent(this.passedElement, 'removeItem', {\n id,\n value,\n label,\n groupValue: group.value,\n });\n } else {\n triggerEvent(this.passedElement, 'removeItem', {\n id,\n value,\n label,\n });\n }\n\n return this;\n }\n\n /**\n * Add choice to dropdown\n * @param {String} value Value of choice\n * @param {String} [label] Label of choice\n * @param {Boolean} [isSelected=false] Whether choice is selected\n * @param {Boolean} [isDisabled=false] Whether choice is disabled\n * @param {Number} [groupId=-1] ID of group choice is within. Negative number indicates no group\n * @param {Object} [customProperties] Object containing user defined properties\n * @return\n * @private\n */\n _addChoice(value, label = null, isSelected = false, isDisabled = false, groupId = -1, customProperties = null, placeholder = false, keyCode = null) {\n if (typeof value === 'undefined' || value === null) {\n return;\n }\n\n // Generate unique id\n const choices = this.store.getChoices();\n const choiceLabel = label || value;\n const choiceId = choices ? choices.length + 1 : 1;\n const choiceElementId = `${this.baseId}-${this.idNames.itemChoice}-${choiceId}`;\n\n this.store.dispatch(\n addChoice(\n value,\n choiceLabel,\n choiceId,\n groupId,\n isDisabled,\n choiceElementId,\n customProperties,\n placeholder,\n keyCode\n )\n );\n\n if (isSelected) {\n this._addItem(\n value,\n choiceLabel,\n choiceId,\n undefined,\n customProperties,\n placeholder,\n keyCode\n );\n }\n }\n\n /**\n * Clear all choices added to the store.\n * @return\n * @private\n */\n _clearChoices() {\n this.store.dispatch(\n clearChoices()\n );\n }\n\n /**\n * Add group to dropdown\n * @param {Object} group Group to add\n * @param {Number} id Group ID\n * @param {String} [valueKey] name of the value property on the object\n * @param {String} [labelKey] name of the label property on the object\n * @return\n * @private\n */\n _addGroup(group, id, valueKey = 'value', labelKey = 'label') {\n const groupChoices = isType('Object', group) ? group.choices : Array.from(group.getElementsByTagName('OPTION'));\n const groupId = id ? id : Math.floor(new Date().valueOf() * Math.random());\n const isDisabled = group.disabled ? group.disabled : false;\n\n if (groupChoices) {\n this.store.dispatch(\n addGroup(\n group.label,\n groupId,\n true,\n isDisabled\n )\n );\n\n groupChoices.forEach((option) => {\n const isOptDisabled = option.disabled || (option.parentNode && option.parentNode.disabled);\n this._addChoice(\n option[valueKey],\n (isType('Object', option)) ? option[labelKey] : option.innerHTML,\n option.selected,\n isOptDisabled,\n groupId,\n option.customProperties,\n option.placeholder\n );\n });\n } else {\n this.store.dispatch(\n addGroup(\n group.label,\n group.id,\n false,\n group.disabled\n )\n );\n }\n }\n\n /**\n * Get template from name\n * @param {String} template Name of template to get\n * @param {...} args Data to pass to template\n * @return {HTMLElement} Template\n * @private\n */\n _getTemplate(template, ...args) {\n if (!template) {\n return null;\n }\n const templates = this.config.templates;\n return templates[template](...args);\n }\n\n /**\n * Create HTML element based on type and arguments\n * @return\n * @private\n */\n _createTemplates() {\n const globalClasses = this.config.classNames;\n const templates = {\n containerOuter: (direction) => {\n return strToEl(`\n