choices.js
Version:
A vanilla JS customisable text input/select box plugin
1,525 lines (1,263 loc) • 199 kB
JavaScript
/*! 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