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