{"version":3,"file":"choices.js","sources":["webpack:///webpack/universalModuleDefinition","webpack:///webpack/bootstrap 811bac5aece6d6cf1850","webpack:///assets/scripts/src/choices.js","webpack:///./~/fuse.js/src/fuse.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/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 811bac5aece6d6cf1850","import Fuse from 'fuse.js';\nimport Store from './store/index.js';\nimport {\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 extend,\n getWidthOfInput,\n sortByAlpha,\n sortByScore,\n triggerEvent,\n}\nfrom './lib/utils.js';\nimport './lib/polyfills.js';\n\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 items: [],\n choices: [],\n maxItemCount: -1,\n addItems: true,\n removeItems: true,\n removeItemButton: false,\n editItems: false,\n duplicateItems: true,\n delimiter: ',',\n paste: true,\n search: true,\n searchFloor: 1,\n flip: true,\n resetScrollPosition: true,\n regexFilter: null,\n shouldSort: true,\n sortFilter: sortByAlpha,\n sortFields: ['label', 'value'],\n placeholder: true,\n placeholderValue: null,\n prependValue: null,\n appendValue: null,\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 \"${value}\"`;\n },\n maxItemText: (maxItemCount) => {\n return `Only ${maxItemCount} values can be added.`;\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 },\n fuseOptions: {\n include: 'score',\n },\n callbackOnInit: null,\n callbackOnCreateTemplates: null,\n };\n\n // Merge options with user options\n this.config = extend(defaultConfig, userConfig);\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 this.isSelectElement = this.passedElement.type === 'select-one' || this.passedElement.type === 'select-multiple';\n this.isTextElement = this.passedElement.type === 'text';\n\n if (!this.passedElement) {\n console.error('Passed element not found');\n return;\n }\n\n this.highlightPosition = 0;\n this.canSearch = this.config.search;\n\n // Assing 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(this.passedElement.value.split(this.config.delimiter));\n }\n\n // Bind methods\n this.init = this.init.bind(this);\n this.render = this.render.bind(this);\n this.destroy = this.destroy.bind(this);\n this.disable = this.disable.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) console.error('Choices: Your browser doesn\\'t support Choices');\n\n // Input type check\n const isValidType = ['select-one', 'select-multiple', 'text'].some(type => type === this.passedElement.type);\n const canInit = isElement(this.passedElement) && isValidType;\n\n if (canInit) {\n // If element has already been initalised with Choices\n if (this.passedElement.getAttribute('data-choice') === 'active') return;\n\n // Let's go\n this.init();\n } else {\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) return;\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) return;\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 this.passedElement.removeAttribute('style', 'display:none;');\n this.passedElement.removeAttribute('aria-hidden');\n this.passedElement.removeAttribute('data-choice', 'active');\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.passedElement.type === 'select-one') {\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);\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) {\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 filter = this.isSearching ? sortByScore : this.config.sortFilter;\n\n // If sorting is enabled or the user is searching, filter choices\n if (this.config.shouldSort || this.isSearching) {\n choices.sort(filter);\n }\n\n choices.forEach((choice) => {\n const dropdownItem = this._getTemplate('choice', choice);\n const shouldRender = this.passedElement.type === 'select-one' || !choice.selected;\n if (shouldRender) {\n choicesFragment.appendChild(dropdownItem);\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 Fragrment to add items to (optional)\n * @return\n * @private\n */\n renderItems(items, fragment) {\n // Create fragment to add elements to\n const itemListFragment = fragment || document.createDocumentFragment();\n // Simplify store data to just values\n const itemsFiltered = this.store.getItemsReducedToValues(items);\n\n if (this.isTextElement) {\n // Assign hidden input array of values\n this.passedElement.setAttribute('value', itemsFiltered.join(this.config.delimiter));\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 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 (this.currentState.choices !== this.prevState.choices ||\n this.currentState.groups !== this.prevState.groups) {\n if (this.passedElement.type === 'select-multiple' ||\n this.passedElement.type === 'select-one') {\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 if (choiceListFragment.childNodes && choiceListFragment.childNodes.length > 0) {\n // If we actually have anything to add to our dropdown\n // append it and highlight the first choice\n this.choiceList.appendChild(choiceListFragment);\n this._highlightChoice();\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) ? this.config.noResultsText() : this.config.noResultsText;\n dropdownItem = this._getTemplate('notice', notice);\n } else {\n notice = isType('Function', this.config.noChoicesText) ? this.config.noChoicesText() : this.config.noChoicesText;\n dropdownItem = this._getTemplate('notice', notice);\n }\n\n this.choiceList.appendChild(dropdownItem);\n }\n }\n }\n\n // Items\n if (this.currentState.items !== this.prevState.items) {\n const activeItems = this.store.getItemsFilteredByActive();\n if (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 // Clear list\n this.itemList.innerHTML = '';\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 * @return {Object} Class instance\n * @public\n */\n highlightItem(item, runEvent = true) {\n if (!item) return;\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(highlightItem(id, true));\n\n if (runEvent) {\n if(group && group.value) {\n triggerEvent(this.passedElement, 'highlightItem', {\n id,\n value: item.value,\n groupValue: group.value\n });\n } else {\n triggerEvent(this.passedElement, 'highlightItem', {\n id,\n value: item.value,\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) return;\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(highlightItem(id, false));\n\n if(group && group.value) {\n triggerEvent(this.passedElement, 'unhighlightItem', {\n id,\n value: item.value,\n groupValue: group.value\n });\n } else {\n triggerEvent(this.passedElement, 'unhighlightItem', {\n id,\n value: item.value,\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 console.error('removeItemsByValue: No value was passed to be removed');\n return;\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(body.scrollHeight, body.offsetHeight, html.clientHeight,\n html.scrollHeight, html.offsetHeight);\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\n const dimensions = this.dropdown.getBoundingClientRect();\n const dropdownPos = Math.ceil(dimensions.top + window.scrollY + dimensions.height);\n // If flip is enabled and the dropdown bottom position is greater than the window height flip the dropdown.\n const shouldFlip = this.config.flip ? dropdownPos >= winHeight : false;\n\n if (shouldFlip) {\n this.containerOuter.classList.add(this.config.classNames.flippedState);\n } else {\n this.containerOuter.classList.remove(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 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\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 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.passedElement.type === 'select-one') {\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/String} args Array of value objects or value strings/Single value string\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 passedElementType = this.passedElement.type,\n handleValue = (item) => {\n const itemType = getType(item);\n if (itemType === 'Object') {\n if (!item.value) return;\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 (passedElementType !== 'text') {\n this._addChoice(true, false, item.value, item.label, -1);\n } else {\n this._addItem(item.value, item.label, item.id);\n }\n } else if (itemType === 'String') {\n if (passedElementType !== 'text') {\n this._addChoice(true, false, item, item, -1);\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.passedElement.type !== 'text') {\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 choice.value === val;\n });\n\n if (foundChoice) {\n if (!foundChoice.selected) {\n this._addItem(foundChoice.value, foundChoice.label, foundChoice.id, foundChoice.groupId);\n } else {\n console.warn('Attempting to select choice already selected');\n }\n } else {\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) return;\n // Clear choices if needed\n if(replaceChoices) {\n this._clearChoices();\n }\n // Add choices if passed\n if (choices && choices.length) {\n this.containerOuter.classList.remove(this.config.classNames.loadingState);\n choices.forEach((result, index) => {\n const isSelected = result.selected ? result.selected : false;\n const isDisabled = result.disabled ? result.disabled : false;\n if (result.choices) {\n this._addGroup(result, (result.id || null), value, label);\n } else {\n this._addChoice(isSelected, isDisabled, result[value], result[label]);\n }\n });\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(clearAll());\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) this.input.value = '';\n if (this.passedElement.type !== 'select-one') {\n this._setInputWidth();\n }\n if (this.passedElement.type !== 'text' && this.config.search) {\n this.isSearching = false;\n this.store.dispatch(activateChoices(true));\n }\n return this;\n }\n\n /**\n * Enable interaction with Choices\n * @return {Object} Class instance\n */\n enable() {\n this.passedElement.disabled = false;\n const isDisabled = this.containerOuter.classList.contains(this.config.classNames.disabledState);\n if (this.initialised && 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 }\n return this;\n }\n\n /**\n * Disable interaction with Choices\n * @return {Object} Class instance\n * @public\n */\n disable() {\n this.passedElement.disabled = true;\n const isEnabled = !this.containerOuter.classList.contains(this.config.classNames.disabledState);\n if (this.initialised && 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 }\n return this;\n }\n\n /**\n * Populate options via ajax callback\n * @param {Function} fn Passed\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 this._handleLoadingState(true);\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) return;\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) return;\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.passedElement.type === 'select-one') {\n const placeholder = this.config.placeholder ? this.config.placeholderValue || this.passedElement.getAttribute('placeholder') :\n false;\n if (placeholder) {\n const placeholderItem = this._getTemplate('placeholder', placeholder);\n this.itemList.appendChild(placeholderItem);\n }\n }\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) return;\n\n // If we are clicking on an item\n if (this.config.removeItems && this.passedElement.type !== 'select-one') {\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) this.input.focus();\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 {[type]} [description]\n */\n _handleChoiceAction(activeItems, element) {\n if (!activeItems || !element) return;\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 hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState);\n\n if (choice && !choice.selected && !choice.disabled) {\n const canAddItem = this._canAddItem(activeItems, choice.value);\n\n if (canAddItem.response) {\n this._addItem(choice.value, choice.label, choice.id, choice.groupId);\n this._triggerChange(choice.value);\n }\n }\n\n this.clearInput(this.passedElement);\n\n // We wont to close the dropdown if we are dealing with a single select box\n if (hasActiveDropdown && this.passedElement.type === 'select-one') {\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 === true);\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) ? this.config.addItemText(value) : this.config.addItemText;\n\n if (this.passedElement.type === 'select-multiple' || this.passedElement.type === 'text') {\n if (this.config.maxItemCount > 0 && this.config.maxItemCount <= this.itemList.children.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) ? this.config.maxItemText(this.config.maxItemCount) : this.config.maxItemText;\n }\n }\n\n if (this.passedElement.type === 'text' && this.config.addItems) {\n const isUnique = !activeItems.some((item) => item.value === value.trim());\n\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 // If no duplicates are allowed, and the value already exists\n // in the array\n if (this.config.duplicateItems === false && !isUnique) {\n canAddItem = false;\n notice = isType('Function', this.config.uniqueItemText) ? this.config.uniqueItemText(value) : this.config.uniqueItemText;\n }\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} isLoading default value set to 'true'.\n * @return\n * @private\n */\n _handleLoadingState(isLoading = true) {\n let placeholderItem = this.itemList.querySelector(`.${this.config.classNames.placeholder}`);\n if(isLoading) {\n this.containerOuter.classList.add(this.config.classNames.loadingState);\n this.containerOuter.setAttribute('aria-busy', 'true');\n if (this.passedElement.type === 'select-one') {\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 const placeholder = this.config.placeholder ? this.config.placeholderValue || this.passedElement.getAttribute('placeholder') : false;\n if (this.passedElement.type === 'select-one') {\n placeholderItem.innerHTML = placeholder || '';\n } else {\n this.input.placeholder = 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) return;\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 parsedResults.forEach((result, index) => {\n const isSelected = result.selected ? result.selected : false;\n const isDisabled = result.disabled ? result.disabled : false;\n if (result.choices) {\n this._addGroup(result, (result.id || null), value, label);\n } else {\n this._addChoice(isSelected, isDisabled, result[value], result[label]);\n }\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.getChoicesFilteredBySelectable();\n const needle = newValue;\n const keys = isType('Array', this.config.sortFields) ? this.config.sortFields : [this.config.sortFields];\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(filterChoices(results));\n }\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) return;\n const choices = this.store.getChoices();\n const hasUnactiveChoices = choices.some((option) => option.active !== true);\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 // Filter available choices\n this._searchChoices(value);\n // Trigger search event\n triggerEvent(this.passedElement, 'search', {\n value,\n });\n } else if (hasUnactiveChoices) {\n // Otherwise reset choices to active\n this.isSearching = false;\n this.store.dispatch(activateChoices(true));\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.passedElement.type && this.passedElement.type === 'select-one') {\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.passedElement.type && this.passedElement.type === 'select-one') {\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.config.placeholder && (this.config.placeholderValue || this.passedElement.getAttribute('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 const placeholder = this.config.placeholder ? this.config.placeholderValue ||\n this.passedElement.getAttribute('placeholder') : false;\n if (this.input.value && this.input.value.length >= (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)) return;\n\n const target = e.target;\n const passedElementType = this.passedElement.type;\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 (passedElementType !== 'text' && /[a-zA-Z0-9-_ ]/.test(keyString) && !hasActiveDropdown) {\n this.showDropdown(true);\n }\n\n this.canSearch = this.config.search;\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(this.itemList.children);\n }\n }\n };\n\n const onEnterKey = () => {\n // If enter key is pressed and the input has a value\n if (passedElementType === 'text' && 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(this.passedElement);\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 this._handleChoiceAction(activeItems, highlighted);\n }\n\n } else if (passedElementType === 'select-one') {\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 }\n };\n\n const onDirectionKey = () => {\n // If up or down key is pressed, traverse through options\n if (hasActiveDropdown || passedElementType === 'select-one') {\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 && passedElementType !== 'select-one') {\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) return;\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 const value = this.input.value;\n\n if (value) {\n const activeItems = this.store.getItemsFilteredByActive();\n const canAddItem = this._canAddItem(activeItems, 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.passedElement.type !== 'text' && this.isSearching) {\n this.isSearching = false;\n this.store.dispatch(activateChoices(true));\n }\n } else if (this.canSearch) {\n this._handleSearch(this.input.value);\n }\n }\n }\n\n /**\n * Input event\n * @param {Object} e Event\n * @return\n * @private\n */\n _onInput() {\n if (this.passedElement.type !== 'select-one') {\n this._setInputWidth();\n }\n }\n\n /**\n * Touch move event\n * @param {Object} e 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.passedElement.type !== 'select-one') {\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 if (this.containerOuter.contains(target) && target !== this.input) {\n const activeItems = this.store.getItemsFilteredByActive();\n const hasShiftKey = e.shiftKey;\n\n if (target.hasAttribute('data-item')) {\n this._handleItemAction(activeItems, target, hasShiftKey);\n } else if (target.hasAttribute('data-choice')) {\n this._handleChoiceAction(activeItems, target);\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.passedElement.type === 'select-one' && target !== this.input && !this.dropdown.contains(target)) {\n this.hideDropdown(true);\n }\n } else {\n const hasHighlightedItems = activeItems.some((item) => item.highlighted === true);\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 focussed 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)) {\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 === true);\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\n if (target === this.input) {\n // Hide dropdown if it is showing\n if (hasActiveDropdown) {\n this.hideDropdown();\n }\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 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 }\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) return;\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} option 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) return;\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) {\n // Highlight first element in dropdown\n const choices = Array.from(this.dropdown.querySelectorAll('[data-choice-selectable]'));\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 (el) {\n // Highlight given option\n el.classList.add(this.config.classNames.highlightedState);\n this.highlightPosition = choices.indexOf(el);\n } else {\n // Highlight choice based on last known highlight location\n let choice;\n\n if (choices.length > this.highlightPosition) {\n // If we have an option to highlight\n choice = choices[this.highlightPosition];\n } else {\n // Otherwise highlight the option before\n choice = choices[choices.length - 1];\n }\n\n if (!choice) choice = choices[0];\n choice.classList.add(this.config.classNames.highlightedState);\n choice.setAttribute('aria-selected', 'true');\n }\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 * @return {Object} Class instance\n * @public\n */\n _addItem(value, label, choiceId = -1, groupId = -1) {\n let passedValue = isType('String', value) ? value.trim() : value;\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(addItem(passedValue, passedLabel, id, passedOptionId, groupId));\n\n if (this.passedElement.type === 'select-one') {\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 groupValue: group.value,\n });\n } else {\n triggerEvent(this.passedElement, 'addItem', {\n id,\n value: passedValue,\n });\n }\n\n return this;\n }\n\n /**\n * Remove item from store\n * @param {Object} item Item to remove\n * @param {Function} callback Callback to trigger\n * @return {Object} Class instance\n * @public\n */\n _removeItem(item) {\n if (!item || !isType('Object', item)) {\n console.error('removeItem: No item object was passed to be removed');\n return;\n }\n\n const id = item.id;\n const value = item.value;\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(removeItem(id, choiceId));\n\n if(group && group.value) {\n triggerEvent(this.passedElement, 'removeItem', {\n id,\n value,\n groupValue: group.value,\n });\n } else {\n triggerEvent(this.passedElement, 'removeItem', {\n id,\n value,\n });\n }\n\n return this;\n }\n\n /**\n * Add choice to dropdown\n * @param {Boolean} isSelected Whether choice is selected\n * @param {Boolean} isDisabled Whether choice is disabled\n * @param {String} value Value of choice\n * @param {String} Label Label of choice\n * @param {Number} groupId ID of group choice is within. Negative number indicates no group\n * @return\n * @private\n */\n _addChoice(isSelected, isDisabled, value, label, groupId = -1) {\n if (typeof value === 'undefined' || value === null) return;\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\n this.store.dispatch(addChoice(value, choiceLabel, choiceId, groupId, isDisabled));\n\n if (isSelected) {\n this._addItem(value, choiceLabel, choiceId);\n }\n }\n\n /**\n * Clear all choices added to the store.\n * @return\n * @private\n */\n _clearChoices() {\n this.store.dispatch(clearChoices());\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(addGroup(group.label, groupId, true, isDisabled));\n\n groupChoices.forEach((option) => {\n const isOptDisabled = (option.disabled || (option.parentNode && option.parentNode.disabled)) || false;\n const isOptSelected = option.selected ? option.selected : false;\n let label;\n\n if (isType('Object', option)) {\n label = option[labelKey] || option[valueKey];\n } else {\n label = option.innerHTML;\n }\n\n this._addChoice(isOptSelected, isOptDisabled, option[valueKey], label, groupId);\n });\n } else {\n this.store.dispatch(addGroup(group.label, group.id, false, group.disabled));\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) return;\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 classNames = this.config.classNames;\n const templates = {\n containerOuter: (direction) => {\n return strToEl(`\n
\n `);\n },\n containerInner: () => {\n return strToEl(`\n \n `);\n },\n itemList: () => {\n return strToEl(`\n \n `);\n },\n placeholder: (value) => {\n return strToEl(`\n