UNPKG

choices.js

Version:

A vanilla JS customisable text input/select box plugin

1,525 lines (1,263 loc) 199 kB
/*! choices.js v3.0.4 | (c) 2018 Josh Johnson | https://github.com/jshjohnson/Choices#readme */ (function webpackUniversalModuleDefinition(root, factory) { if(typeof exports === 'object' && typeof module === 'object') module.exports = factory(); else if(typeof define === 'function' && define.amd) define([], factory); else if(typeof exports === 'object') exports["Choices"] = factory(); else root["Choices"] = factory(); })(this, function() { return /******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ // Check if module is in cache /******/ if(installedModules[moduleId]) /******/ return installedModules[moduleId].exports; /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { /******/ exports: {}, /******/ id: moduleId, /******/ loaded: false /******/ }; /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ // Flag the module as loaded /******/ module.loaded = true; /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = modules; /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; /******/ // __webpack_public_path__ /******/ __webpack_require__.p = "/assets/scripts/dist/"; /******/ // Load entry module and return exports /******/ return __webpack_require__(0); /******/ }) /************************************************************************/ /******/ ([ /* 0 */ /***/ (function(module, exports, __webpack_require__) { module.exports = __webpack_require__(1); /***/ }), /* 1 */ /***/ (function(module, exports, __webpack_require__) { 'use strict'; var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); var _fuse = __webpack_require__(2); var _fuse2 = _interopRequireDefault(_fuse); var _classnames = __webpack_require__(3); var _classnames2 = _interopRequireDefault(_classnames); var _index = __webpack_require__(4); var _index2 = _interopRequireDefault(_index); var _index3 = __webpack_require__(31); var _utils = __webpack_require__(32); __webpack_require__(33); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; } function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } /** * Choices */ var Choices = function () { function Choices() { var element = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : '[data-choice]'; var userConfig = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {}; _classCallCheck(this, Choices); // If there are multiple elements, create a new instance // for each element besides the first one (as that already has an instance) if ((0, _utils.isType)('String', element)) { var elements = document.querySelectorAll(element); if (elements.length > 1) { for (var i = 1; i < elements.length; i++) { var el = elements[i]; new Choices(el, userConfig); } } } var defaultConfig = { silent: false, items: [], choices: [], renderChoiceLimit: -1, maxItemCount: -1, addItems: true, removeItems: true, removeItemButton: false, editItems: false, duplicateItems: true, delimiter: ',', paste: true, searchEnabled: true, searchChoices: true, searchFloor: 1, searchResultLimit: 4, searchFields: ['label', 'value'], position: 'auto', resetScrollPosition: true, regexFilter: null, shouldSort: true, shouldSortItems: false, sortFilter: _utils.sortByAlpha, placeholder: true, placeholderValue: null, searchPlaceholderValue: null, prependValue: null, appendValue: null, renderSelectedChoices: 'auto', loadingText: 'Loading...', noResultsText: 'No results found', noChoicesText: 'No choices to choose from', itemSelectText: 'Press to select', addItemText: function addItemText(value) { return 'Press Enter to add <b>"' + (0, _utils.stripHTML)(value) + '"</b>'; }, maxItemText: function maxItemText(maxItemCount) { return 'Only ' + maxItemCount + ' values can be added.'; }, itemComparer: function itemComparer(choice, item) { return choice === item; }, uniqueItemText: 'Only unique values can be added.', classNames: { containerOuter: 'choices', containerInner: 'choices__inner', input: 'choices__input', inputCloned: 'choices__input--cloned', list: 'choices__list', listItems: 'choices__list--multiple', listSingle: 'choices__list--single', listDropdown: 'choices__list--dropdown', item: 'choices__item', itemSelectable: 'choices__item--selectable', itemDisabled: 'choices__item--disabled', itemChoice: 'choices__item--choice', placeholder: 'choices__placeholder', group: 'choices__group', groupHeading: 'choices__heading', button: 'choices__button', activeState: 'is-active', focusState: 'is-focused', openState: 'is-open', disabledState: 'is-disabled', highlightedState: 'is-highlighted', hiddenState: 'is-hidden', flippedState: 'is-flipped', loadingState: 'is-loading', noResults: 'has-no-results', noChoices: 'has-no-choices' }, fuseOptions: { include: 'score' }, callbackOnInit: null, callbackOnCreateTemplates: null }; this.idNames = { itemChoice: 'item-choice' }; // Merge options with user options this.config = (0, _utils.extend)(defaultConfig, userConfig); if (this.config.renderSelectedChoices !== 'auto' && this.config.renderSelectedChoices !== 'always') { if (!this.config.silent) { console.warn('renderSelectedChoices: Possible values are \'auto\' and \'always\'. Falling back to \'auto\'.'); } this.config.renderSelectedChoices = 'auto'; } // Create data store this.store = new _index2.default(this.render); // State tracking this.initialised = false; this.currentState = {}; this.prevState = {}; this.currentValue = ''; // Retrieve triggering element (i.e. element with 'data-choice' trigger) this.element = element; this.passedElement = (0, _utils.isType)('String', element) ? document.querySelector(element) : element; if (!this.passedElement) { if (!this.config.silent) { console.error('Passed element not found'); } return; } this.isTextElement = this.passedElement.type === 'text'; this.isSelectOneElement = this.passedElement.type === 'select-one'; this.isSelectMultipleElement = this.passedElement.type === 'select-multiple'; this.isSelectElement = this.isSelectOneElement || this.isSelectMultipleElement; this.isValidElementType = this.isTextElement || this.isSelectElement; this.isIe11 = !!(navigator.userAgent.match(/Trident/) && navigator.userAgent.match(/rv[ :]11/)); this.isScrollingOnIe = false; if (this.config.shouldSortItems === true && this.isSelectOneElement) { if (!this.config.silent) { console.warn('shouldSortElements: Type of passed element is \'select-one\', falling back to false.'); } } this.highlightPosition = 0; this.canSearch = this.config.searchEnabled; this.placeholder = false; if (!this.isSelectOneElement) { this.placeholder = this.config.placeholder ? this.config.placeholderValue || this.passedElement.getAttribute('placeholder') : false; } // Assign preset choices from passed object this.presetChoices = this.config.choices; // Assign preset items from passed object first this.presetItems = this.config.items; // Then add any values passed from attribute if (this.passedElement.value) { this.presetItems = this.presetItems.concat(this.passedElement.value.split(this.config.delimiter)); } // Set unique base Id this.baseId = (0, _utils.generateId)(this.passedElement, 'choices-'); // Bind methods this.render = this.render.bind(this); // Bind event handlers this._onFocus = this._onFocus.bind(this); this._onBlur = this._onBlur.bind(this); this._onKeyUp = this._onKeyUp.bind(this); this._onKeyDown = this._onKeyDown.bind(this); this._onClick = this._onClick.bind(this); this._onTouchMove = this._onTouchMove.bind(this); this._onTouchEnd = this._onTouchEnd.bind(this); this._onMouseDown = this._onMouseDown.bind(this); this._onMouseOver = this._onMouseOver.bind(this); this._onPaste = this._onPaste.bind(this); this._onInput = this._onInput.bind(this); // Monitor touch taps/scrolls this.wasTap = true; // Cutting the mustard var cuttingTheMustard = 'classList' in document.documentElement; if (!cuttingTheMustard && !this.config.silent) { console.error('Choices: Your browser doesn\'t support Choices'); } var canInit = (0, _utils.isElement)(this.passedElement) && this.isValidElementType; if (canInit) { // If element has already been initialised with Choices if (this.passedElement.getAttribute('data-choice') === 'active') { return; } // Let's go this.init(); } else if (!this.config.silent) { console.error('Incompatible input passed'); } } /*======================================== = Public functions = ========================================*/ /** * Initialise Choices * @return * @public */ _createClass(Choices, [{ key: 'init', value: function init() { if (this.initialised === true) { return; } var callback = this.config.callbackOnInit; // Set initialise flag this.initialised = true; // Create required elements this._createTemplates(); // Generate input markup this._createInput(); // Subscribe store to render method this.store.subscribe(this.render); // Render any items this.render(); // Trigger event listeners this._addEventListeners(); // Run callback if it is a function if (callback) { if ((0, _utils.isType)('Function', callback)) { callback.call(this); } } } /** * Destroy Choices and nullify values * @return * @public */ }, { key: 'destroy', value: function destroy() { if (this.initialised === false) { return; } // Remove all event listeners this._removeEventListeners(); // Reinstate passed element this.passedElement.classList.remove(this.config.classNames.input, this.config.classNames.hiddenState); this.passedElement.removeAttribute('tabindex'); // Recover original styles if any var origStyle = this.passedElement.getAttribute('data-choice-orig-style'); if (Boolean(origStyle)) { this.passedElement.removeAttribute('data-choice-orig-style'); this.passedElement.setAttribute('style', origStyle); } else { this.passedElement.removeAttribute('style'); } this.passedElement.removeAttribute('aria-hidden'); this.passedElement.removeAttribute('data-choice'); // Re-assign values - this is weird, I know this.passedElement.value = this.passedElement.value; // Move passed element back to original position this.containerOuter.parentNode.insertBefore(this.passedElement, this.containerOuter); // Remove added elements this.containerOuter.parentNode.removeChild(this.containerOuter); // Clear data store this.clearStore(); // Nullify instance-specific data this.config.templates = null; // Uninitialise this.initialised = false; } /** * Render group choices into a DOM fragment and append to choice list * @param {Array} groups Groups to add to list * @param {Array} choices Choices to add to groups * @param {DocumentFragment} fragment Fragment to add groups and options to (optional) * @return {DocumentFragment} Populated options fragment * @private */ }, { key: 'renderGroups', value: function renderGroups(groups, choices, fragment) { var _this = this; var groupFragment = fragment || document.createDocumentFragment(); var filter = this.config.sortFilter; // If sorting is enabled, filter groups if (this.config.shouldSort) { groups.sort(filter); } groups.forEach(function (group) { // Grab options that are children of this group var groupChoices = choices.filter(function (choice) { if (_this.isSelectOneElement) { return choice.groupId === group.id; } return choice.groupId === group.id && !choice.selected; }); if (groupChoices.length >= 1) { var dropdownGroup = _this._getTemplate('choiceGroup', group); groupFragment.appendChild(dropdownGroup); _this.renderChoices(groupChoices, groupFragment, true); } }); return groupFragment; } /** * Render choices into a DOM fragment and append to choice list * @param {Array} choices Choices to add to list * @param {DocumentFragment} fragment Fragment to add choices to (optional) * @return {DocumentFragment} Populated choices fragment * @private */ }, { key: 'renderChoices', value: function renderChoices(choices, fragment) { var _this2 = this; var withinGroup = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; // Create a fragment to store our list items (so we don't have to update the DOM for each item) var choicesFragment = fragment || document.createDocumentFragment(); var _config = this.config, renderSelectedChoices = _config.renderSelectedChoices, searchResultLimit = _config.searchResultLimit, renderChoiceLimit = _config.renderChoiceLimit; var filter = this.isSearching ? _utils.sortByScore : this.config.sortFilter; var appendChoice = function appendChoice(choice) { var shouldRender = renderSelectedChoices === 'auto' ? _this2.isSelectOneElement || !choice.selected : true; if (shouldRender) { var dropdownItem = _this2._getTemplate('choice', choice); choicesFragment.appendChild(dropdownItem); } }; var rendererableChoices = choices; if (renderSelectedChoices === 'auto' && !this.isSelectOneElement) { rendererableChoices = choices.filter(function (choice) { return !choice.selected; }); } // Split array into placeholders and "normal" choices var _rendererableChoices$ = rendererableChoices.reduce(function (acc, choice) { if (choice.placeholder) { acc.placeholderChoices.push(choice); } else { acc.normalChoices.push(choice); } return acc; }, { placeholderChoices: [], normalChoices: [] }), placeholderChoices = _rendererableChoices$.placeholderChoices, normalChoices = _rendererableChoices$.normalChoices; // If sorting is enabled or the user is searching, filter choices if (this.config.shouldSort || this.isSearching) { normalChoices.sort(filter); } var choiceLimit = rendererableChoices.length; // Prepend placeholeder var sortedChoices = [].concat(_toConsumableArray(placeholderChoices), _toConsumableArray(normalChoices)); if (this.isSearching) { choiceLimit = searchResultLimit; } else if (renderChoiceLimit > 0 && !withinGroup) { choiceLimit = renderChoiceLimit; } // Add each choice to dropdown within range for (var i = 0; i < choiceLimit; i++) { if (sortedChoices[i]) { appendChoice(sortedChoices[i]); } }; return choicesFragment; } /** * Render items into a DOM fragment and append to items list * @param {Array} items Items to add to list * @param {DocumentFragment} [fragment] Fragment to add items to (optional) * @return * @private */ }, { key: 'renderItems', value: function renderItems(items) { var _this3 = this; var fragment = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : null; // Create fragment to add elements to var itemListFragment = fragment || document.createDocumentFragment(); // If sorting is enabled, filter items if (this.config.shouldSortItems && !this.isSelectOneElement) { items.sort(this.config.sortFilter); } if (this.isTextElement) { // Simplify store data to just values var itemsFiltered = this.store.getItemsReducedToValues(items); var itemsFilteredString = itemsFiltered.join(this.config.delimiter); // Update the value of the hidden input this.passedElement.setAttribute('value', itemsFilteredString); this.passedElement.value = itemsFilteredString; } else { var selectedOptionsFragment = document.createDocumentFragment(); // Add each list item to list items.forEach(function (item) { // Create a standard select option var option = _this3._getTemplate('option', item); // Append it to fragment selectedOptionsFragment.appendChild(option); }); // Update selected choices this.passedElement.innerHTML = ''; this.passedElement.appendChild(selectedOptionsFragment); } // Add each list item to list items.forEach(function (item) { // Create new list element var listItem = _this3._getTemplate('item', item); // Append it to list itemListFragment.appendChild(listItem); }); return itemListFragment; } /** * Render DOM with values * @return * @private */ }, { key: 'render', value: function render() { if (this.store.isLoading()) { return; } this.currentState = this.store.getState(); // Only render if our state has actually changed if (this.currentState !== this.prevState) { // Choices if (this.currentState.choices !== this.prevState.choices || this.currentState.groups !== this.prevState.groups || this.currentState.items !== this.prevState.items) { if (this.isSelectElement) { // Get active groups/choices var activeGroups = this.store.getGroupsFilteredByActive(); var activeChoices = this.store.getChoicesFilteredByActive(); var choiceListFragment = document.createDocumentFragment(); // Clear choices this.choiceList.innerHTML = ''; // Scroll back to top of choices list if (this.config.resetScrollPosition) { this.choiceList.scrollTop = 0; } // If we have grouped options if (activeGroups.length >= 1 && this.isSearching !== true) { choiceListFragment = this.renderGroups(activeGroups, activeChoices, choiceListFragment); } else if (activeChoices.length >= 1) { choiceListFragment = this.renderChoices(activeChoices, choiceListFragment); } var activeItems = this.store.getItemsFilteredByActive(); var canAddItem = this._canAddItem(activeItems, this.input.value); // If we have choices to show if (choiceListFragment.childNodes && choiceListFragment.childNodes.length > 0) { // ...and we can select them if (canAddItem.response) { // ...append them and highlight the first choice this.choiceList.appendChild(choiceListFragment); this._highlightChoice(); } else { // ...otherwise show a notice this.choiceList.appendChild(this._getTemplate('notice', canAddItem.notice)); } } else { // Otherwise show a notice var dropdownItem = void 0; var notice = void 0; if (this.isSearching) { notice = (0, _utils.isType)('Function', this.config.noResultsText) ? this.config.noResultsText() : this.config.noResultsText; dropdownItem = this._getTemplate('notice', notice, 'no-results'); } else { notice = (0, _utils.isType)('Function', this.config.noChoicesText) ? this.config.noChoicesText() : this.config.noChoicesText; dropdownItem = this._getTemplate('notice', notice, 'no-choices'); } this.choiceList.appendChild(dropdownItem); } } } // Items if (this.currentState.items !== this.prevState.items) { // Get active items (items that can be selected) var _activeItems = this.store.getItemsFilteredByActive(); // Clear list this.itemList.innerHTML = ''; if (_activeItems && _activeItems) { // Create a fragment to store our list items // (so we don't have to update the DOM for each item) var itemListFragment = this.renderItems(_activeItems); // If we have items to add if (itemListFragment.childNodes) { // Update list this.itemList.appendChild(itemListFragment); } } } this.prevState = this.currentState; } } /** * Select item (a selected item can be deleted) * @param {Element} item Element to select * @param {Boolean} [runEvent=true] Whether to trigger 'highlightItem' event * @return {Object} Class instance * @public */ }, { key: 'highlightItem', value: function highlightItem(item) { var runEvent = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : true; if (!item) { return this; } var id = item.id; var groupId = item.groupId; var group = groupId >= 0 ? this.store.getGroupById(groupId) : null; this.store.dispatch((0, _index3.highlightItem)(id, true)); if (runEvent) { if (group && group.value) { (0, _utils.triggerEvent)(this.passedElement, 'highlightItem', { id: id, value: item.value, label: item.label, groupValue: group.value }); } else { (0, _utils.triggerEvent)(this.passedElement, 'highlightItem', { id: id, value: item.value, label: item.label }); } } return this; } /** * Deselect item * @param {Element} item Element to de-select * @return {Object} Class instance * @public */ }, { key: 'unhighlightItem', value: function unhighlightItem(item) { if (!item) { return this; } var id = item.id; var groupId = item.groupId; var group = groupId >= 0 ? this.store.getGroupById(groupId) : null; this.store.dispatch((0, _index3.highlightItem)(id, false)); if (group && group.value) { (0, _utils.triggerEvent)(this.passedElement, 'unhighlightItem', { id: id, value: item.value, label: item.label, groupValue: group.value }); } else { (0, _utils.triggerEvent)(this.passedElement, 'unhighlightItem', { id: id, value: item.value, label: item.label }); } return this; } /** * Highlight items within store * @return {Object} Class instance * @public */ }, { key: 'highlightAll', value: function highlightAll() { var _this4 = this; var items = this.store.getItems(); items.forEach(function (item) { _this4.highlightItem(item); }); return this; } /** * Deselect items within store * @return {Object} Class instance * @public */ }, { key: 'unhighlightAll', value: function unhighlightAll() { var _this5 = this; var items = this.store.getItems(); items.forEach(function (item) { _this5.unhighlightItem(item); }); return this; } /** * Remove an item from the store by its value * @param {String} value Value to search for * @return {Object} Class instance * @public */ }, { key: 'removeItemsByValue', value: function removeItemsByValue(value) { var _this6 = this; if (!value || !(0, _utils.isType)('String', value)) { return this; } var items = this.store.getItemsFilteredByActive(); items.forEach(function (item) { if (item.value === value) { _this6._removeItem(item); } }); return this; } /** * Remove all items from store array * @note Removed items are soft deleted * @param {Number} excludedId Optionally exclude item by ID * @return {Object} Class instance * @public */ }, { key: 'removeActiveItems', value: function removeActiveItems(excludedId) { var _this7 = this; var items = this.store.getItemsFilteredByActive(); items.forEach(function (item) { if (item.active && excludedId !== item.id) { _this7._removeItem(item); } }); return this; } /** * Remove all selected items from store * @note Removed items are soft deleted * @return {Object} Class instance * @public */ }, { key: 'removeHighlightedItems', value: function removeHighlightedItems() { var _this8 = this; var runEvent = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; var items = this.store.getItemsFilteredByActive(); items.forEach(function (item) { if (item.highlighted && item.active) { _this8._removeItem(item); // If this action was performed by the user // trigger the event if (runEvent) { _this8._triggerChange(item.value); } } }); return this; } /** * Show dropdown to user by adding active state class * @return {Object} Class instance * @public */ }, { key: 'showDropdown', value: function showDropdown() { var focusInput = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; var body = document.body; var html = document.documentElement; var winHeight = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight, html.scrollHeight, html.offsetHeight); this.containerOuter.classList.add(this.config.classNames.openState); this.containerOuter.setAttribute('aria-expanded', 'true'); this.dropdown.classList.add(this.config.classNames.activeState); this.dropdown.setAttribute('aria-expanded', 'true'); var dimensions = this.dropdown.getBoundingClientRect(); var dropdownPos = Math.ceil(dimensions.top + window.scrollY + this.dropdown.offsetHeight); // If flip is enabled and the dropdown bottom position is greater than the window height flip the dropdown. var shouldFlip = false; if (this.config.position === 'auto') { shouldFlip = dropdownPos >= winHeight; } else if (this.config.position === 'top') { shouldFlip = true; } if (shouldFlip) { this.containerOuter.classList.add(this.config.classNames.flippedState); } // Optionally focus the input if we have a search input if (focusInput && this.canSearch && document.activeElement !== this.input) { this.input.focus(); } (0, _utils.triggerEvent)(this.passedElement, 'showDropdown', {}); return this; } /** * Hide dropdown from user * @return {Object} Class instance * @public */ }, { key: 'hideDropdown', value: function hideDropdown() { var blurInput = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; // A dropdown flips if it does not have space within the page var isFlipped = this.containerOuter.classList.contains(this.config.classNames.flippedState); this.containerOuter.classList.remove(this.config.classNames.openState); this.containerOuter.setAttribute('aria-expanded', 'false'); this.dropdown.classList.remove(this.config.classNames.activeState); this.dropdown.setAttribute('aria-expanded', 'false'); if (isFlipped) { this.containerOuter.classList.remove(this.config.classNames.flippedState); } // Optionally blur the input if we have a search input if (blurInput && this.canSearch && document.activeElement === this.input) { this.input.blur(); } (0, _utils.triggerEvent)(this.passedElement, 'hideDropdown', {}); return this; } /** * Determine whether to hide or show dropdown based on its current state * @return {Object} Class instance * @public */ }, { key: 'toggleDropdown', value: function toggleDropdown() { var hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState); if (hasActiveDropdown) { this.hideDropdown(); } else { this.showDropdown(true); } return this; } /** * Get value(s) of input (i.e. inputted items (text) or selected choices (select)) * @param {Boolean} valueOnly Get only values of selected items, otherwise return selected items * @return {Array/String} selected value (select-one) or array of selected items (inputs & select-multiple) * @public */ }, { key: 'getValue', value: function getValue() { var _this9 = this; var valueOnly = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : false; var items = this.store.getItemsFilteredByActive(); var selectedItems = []; items.forEach(function (item) { if (_this9.isTextElement) { selectedItems.push(valueOnly ? item.value : item); } else if (item.active) { selectedItems.push(valueOnly ? item.value : item); } }); if (this.isSelectOneElement) { return selectedItems[0]; } return selectedItems; } /** * Set value of input. If the input is a select box, a choice will be created and selected otherwise * an item will created directly. * @param {Array} args Array of value objects or value strings * @return {Object} Class instance * @public */ }, { key: 'setValue', value: function setValue(args) { var _this10 = this; if (this.initialised === true) { // Convert args to an iterable array var values = [].concat(_toConsumableArray(args)), handleValue = function handleValue(item) { var itemType = (0, _utils.getType)(item); if (itemType === 'Object') { if (!item.value) { return; } // If we are dealing with a select input, we need to create an option first // that is then selected. For text inputs we can just add items normally. if (!_this10.isTextElement) { _this10._addChoice(item.value, item.label, true, false, -1, item.customProperties, item.placeholder); } else { _this10._addItem(item.value, item.label, item.id, undefined, item.customProperties, item.placeholder); } } else if (itemType === 'String') { if (!_this10.isTextElement) { _this10._addChoice(item, item, true, false, -1, null); } else { _this10._addItem(item); } } }; if (values.length > 1) { values.forEach(function (value) { handleValue(value); }); } else { handleValue(values[0]); } } return this; } /** * Select value of select box via the value of an existing choice * @param {Array/String} value An array of strings of a single string * @return {Object} Class instance * @public */ }, { key: 'setValueByChoice', value: function setValueByChoice(value) { var _this11 = this; if (!this.isTextElement) { var choices = this.store.getChoices(); // If only one value has been passed, convert to array var choiceValue = (0, _utils.isType)('Array', value) ? value : [value]; // Loop through each value and choiceValue.forEach(function (val) { var foundChoice = choices.find(function (choice) { // Check 'value' property exists and the choice isn't already selected return _this11.config.itemComparer(choice.value, val); }); if (foundChoice) { if (!foundChoice.selected) { _this11._addItem(foundChoice.value, foundChoice.label, foundChoice.id, foundChoice.groupId, foundChoice.customProperties, foundChoice.placeholder, foundChoice.keyCode); } else if (!_this11.config.silent) { console.warn('Attempting to select choice already selected'); } } else if (!_this11.config.silent) { console.warn('Attempting to select choice that does not exist'); } }); } return this; } /** * Direct populate choices * @param {Array} choices - Choices to insert * @param {String} value - Name of 'value' property * @param {String} label - Name of 'label' property * @param {Boolean} replaceChoices Whether existing choices should be removed * @return {Object} Class instance * @public */ }, { key: 'setChoices', value: function setChoices(choices, value, label) { var _this12 = this; var replaceChoices = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false; if (this.initialised === true) { if (this.isSelectElement) { if (!(0, _utils.isType)('Array', choices) || !value) { return this; } // Clear choices if needed if (replaceChoices) { this._clearChoices(); } this._setLoading(true); // Add choices if passed if (choices && choices.length) { this.containerOuter.classList.remove(this.config.classNames.loadingState); choices.forEach(function (result) { if (result.choices) { _this12._addGroup(result, result.id || null, value, label); } else { _this12._addChoice(result[value], result[label], result.selected, result.disabled, undefined, result.customProperties, result.placeholder); } }); } this._setLoading(false); } } return this; } /** * Clear items,choices and groups * @note Hard delete * @return {Object} Class instance * @public */ }, { key: 'clearStore', value: function clearStore() { this.store.dispatch((0, _index3.clearAll)()); return this; } /** * Set value of input to blank * @return {Object} Class instance * @public */ }, { key: 'clearInput', value: function clearInput() { if (this.input.value) { this.input.value = ''; } if (!this.isSelectOneElement) { this._setInputWidth(); } if (!this.isTextElement && this.config.searchEnabled) { this.isSearching = false; this.store.dispatch((0, _index3.activateChoices)(true)); } return this; } /** * Enable interaction with Choices * @return {Object} Class instance */ }, { key: 'enable', value: function enable() { if (this.initialised) { this.passedElement.disabled = false; var isDisabled = this.containerOuter.classList.contains(this.config.classNames.disabledState); if (isDisabled) { this._addEventListeners(); this.passedElement.removeAttribute('disabled'); this.input.removeAttribute('disabled'); this.containerOuter.classList.remove(this.config.classNames.disabledState); this.containerOuter.removeAttribute('aria-disabled'); if (this.isSelectOneElement) { this.containerOuter.setAttribute('tabindex', '0'); } } } return this; } /** * Disable interaction with Choices * @return {Object} Class instance * @public */ }, { key: 'disable', value: function disable() { if (this.initialised) { this.passedElement.disabled = true; var isEnabled = !this.containerOuter.classList.contains(this.config.classNames.disabledState); if (isEnabled) { this._removeEventListeners(); this.passedElement.setAttribute('disabled', ''); this.input.setAttribute('disabled', ''); this.containerOuter.classList.add(this.config.classNames.disabledState); this.containerOuter.setAttribute('aria-disabled', 'true'); if (this.isSelectOneElement) { this.containerOuter.setAttribute('tabindex', '-1'); } } } return this; } /** * Populate options via ajax callback * @param {Function} fn Function that actually makes an AJAX request * @return {Object} Class instance * @public */ }, { key: 'ajax', value: function ajax(fn) { var _this13 = this; if (this.initialised === true) { if (this.isSelectElement) { // Show loading text requestAnimationFrame(function () { _this13._handleLoadingState(true); }); // Run callback fn(this._ajaxCallback()); } } return this; } /*===== End of Public functions ======*/ /*============================================= = Private functions = =============================================*/ /** * Call change callback * @param {String} value - last added/deleted/selected value * @return * @private */ }, { key: '_triggerChange', value: function _triggerChange(value) { if (!value) { return; } (0, _utils.triggerEvent)(this.passedElement, 'change', { value: value }); } /** * Process enter/click of an item button * @param {Array} activeItems The currently active items * @param {Element} element Button being interacted with * @return * @private */ }, { key: '_handleButtonAction', value: function _handleButtonAction(activeItems, element) { if (!activeItems || !element) { return; } // If we are clicking on a button if (this.config.removeItems && this.config.removeItemButton) { var itemId = element.parentNode.getAttribute('data-id'); var itemToRemove = activeItems.find(function (item) { return item.id === parseInt(itemId, 10); }); // Remove item associated with button this._removeItem(itemToRemove); this._triggerChange(itemToRemove.value); if (this.isSelectOneElement) { this._selectPlaceholderChoice(); } } } /** * Select placeholder choice */ }, { key: '_selectPlaceholderChoice', value: function _selectPlaceholderChoice() { var placeholderChoice = this.store.getPlaceholderChoice(); if (placeholderChoice) { this._addItem(placeholderChoice.value, placeholderChoice.label, placeholderChoice.id, placeholderChoice.groupId, null, placeholderChoice.placeholder); this._triggerChange(placeholderChoice.value); } } /** * Process click of an item * @param {Array} activeItems The currently active items * @param {Element} element Item being interacted with * @param {Boolean} hasShiftKey Whether the user has the shift key active * @return * @private */ }, { key: '_handleItemAction', value: function _handleItemAction(activeItems, element) { var _this14 = this; var hasShiftKey = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; if (!activeItems || !element) { return; } // If we are clicking on an item if (this.config.removeItems && !this.isSelectOneElement) { var passedId = element.getAttribute('data-id'); // We only want to select one item with a click // so we deselect any items that aren't the target // unless shift is being pressed activeItems.forEach(function (item) { if (item.id === parseInt(passedId, 10) && !item.highlighted) { _this14.highlightItem(item); } else if (!hasShiftKey) { if (item.highlighted) { _this14.unhighlightItem(item); } } }); // Focus input as without focus, a user cannot do anything with a // highlighted item if (document.activeElement !== this.input) { this.input.focus(); } } } /** * Process click of a choice * @param {Array} activeItems The currently active items * @param {Element} element Choice being interacted with * @return */ }, { key: '_handleChoiceAction', value: function _handleChoiceAction(activeItems, element) { if (!activeItems || !element) { return; } // If we are clicking on an option var id = element.getAttribute('data-id'); var choice = this.store.getChoiceById(id); var passedKeyCode = activeItems[0] && activeItems[0].keyCode ? activeItems[0].keyCode : null; var hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState); // Update choice keyCode choice.keyCode = passedKeyCode; (0, _utils.triggerEvent)(this.passedElement, 'choice', { choice: choice }); if (choice && !choice.selected && !choice.disabled) { var canAddItem = this._canAddItem(activeItems, choice.value); if (canAddItem.response) { this._addItem(choice.value, choice.label, choice.id, choice.groupId, choice.customProperties, choice.placeholder, choice.keyCode); this._triggerChange(choice.value); } } this.clearInput(); // We wont to close the dropdown if we are dealing with a single select box if (hasActiveDropdown && this.isSelectOneElement) { this.hideDropdown(); this.containerOuter.focus(); } } /** * Process back space event * @param {Array} activeItems items * @return * @private */ }, { key: '_handleBackspace', value: function _handleBackspace(activeItems) { if (this.config.removeItems && activeItems) { var lastItem = activeItems[activeItems.length - 1]; var hasHighlightedItems = activeItems.some(function (item) { return item.highlighted; }); // If editing the last item is allowed and there are not other selected items, // we can edit the item value. Otherwise if we can remove items, remove all selected items if (this.config.editItems && !hasHighlightedItems && lastItem) { this.input.value = lastItem.value; this._setInputWidth(); this._removeItem(lastItem); this._triggerChange(lastItem.value); } else { if (!hasHighlightedItems) { this.highlightItem(lastItem, false); } this.removeHighlightedItems(true); } } } /** * Validates whether an item can be added by a user * @param {Array} activeItems The currently active items * @param {String} value Value of item to add * @return {Object} Response: Whether user can add item * Notice: Notice show in dropdown */ }, { key: '_canAddItem', value: function _canAddItem(activeItems, value) { var canAddItem = true; var notice = (0, _utils.isType)('Function', this.config.addItemText) ? this.config.addItemText(value) : this.config.addItemText; if (this.isSelectMultipleElement || this.isTextElement) { if (this.config.maxItemCount > 0 && this.config.maxItemCount <= activeItems.length) { // If there is a max entry limit and we have reached that limit // don't update canAddItem = false; notice = (0, _utils.isType)('Function', this.config.maxItemText) ? this.config.maxItemText(this.config.maxItemCount) : this.config.maxItemText; } } if (this.isTextElement && this.config.addItems && canAddItem) { // If a user has supplied a regular expression filter if (this.config.regexFilter) { // Determine whether we can update based on whether // our regular expression passes canAddItem = this._regexFilter(value); } } // If no duplicates are allowed, and the value already exists // in the array var isUnique = !activeItems.some(function (item) { if ((0, _utils.isType)('String', value)) { return item.value === value.trim(); } return item.value === value; }); if (!isUnique && !this.config.duplicateItems && !this.isSelectOneElement && canAddItem) { canAddItem = false; notice = (0, _utils.isType)('Function', this.config.uniqueItemText) ? this.config.uniqueItemText(value) : this.config.uniqueItemText; } return { response: canAddItem, notice: notice }; } /** * Apply or remove a loading state to the component. * @param {Boolean} setLoading default value set to 'true'. * @return * @private */ }, { key: '_handleLoadingState', value: function _handleLoadingState() { var setLoading = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : true; var placeholderItem = this.itemList.querySelector('.' + this.config.classNames.placeholder); if (setLoading) { this.containerOuter.classList.add(this.config.classNames.loadingState); this.containerOuter.setAttribute('aria-busy', 'true'); if (this.isSelectOneElement) { if (!placeholderItem) { placeholderItem = this._getTemplate('placeholder', this.config.loadingText); this.itemList.appendChild(placeholderItem); } else { placeholderItem.innerHTML = this.config.loadingText; } } else { this.input.placeholder = this.config.loadingText; } } else { // Remove loading states/text this.containerOuter.classList.remove(this.config.classNames.loa