Choices/src/scripts/choices.js
Chris DeLuca e7d775e2ae Use event delegation (#644)
Instead of attaching a new root-level event listener for bubbling events
for every choices instance, use a simple event delegation script to
handle each event type.

Each event callback function already is coded as if it were fully
delegated, since the events are attached at the document level, so
no changes are needed to detect which element is being called.

Note that focus and blur event do not bubble, so they have been left as
they are.

Also note that the event delegation uses an IIFE purposely instead of
ES6 modules, since the event list should be globally cached, and it
doesn't make sense to instantiate a new scope for each instance (then
we're back where we started!)

fix #643
2019-10-15 08:42:31 +01:00

2088 lines
58 KiB
JavaScript

import Fuse from 'fuse.js';
import merge from 'deepmerge';
import './lib/delegate-events';
import Store from './store/store';
import {
Dropdown,
Container,
Input,
List,
WrappedInput,
WrappedSelect,
} from './components';
import { DEFAULT_CONFIG, EVENTS, KEY_CODES } from './constants';
import { TEMPLATES } from './templates';
import {
addChoice,
filterChoices,
activateChoices,
clearChoices,
} from './actions/choices';
import { addItem, removeItem, highlightItem } from './actions/items';
import { addGroup } from './actions/groups';
import { clearAll, resetTo } from './actions/misc';
import { setIsLoading } from './actions/general';
import {
isScrolledIntoView,
getAdjacentEl,
getType,
isType,
strToEl,
sortByScore,
generateId,
findAncestorByAttrName,
fetchFromObject,
isIE11,
existsInArray,
cloneObject,
diff,
} from './lib/utils';
/**
* Choices
* @author Josh Johnson<josh@joshuajohnson.co.uk>
*/
class Choices {
constructor(element = '[data-choice]', userConfig = {}) {
if (isType('String', element)) {
const elements = Array.from(document.querySelectorAll(element));
// If there are multiple elements, create a new instance
// for each element besides the first one (as that already has an instance)
if (elements.length > 1) {
return this._generateInstances(elements, userConfig);
}
}
this.config = merge.all(
[DEFAULT_CONFIG, Choices.userDefaults, userConfig],
// When merging array configs, replace with a copy of the userConfig array,
// instead of concatenating with the default array
{ arrayMerge: (destinationArray, sourceArray) => [...sourceArray] },
);
const invalidConfigOptions = diff(this.config, DEFAULT_CONFIG);
if (invalidConfigOptions.length) {
console.warn(
'Unknown config option(s) passed',
invalidConfigOptions.join(', '),
);
}
if (!['auto', 'always'].includes(this.config.renderSelectedChoices)) {
this.config.renderSelectedChoices = 'auto';
}
// Retrieve triggering element (i.e. element with 'data-choice' trigger)
const passedElement = isType('String', element)
? document.querySelector(element)
: element;
if (!passedElement) {
return console.error(
'Could not find passed element or passed element was of an invalid type',
);
}
this._isTextElement = passedElement.type === 'text';
this._isSelectOneElement = passedElement.type === 'select-one';
this._isSelectMultipleElement = passedElement.type === 'select-multiple';
this._isSelectElement =
this._isSelectOneElement || this._isSelectMultipleElement;
if (this._isTextElement) {
this.passedElement = new WrappedInput({
element: passedElement,
classNames: this.config.classNames,
delimiter: this.config.delimiter,
});
} else if (this._isSelectElement) {
this.passedElement = new WrappedSelect({
element: passedElement,
classNames: this.config.classNames,
});
}
if (!this.passedElement) {
return console.error('Passed element was of an invalid type');
}
if (
this.config.shouldSortItems === true &&
this._isSelectOneElement &&
!this.config.silent
) {
console.warn(
"shouldSortElements: Type of passed element is 'select-one', falling back to false.",
);
}
this.initialised = false;
this._store = new Store(this.render);
this._initialState = {};
this._currentState = {};
this._prevState = {};
this._currentValue = '';
this._canSearch = this.config.searchEnabled;
this._isScrollingOnIe = false;
this._highlightPosition = 0;
this._wasTap = true;
this._placeholderValue = this._generatePlaceholderValue();
this._baseId = generateId(this.passedElement.element, 'choices-');
this._direction = this.passedElement.element.getAttribute('dir') || 'ltr';
this._idNames = {
itemChoice: 'item-choice',
};
// 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),
);
}
this._render = this._render.bind(this);
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._onFormReset = this._onFormReset.bind(this);
this._onAKey = this._onAKey.bind(this);
this._onEnterKey = this._onEnterKey.bind(this);
this._onEscapeKey = this._onEscapeKey.bind(this);
this._onDirectionKey = this._onDirectionKey.bind(this);
this._onDeleteKey = this._onDeleteKey.bind(this);
// If element has already been initialised with Choices, fail silently
if (this.passedElement.element.getAttribute('data-choice') === 'active') {
console.warn(
'Trying to initialise Choices on element already initialised',
);
}
// Let's go
this.init();
}
/* ========================================
= Public functions =
======================================== */
init() {
if (this.initialised) {
return;
}
this._createTemplates();
this._createElements();
this._createStructure();
// Set initial state (We need to clone the state because some reducers
// modify the inner objects properties in the state) 🤢
this._initialState = cloneObject(this._store.state);
this._store.subscribe(this._render);
this._render();
this._addEventListeners();
const shouldDisable =
!this.config.addItems ||
this.passedElement.element.hasAttribute('disabled');
if (shouldDisable) {
this.disable();
}
this.initialised = true;
const { callbackOnInit } = this.config;
// Run callback if it is a function
if (callbackOnInit && isType('Function', callbackOnInit)) {
callbackOnInit.call(this);
}
}
destroy() {
if (!this.initialised) {
return;
}
this._removeEventListeners();
this.passedElement.reveal();
this.containerOuter.unwrap(this.passedElement.element);
if (this._isSelectElement) {
this.passedElement.options = this._presetChoices;
}
this.clearStore();
this.config.templates = null;
this.initialised = false;
}
enable() {
if (this.passedElement.isDisabled) {
this.passedElement.enable();
}
if (this.containerOuter.isDisabled) {
this._addEventListeners();
this.input.enable();
this.containerOuter.enable();
}
return this;
}
disable() {
if (!this.passedElement.isDisabled) {
this.passedElement.disable();
}
if (!this.containerOuter.isDisabled) {
this._removeEventListeners();
this.input.disable();
this.containerOuter.disable();
}
return this;
}
highlightItem(item, runEvent = true) {
if (!item) {
return this;
}
const { id, groupId = -1, value = '', label = '' } = item;
const group = groupId >= 0 ? this._store.getGroupById(groupId) : null;
this._store.dispatch(highlightItem(id, true));
if (runEvent) {
this.passedElement.triggerEvent(EVENTS.highlightItem, {
id,
value,
label,
groupValue: group && group.value ? group.value : null,
});
}
return this;
}
unhighlightItem(item) {
if (!item) {
return this;
}
const { id, groupId = -1, value = '', label = '' } = item;
const group = groupId >= 0 ? this._store.getGroupById(groupId) : null;
this._store.dispatch(highlightItem(id, false));
this.passedElement.triggerEvent(EVENTS.highlightItem, {
id,
value,
label,
groupValue: group && group.value ? group.value : null,
});
return this;
}
highlightAll() {
this._store.items.forEach(item => this.highlightItem(item));
return this;
}
unhighlightAll() {
this._store.items.forEach(item => this.unhighlightItem(item));
return this;
}
removeActiveItemsByValue(value) {
this._store.activeItems
.filter(item => item.value === value)
.forEach(item => this._removeItem(item));
return this;
}
removeActiveItems(excludedId) {
this._store.activeItems
.filter(({ id }) => id !== excludedId)
.forEach(item => this._removeItem(item));
return this;
}
removeHighlightedItems(runEvent = false) {
this._store.highlightedActiveItems.forEach(item => {
this._removeItem(item);
// If this action was performed by the user
// trigger the event
if (runEvent) {
this._triggerChange(item.value);
}
});
return this;
}
showDropdown(preventInputFocus) {
if (this.dropdown.isActive) {
return this;
}
requestAnimationFrame(() => {
this.dropdown.show();
this.containerOuter.open(this.dropdown.distanceFromTopWindow());
if (!preventInputFocus && this._canSearch) {
this.input.focus();
}
this.passedElement.triggerEvent(EVENTS.showDropdown, {});
});
return this;
}
hideDropdown(preventInputBlur) {
if (!this.dropdown.isActive) {
return this;
}
requestAnimationFrame(() => {
this.dropdown.hide();
this.containerOuter.close();
if (!preventInputBlur && this._canSearch) {
this.input.removeActiveDescendant();
this.input.blur();
}
this.passedElement.triggerEvent(EVENTS.hideDropdown, {});
});
return this;
}
getValue(valueOnly = false) {
const values = this._store.activeItems.reduce((selectedItems, item) => {
const itemValue = valueOnly ? item.value : item;
selectedItems.push(itemValue);
return selectedItems;
}, []);
return this._isSelectOneElement ? values[0] : values;
}
setValue(args) {
if (!this.initialised) {
return this;
}
[...args].forEach(value => this._setChoiceOrItem(value));
return this;
}
setChoiceByValue(value) {
if (!this.initialised || this._isTextElement) {
return this;
}
// If only one value has been passed, convert to array
const choiceValue = isType('Array', value) ? value : [value];
// Loop through each value and
choiceValue.forEach(val => this._findAndSelectChoiceByValue(val));
return this;
}
setChoices(choices = [], value = '', label = '', replaceChoices = false) {
if (!this._isSelectElement || !value) {
return this;
}
// Clear choices if needed
if (replaceChoices) {
this.clearChoices();
}
this.containerOuter.removeLoadingState();
const addGroupsAndChoices = groupOrChoice => {
if (groupOrChoice.choices) {
this._addGroup({
group: groupOrChoice,
id: groupOrChoice.id || null,
valueKey: value,
labelKey: label,
});
} else {
this._addChoice({
value: groupOrChoice[value],
label: groupOrChoice[label],
isSelected: groupOrChoice.selected,
isDisabled: groupOrChoice.disabled,
customProperties: groupOrChoice.customProperties,
placeholder: groupOrChoice.placeholder,
});
}
};
this._setLoading(true);
choices.forEach(addGroupsAndChoices);
this._setLoading(false);
return this;
}
clearChoices() {
this._store.dispatch(clearChoices());
}
clearStore() {
this._store.dispatch(clearAll());
return this;
}
clearInput() {
const shouldSetInputWidth = !this._isSelectOneElement;
this.input.clear(shouldSetInputWidth);
if (!this._isTextElement && this._canSearch) {
this._isSearching = false;
this._store.dispatch(activateChoices(true));
}
return this;
}
ajax(fn) {
if (!this.initialised || !this._isSelectElement || !fn) {
return this;
}
requestAnimationFrame(() => this._handleLoadingState(true));
fn(this._ajaxCallback());
return this;
}
/* ===== End of Public functions ====== */
/* =============================================
= Private functions =
============================================= */
_render() {
if (this._store.isLoading()) {
return;
}
this._currentState = this._store.state;
const stateChanged =
this._currentState.choices !== this._prevState.choices ||
this._currentState.groups !== this._prevState.groups ||
this._currentState.items !== this._prevState.items;
const shouldRenderChoices = this._isSelectElement;
const shouldRenderItems =
this._currentState.items !== this._prevState.items;
if (!stateChanged) {
return;
}
if (shouldRenderChoices) {
this._renderChoices();
}
if (shouldRenderItems) {
this._renderItems();
}
this._prevState = this._currentState;
}
_renderChoices() {
const { activeGroups, activeChoices } = this._store;
let choiceListFragment = document.createDocumentFragment();
this.choiceList.clear();
if (this.config.resetScrollPosition) {
requestAnimationFrame(() => this.choiceList.scrollToTop());
}
// If we have grouped options
if (activeGroups.length >= 1 && !this._isSearching) {
// If we have a placeholder choice along with groups
const activePlaceholders = activeChoices.filter(
activeChoice =>
activeChoice.placeholder === true && activeChoice.groupId === -1,
);
if (activePlaceholders.length >= 1) {
choiceListFragment = this._createChoicesFragment(
activePlaceholders,
choiceListFragment,
);
}
choiceListFragment = this._createGroupsFragment(
activeGroups,
activeChoices,
choiceListFragment,
);
} else if (activeChoices.length >= 1) {
choiceListFragment = this._createChoicesFragment(
activeChoices,
choiceListFragment,
);
}
// If we have choices to show
if (
choiceListFragment.childNodes &&
choiceListFragment.childNodes.length > 0
) {
const activeItems = this._store.activeItems;
const canAddItem = this._canAddItem(activeItems, this.input.value);
// ...and we can select them
if (canAddItem.response) {
// ...append them and highlight the first choice
this.choiceList.append(choiceListFragment);
this._highlightChoice();
} else {
// ...otherwise show a notice
this.choiceList.append(this._getTemplate('notice', canAddItem.notice));
}
} else {
// Otherwise show a notice
let dropdownItem;
let notice;
if (this._isSearching) {
notice = isType('Function', this.config.noResultsText)
? this.config.noResultsText()
: this.config.noResultsText;
dropdownItem = this._getTemplate('notice', notice, 'no-results');
} else {
notice = isType('Function', this.config.noChoicesText)
? this.config.noChoicesText()
: this.config.noChoicesText;
dropdownItem = this._getTemplate('notice', notice, 'no-choices');
}
this.choiceList.append(dropdownItem);
}
}
_renderItems() {
const activeItems = this._store.activeItems || [];
this.itemList.clear();
// Create a fragment to store our list items
// (so we don't have to update the DOM for each item)
const itemListFragment = this._createItemsFragment(activeItems);
// If we have items to add, append them
if (itemListFragment.childNodes) {
this.itemList.append(itemListFragment);
}
}
_createGroupsFragment(groups, choices, fragment) {
const groupFragment = fragment || document.createDocumentFragment();
const getGroupChoices = group =>
choices.filter(choice => {
if (this._isSelectOneElement) {
return choice.groupId === group.id;
}
return (
choice.groupId === group.id &&
(this.config.renderSelectedChoices === 'always' || !choice.selected)
);
});
// If sorting is enabled, filter groups
if (this.config.shouldSort) {
groups.sort(this.config.sortFn);
}
groups.forEach(group => {
const groupChoices = getGroupChoices(group);
if (groupChoices.length >= 1) {
const dropdownGroup = this._getTemplate('choiceGroup', group);
groupFragment.appendChild(dropdownGroup);
this._createChoicesFragment(groupChoices, groupFragment, true);
}
});
return groupFragment;
}
_createChoicesFragment(choices, fragment, withinGroup = false) {
// Create a fragment to store our list items (so we don't have to update the DOM for each item)
const choicesFragment = fragment || document.createDocumentFragment();
const {
renderSelectedChoices,
searchResultLimit,
renderChoiceLimit,
} = this.config;
const filter = this._isSearching ? sortByScore : this.config.sortFn;
const appendChoice = choice => {
const shouldRender =
renderSelectedChoices === 'auto'
? this._isSelectOneElement || !choice.selected
: true;
if (shouldRender) {
const dropdownItem = this._getTemplate(
'choice',
choice,
this.config.itemSelectText,
);
choicesFragment.appendChild(dropdownItem);
}
};
let rendererableChoices = choices;
if (renderSelectedChoices === 'auto' && !this._isSelectOneElement) {
rendererableChoices = choices.filter(choice => !choice.selected);
}
// Split array into placeholders and "normal" choices
const { placeholderChoices, normalChoices } = rendererableChoices.reduce(
(acc, choice) => {
if (choice.placeholder) {
acc.placeholderChoices.push(choice);
} else {
acc.normalChoices.push(choice);
}
return acc;
},
{ placeholderChoices: [], normalChoices: [] },
);
// If sorting is enabled or the user is searching, filter choices
if (this.config.shouldSort || this._isSearching) {
normalChoices.sort(filter);
}
let choiceLimit = rendererableChoices.length;
// Prepend placeholeder
const sortedChoices = [...placeholderChoices, ...normalChoices];
if (this._isSearching) {
choiceLimit = searchResultLimit;
} else if (renderChoiceLimit > 0 && !withinGroup) {
choiceLimit = renderChoiceLimit;
}
// Add each choice to dropdown within range
for (let i = 0; i < choiceLimit; i += 1) {
if (sortedChoices[i]) {
appendChoice(sortedChoices[i]);
}
}
return choicesFragment;
}
_createItemsFragment(items, fragment = null) {
// Create fragment to add elements to
const { shouldSortItems, sortFn, removeItemButton } = this.config;
const itemListFragment = fragment || document.createDocumentFragment();
// If sorting is enabled, filter items
if (shouldSortItems && !this._isSelectOneElement) {
items.sort(sortFn);
}
if (this._isTextElement) {
// Update the value of the hidden input
this.passedElement.value = items;
} else {
// Update the options of the hidden input
this.passedElement.options = items;
}
const addItemToFragment = item => {
// Create new list element
const listItem = this._getTemplate('item', item, removeItemButton);
// Append it to list
itemListFragment.appendChild(listItem);
};
// Add each list item to list
items.forEach(item => addItemToFragment(item));
return itemListFragment;
}
_triggerChange(value) {
if (value === undefined || value === null) {
return;
}
this.passedElement.triggerEvent(EVENTS.change, {
value,
});
}
_selectPlaceholderChoice() {
const placeholderChoice = this._store.placeholderChoice;
if (placeholderChoice) {
this._addItem({
value: placeholderChoice.value,
label: placeholderChoice.label,
choiceId: placeholderChoice.id,
groupId: placeholderChoice.groupId,
placeholder: placeholderChoice.placeholder,
});
this._triggerChange(placeholderChoice.value);
}
}
_handleButtonAction(activeItems, element) {
if (
!activeItems ||
!element ||
!this.config.removeItems ||
!this.config.removeItemButton
) {
return;
}
const itemId = element.parentNode.getAttribute('data-id');
const itemToRemove = activeItems.find(
item => item.id === parseInt(itemId, 10),
);
// Remove item associated with button
this._removeItem(itemToRemove);
this._triggerChange(itemToRemove.value);
if (this._isSelectOneElement) {
this._selectPlaceholderChoice();
}
}
_handleItemAction(activeItems, element, hasShiftKey = false) {
if (
!activeItems ||
!element ||
!this.config.removeItems ||
this._isSelectOneElement
) {
return;
}
const 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(item => {
if (item.id === parseInt(passedId, 10) && !item.highlighted) {
this.highlightItem(item);
} else if (!hasShiftKey && item.highlighted) {
this.unhighlightItem(item);
}
});
// Focus input as without focus, a user cannot do anything with a
// highlighted item
this.input.focus();
}
_handleChoiceAction(activeItems, element) {
if (!activeItems || !element) {
return;
}
// If we are clicking on an option
const id = element.getAttribute('data-id');
const choice = this._store.getChoiceById(id);
const passedKeyCode =
activeItems[0] && activeItems[0].keyCode ? activeItems[0].keyCode : null;
const hasActiveDropdown = this.dropdown.isActive;
// Update choice keyCode
choice.keyCode = passedKeyCode;
this.passedElement.triggerEvent(EVENTS.choice, {
choice,
});
if (choice && !choice.selected && !choice.disabled) {
const canAddItem = this._canAddItem(activeItems, choice.value);
if (canAddItem.response) {
this._addItem({
value: choice.value,
label: choice.label,
choiceId: choice.id,
groupId: choice.groupId,
customProperties: choice.customProperties,
placeholder: choice.placeholder,
keyCode: 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(true);
this.containerOuter.focus();
}
}
_handleBackspace(activeItems) {
if (!this.config.removeItems || !activeItems) {
return;
}
const lastItem = activeItems[activeItems.length - 1];
const hasHighlightedItems = activeItems.some(item => 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.input.setWidth();
this._removeItem(lastItem);
this._triggerChange(lastItem.value);
} else {
if (!hasHighlightedItems) {
// Highlight last item if none already highlighted
this.highlightItem(lastItem, false);
}
this.removeHighlightedItems(true);
}
}
_setLoading(isLoading) {
this._store.dispatch(setIsLoading(isLoading));
}
_handleLoadingState(setLoading = true) {
let placeholderItem = this.itemList.getChild(
`.${this.config.classNames.placeholder}`,
);
if (setLoading) {
this.disable();
this.containerOuter.addLoadingState();
if (this._isSelectOneElement) {
if (!placeholderItem) {
placeholderItem = this._getTemplate(
'placeholder',
this.config.loadingText,
);
this.itemList.append(placeholderItem);
} else {
placeholderItem.innerHTML = this.config.loadingText;
}
} else {
this.input.placeholder = this.config.loadingText;
}
} else {
this.enable();
this.containerOuter.removeLoadingState();
if (this._isSelectOneElement) {
placeholderItem.innerHTML = this._placeholderValue || '';
} else {
this.input.placeholder = this._placeholderValue || '';
}
}
}
_handleSearch(value) {
if (!value || !this.input.isFocussed) {
return;
}
const choices = this._store.choices;
const { searchFloor, searchChoices } = this.config;
const hasUnactiveChoices = choices.some(option => !option.active);
// Check that we have a value to search and the input was an alphanumeric character
if (value && value.length >= searchFloor) {
const resultCount = searchChoices ? this._searchChoices(value) : 0;
// Trigger search event
this.passedElement.triggerEvent(EVENTS.search, {
value,
resultCount,
});
} else if (hasUnactiveChoices) {
// Otherwise reset choices to active
this._isSearching = false;
this._store.dispatch(activateChoices(true));
}
}
_canAddItem(activeItems, value) {
let canAddItem = true;
let notice = isType('Function', this.config.addItemText)
? this.config.addItemText(value)
: this.config.addItemText;
if (!this._isSelectOneElement) {
const isDuplicateValue = existsInArray(activeItems, value);
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 = isType('Function', this.config.maxItemText)
? this.config.maxItemText(this.config.maxItemCount)
: this.config.maxItemText;
}
if (
!this.config.duplicateItemsAllowed &&
isDuplicateValue &&
canAddItem
) {
canAddItem = false;
notice = isType('Function', this.config.uniqueItemText)
? this.config.uniqueItemText(value)
: this.config.uniqueItemText;
}
if (
this._isTextElement &&
this.config.addItems &&
canAddItem &&
isType('Function', this.config.addItemFilterFn) &&
!this.config.addItemFilterFn(value)
) {
canAddItem = false;
notice = isType('Function', this.config.customAddItemText)
? this.config.customAddItemText(value)
: this.config.customAddItemText;
}
}
return {
response: canAddItem,
notice,
};
}
_ajaxCallback() {
return (results, value, label) => {
if (!results || !value) {
return;
}
const parsedResults = isType('Object', results) ? [results] : results;
if (
parsedResults &&
isType('Array', parsedResults) &&
parsedResults.length
) {
// Remove loading states/text
this._handleLoadingState(false);
this._setLoading(true);
// Add each result as a choice
parsedResults.forEach(result => {
if (result.choices) {
this._addGroup({
group: result,
id: result.id || null,
valueKey: value,
labelKey: label,
});
} else {
this._addChoice({
value: fetchFromObject(result, value),
label: fetchFromObject(result, label),
isSelected: result.selected,
isDisabled: result.disabled,
customProperties: result.customProperties,
placeholder: result.placeholder,
});
}
});
this._setLoading(false);
if (this._isSelectOneElement) {
this._selectPlaceholderChoice();
}
} else {
// No results, remove loading state
this._handleLoadingState(false);
}
};
}
_searchChoices(value) {
const newValue = isType('String', value) ? value.trim() : value;
const currentValue = isType('String', this._currentValue)
? this._currentValue.trim()
: this._currentValue;
if (newValue.length < 1 && newValue === `${currentValue} `) {
return 0;
}
// If new value matches the desired length and is not the same as the current value with a space
const haystack = this._store.searchableChoices;
const needle = newValue;
const keys = [...this.config.searchFields];
const options = Object.assign(this.config.fuseOptions, { keys });
const fuse = new Fuse(haystack, options);
const results = fuse.search(needle);
this._currentValue = newValue;
this._highlightPosition = 0;
this._isSearching = true;
this._store.dispatch(filterChoices(results));
return results.length;
}
_addEventListeners() {
window.delegateEvent.add('keyup', this._onKeyUp);
window.delegateEvent.add('keydown', this._onKeyDown);
window.delegateEvent.add('click', this._onClick);
window.delegateEvent.add('touchmove', this._onTouchMove);
window.delegateEvent.add('touchend', this._onTouchEnd);
window.delegateEvent.add('mousedown', this._onMouseDown);
window.delegateEvent.add('mouseover', this._onMouseOver);
if (this._isSelectOneElement) {
this.containerOuter.element.addEventListener('focus', this._onFocus);
this.containerOuter.element.addEventListener('blur', this._onBlur);
}
this.input.element.addEventListener('focus', this._onFocus);
this.input.element.addEventListener('blur', this._onBlur);
if (this.input.element.form) {
this.input.element.form.addEventListener('reset', this._onFormReset);
}
this.input.addEventListeners();
}
_removeEventListeners() {
window.delegateEvent.remove('keyup', this._onKeyUp);
window.delegateEvent.remove('keydown', this._onKeyDown);
window.delegateEvent.remove('click', this._onClick);
window.delegateEvent.remove('touchmove', this._onTouchMove);
window.delegateEvent.remove('touchend', this._onTouchEnd);
window.delegateEvent.remove('mousedown', this._onMouseDown);
window.delegateEvent.remove('mouseover', this._onMouseOver);
if (this._isSelectOneElement) {
this.containerOuter.element.removeEventListener('focus', this._onFocus);
this.containerOuter.element.removeEventListener('blur', this._onBlur);
}
this.input.element.removeEventListener('focus', this._onFocus);
this.input.element.removeEventListener('blur', this._onBlur);
if (this.input.element.form) {
this.input.element.form.removeEventListener('reset', this._onFormReset);
}
this.input.removeEventListeners();
}
_onKeyDown(event) {
const { target, keyCode, ctrlKey, metaKey } = event;
if (
target !== this.input.element &&
!this.containerOuter.element.contains(target)
) {
return;
}
const activeItems = this._store.activeItems;
const hasFocusedInput = this.input.isFocussed;
const hasActiveDropdown = this.dropdown.isActive;
const hasItems = this.itemList.hasChildren;
const keyString = String.fromCharCode(keyCode);
const {
BACK_KEY,
DELETE_KEY,
ENTER_KEY,
A_KEY,
ESC_KEY,
UP_KEY,
DOWN_KEY,
PAGE_UP_KEY,
PAGE_DOWN_KEY,
} = KEY_CODES;
const hasCtrlDownKeyPressed = ctrlKey || metaKey;
// If a user is typing and the dropdown is not active
if (!this._isTextElement && /[a-zA-Z0-9-_ ]/.test(keyString)) {
this.showDropdown();
}
// Map keys to key actions
const keyDownActions = {
[A_KEY]: this._onAKey,
[ENTER_KEY]: this._onEnterKey,
[ESC_KEY]: this._onEscapeKey,
[UP_KEY]: this._onDirectionKey,
[PAGE_UP_KEY]: this._onDirectionKey,
[DOWN_KEY]: this._onDirectionKey,
[PAGE_DOWN_KEY]: this._onDirectionKey,
[DELETE_KEY]: this._onDeleteKey,
[BACK_KEY]: this._onDeleteKey,
};
// If keycode has a function, run it
if (keyDownActions[keyCode]) {
keyDownActions[keyCode]({
event,
target,
keyCode,
metaKey,
activeItems,
hasFocusedInput,
hasActiveDropdown,
hasItems,
hasCtrlDownKeyPressed,
});
}
}
_onKeyUp({ target, keyCode }) {
if (target !== this.input.element) {
return;
}
const value = this.input.value;
const activeItems = this._store.activeItems;
const canAddItem = this._canAddItem(activeItems, value);
const { BACK_KEY: backKey, DELETE_KEY: deleteKey } = KEY_CODES;
// We are typing into a text input and have a value, we want to show a dropdown
// notice. Otherwise hide the dropdown
if (this._isTextElement) {
const canShowDropdownNotice = canAddItem.notice && value;
if (canShowDropdownNotice) {
const dropdownItem = this._getTemplate('notice', canAddItem.notice);
this.dropdown.element.innerHTML = dropdownItem.outerHTML;
this.showDropdown(true);
} else {
this.hideDropdown(true);
}
} else {
const userHasRemovedValue =
(keyCode === backKey || keyCode === deleteKey) && !target.value;
const canReactivateChoices = !this._isTextElement && this._isSearching;
const canSearch = this._canSearch && canAddItem.response;
if (userHasRemovedValue && canReactivateChoices) {
this._isSearching = false;
this._store.dispatch(activateChoices(true));
} else if (canSearch) {
this._handleSearch(this.input.value);
}
}
this._canSearch = this.config.searchEnabled;
}
_onAKey({ hasItems, hasCtrlDownKeyPressed }) {
// If CTRL + A or CMD + A have been pressed and there are items to select
if (hasCtrlDownKeyPressed && hasItems) {
this._canSearch = false;
const shouldHightlightAll =
this.config.removeItems &&
!this.input.value &&
this.input.element === document.activeElement;
if (shouldHightlightAll) {
this.highlightAll();
}
}
}
_onEnterKey({ event, target, activeItems, hasActiveDropdown }) {
const { ENTER_KEY: enterKey } = KEY_CODES;
const targetWasButton = target.hasAttribute('data-button');
if (this._isTextElement && target.value) {
const value = this.input.value;
const canAddItem = this._canAddItem(activeItems, value);
if (canAddItem.response) {
this.hideDropdown(true);
this._addItem({ value });
this._triggerChange(value);
this.clearInput();
}
}
if (targetWasButton) {
this._handleButtonAction(activeItems, target);
event.preventDefault();
}
if (hasActiveDropdown) {
const highlightedChoice = this.dropdown.getChild(
`.${this.config.classNames.highlightedState}`,
);
if (highlightedChoice) {
// add enter keyCode value
if (activeItems[0]) {
activeItems[0].keyCode = enterKey; // eslint-disable-line no-param-reassign
}
this._handleChoiceAction(activeItems, highlightedChoice);
}
event.preventDefault();
} else if (this._isSelectOneElement) {
this.showDropdown();
event.preventDefault();
}
}
_onEscapeKey({ hasActiveDropdown }) {
if (hasActiveDropdown) {
this.hideDropdown(true);
this.containerOuter.focus();
}
}
_onDirectionKey({ event, hasActiveDropdown, keyCode, metaKey }) {
const {
DOWN_KEY: downKey,
PAGE_UP_KEY: pageUpKey,
PAGE_DOWN_KEY: pageDownKey,
} = KEY_CODES;
// If up or down key is pressed, traverse through options
if (hasActiveDropdown || this._isSelectOneElement) {
this.showDropdown();
this._canSearch = false;
const directionInt =
keyCode === downKey || keyCode === pageDownKey ? 1 : -1;
const skipKey =
metaKey || keyCode === pageDownKey || keyCode === pageUpKey;
const selectableChoiceIdentifier = '[data-choice-selectable]';
let nextEl;
if (skipKey) {
if (directionInt > 0) {
nextEl = Array.from(
this.dropdown.element.querySelectorAll(selectableChoiceIdentifier),
).pop();
} else {
nextEl = this.dropdown.element.querySelector(
selectableChoiceIdentifier,
);
}
} else {
const currentEl = this.dropdown.element.querySelector(
`.${this.config.classNames.highlightedState}`,
);
if (currentEl) {
nextEl = getAdjacentEl(
currentEl,
selectableChoiceIdentifier,
directionInt,
);
} else {
nextEl = this.dropdown.element.querySelector(
selectableChoiceIdentifier,
);
}
}
if (nextEl) {
// We prevent default to stop the cursor moving
// when pressing the arrow
if (
!isScrolledIntoView(nextEl, this.choiceList.element, directionInt)
) {
this.choiceList.scrollToChoice(nextEl, directionInt);
}
this._highlightChoice(nextEl);
}
// Prevent default to maintain cursor position whilst
// traversing dropdown options
event.preventDefault();
}
}
_onDeleteKey({ event, target, hasFocusedInput, activeItems }) {
// If backspace or delete key is pressed and the input has no value
if (hasFocusedInput && !target.value && !this._isSelectOneElement) {
this._handleBackspace(activeItems);
event.preventDefault();
}
}
_onTouchMove() {
if (this._wasTap) {
this._wasTap = false;
}
}
_onTouchEnd(event) {
const { target } = event || event.touches[0];
const touchWasWithinContainer =
this._wasTap && this.containerOuter.element.contains(target);
if (touchWasWithinContainer) {
const containerWasExactTarget =
target === this.containerOuter.element ||
target === this.containerInner.element;
if (containerWasExactTarget) {
if (this._isTextElement) {
this.input.focus();
} else if (this._isSelectMultipleElement) {
this.showDropdown();
}
}
// Prevents focus event firing
event.stopPropagation();
}
this._wasTap = true;
}
_onMouseDown(event) {
const { target, shiftKey } = event;
// If we have our mouse down on the scrollbar and are on IE11...
if (this.choiceList.element.contains(target) && isIE11()) {
this._isScrollingOnIe = true;
}
if (
!this.containerOuter.element.contains(target) ||
target === this.input.element
) {
return;
}
const activeItems = this._store.activeItems;
const hasShiftKey = shiftKey;
const buttonTarget = findAncestorByAttrName(target, 'data-button');
const itemTarget = findAncestorByAttrName(target, 'data-item');
const choiceTarget = findAncestorByAttrName(target, 'data-choice');
if (buttonTarget) {
this._handleButtonAction(activeItems, buttonTarget);
} else if (itemTarget) {
this._handleItemAction(activeItems, itemTarget, hasShiftKey);
} else if (choiceTarget) {
this._handleChoiceAction(activeItems, choiceTarget);
}
event.preventDefault();
}
_onMouseOver({ target }) {
const targetWithinDropdown =
target === this.dropdown || this.dropdown.element.contains(target);
const shouldHighlightChoice =
targetWithinDropdown && target.hasAttribute('data-choice');
if (shouldHighlightChoice) {
this._highlightChoice(target);
}
}
_onClick({ target }) {
const clickWasWithinContainer = this.containerOuter.element.contains(
target,
);
if (clickWasWithinContainer) {
if (!this.dropdown.isActive && !this.containerOuter.isDisabled) {
if (this._isTextElement) {
if (document.activeElement !== this.input.element) {
this.input.focus();
}
} else {
this.showDropdown();
this.containerOuter.focus();
}
} else if (
this._isSelectOneElement &&
target !== this.input.element &&
!this.dropdown.element.contains(target)
) {
this.hideDropdown();
}
} else {
const hasHighlightedItems = this._store.highlightedActiveItems.length > 0;
if (hasHighlightedItems) {
this.unhighlightAll();
}
this.containerOuter.removeFocusState();
this.hideDropdown(true);
}
}
_onFocus({ target }) {
const focusWasWithinContainer = this.containerOuter.element.contains(
target,
);
if (!focusWasWithinContainer) {
return;
}
const focusActions = {
text: () => {
if (target === this.input.element) {
this.containerOuter.addFocusState();
}
},
'select-one': () => {
this.containerOuter.addFocusState();
if (target === this.input.element) {
this.showDropdown(true);
}
},
'select-multiple': () => {
if (target === this.input.element) {
this.showDropdown(true);
// If element is a select box, the focused element is the container and the dropdown
// isn't already open, focus and show dropdown
this.containerOuter.addFocusState();
}
},
};
focusActions[this.passedElement.element.type]();
}
_onBlur({ target }) {
const blurWasWithinContainer = this.containerOuter.element.contains(target);
if (blurWasWithinContainer && !this._isScrollingOnIe) {
const activeItems = this._store.activeItems;
const hasHighlightedItems = activeItems.some(item => item.highlighted);
const blurActions = {
text: () => {
if (target === this.input.element) {
this.containerOuter.removeFocusState();
if (hasHighlightedItems) {
this.unhighlightAll();
}
this.hideDropdown(true);
}
},
'select-one': () => {
this.containerOuter.removeFocusState();
if (
target === this.input.element ||
(target === this.containerOuter.element && !this._canSearch)
) {
this.hideDropdown(true);
}
},
'select-multiple': () => {
if (target === this.input.element) {
this.containerOuter.removeFocusState();
this.hideDropdown(true);
if (hasHighlightedItems) {
this.unhighlightAll();
}
}
},
};
blurActions[this.passedElement.element.type]();
} else {
// On IE11, clicking the scollbar blurs our input and thus
// closes the dropdown. To stop this, we refocus our input
// if we know we are on IE *and* are scrolling.
this._isScrollingOnIe = false;
this.input.element.focus();
}
}
_onFormReset() {
this._store.dispatch(resetTo(this._initialState));
}
_highlightChoice(el = null) {
const choices = Array.from(
this.dropdown.element.querySelectorAll('[data-choice-selectable]'),
);
if (!choices.length) {
return;
}
let passedEl = el;
const highlightedChoices = Array.from(
this.dropdown.element.querySelectorAll(
`.${this.config.classNames.highlightedState}`,
),
);
// Remove any highlighted choices
highlightedChoices.forEach(choice => {
choice.classList.remove(this.config.classNames.highlightedState);
choice.setAttribute('aria-selected', 'false');
});
if (passedEl) {
this._highlightPosition = choices.indexOf(passedEl);
} else {
// Highlight choice based on last known highlight location
if (choices.length > this._highlightPosition) {
// If we have an option to highlight
passedEl = choices[this._highlightPosition];
} else {
// Otherwise highlight the option before
passedEl = choices[choices.length - 1];
}
if (!passedEl) {
passedEl = choices[0];
}
}
passedEl.classList.add(this.config.classNames.highlightedState);
passedEl.setAttribute('aria-selected', 'true');
this.passedElement.triggerEvent(EVENTS.highlightChoice, { el: passedEl });
if (this.dropdown.isActive) {
// IE11 ignores aria-label and blocks virtual keyboard
// if aria-activedescendant is set without a dropdown
this.input.setActiveDescendant(passedEl.id);
this.containerOuter.setActiveDescendant(passedEl.id);
}
}
_addItem({
value,
label = null,
choiceId = -1,
groupId = -1,
customProperties = null,
placeholder = false,
keyCode = null,
}) {
let passedValue = isType('String', value) ? value.trim() : value;
const passedKeyCode = keyCode;
const passedCustomProperties = customProperties;
const items = this._store.items;
const passedLabel = label || passedValue;
const passedOptionId = parseInt(choiceId, 10) || -1;
const group = groupId >= 0 ? this._store.getGroupById(groupId) : null;
const id = items ? items.length + 1 : 1;
// If a prepended value has been passed, prepend it
if (this.config.prependValue) {
passedValue = this.config.prependValue + passedValue.toString();
}
// If an appended value has been passed, append it
if (this.config.appendValue) {
passedValue += this.config.appendValue.toString();
}
this._store.dispatch(
addItem({
value: passedValue,
label: passedLabel,
id,
choiceId: passedOptionId,
groupId,
customProperties,
placeholder,
keyCode: passedKeyCode,
}),
);
if (this._isSelectOneElement) {
this.removeActiveItems(id);
}
// Trigger change event
this.passedElement.triggerEvent(EVENTS.addItem, {
id,
value: passedValue,
label: passedLabel,
customProperties: passedCustomProperties,
groupValue: group && group.value ? group.value : undefined,
keyCode: passedKeyCode,
});
return this;
}
_removeItem(item) {
if (!item || !isType('Object', item)) {
return this;
}
const { id, value, label, choiceId, groupId } = item;
const group = groupId >= 0 ? this._store.getGroupById(groupId) : null;
this._store.dispatch(removeItem(id, choiceId));
if (group && group.value) {
this.passedElement.triggerEvent(EVENTS.removeItem, {
id,
value,
label,
groupValue: group.value,
});
} else {
this.passedElement.triggerEvent(EVENTS.removeItem, {
id,
value,
label,
});
}
return this;
}
_addChoice({
value,
label = null,
isSelected = false,
isDisabled = false,
groupId = -1,
customProperties = null,
placeholder = false,
keyCode = null,
}) {
if (typeof value === 'undefined' || value === null) {
return;
}
// Generate unique id
const choices = this._store.choices;
const choiceLabel = label || value;
const choiceId = choices ? choices.length + 1 : 1;
const choiceElementId = `${this._baseId}-${
this._idNames.itemChoice
}-${choiceId}`;
this._store.dispatch(
addChoice({
value,
label: choiceLabel,
id: choiceId,
groupId,
disabled: isDisabled,
elementId: choiceElementId,
customProperties,
placeholder,
keyCode,
}),
);
if (isSelected) {
this._addItem({
value,
label: choiceLabel,
choiceId,
customProperties,
placeholder,
keyCode,
});
}
}
_addGroup({ group, id, valueKey = 'value', labelKey = 'label' }) {
const groupChoices = isType('Object', group)
? group.choices
: Array.from(group.getElementsByTagName('OPTION'));
const groupId = id || Math.floor(new Date().valueOf() * Math.random());
const isDisabled = group.disabled ? group.disabled : false;
if (groupChoices) {
this._store.dispatch(addGroup(group.label, groupId, true, isDisabled));
const addGroupChoices = choice => {
const isOptDisabled =
choice.disabled || (choice.parentNode && choice.parentNode.disabled);
this._addChoice({
value: choice[valueKey],
label: isType('Object', choice) ? choice[labelKey] : choice.innerHTML,
isSelected: choice.selected,
isDisabled: isOptDisabled,
groupId,
customProperties: choice.customProperties,
placeholder: choice.placeholder,
});
};
groupChoices.forEach(addGroupChoices);
} else {
this._store.dispatch(
addGroup(group.label, group.id, false, group.disabled),
);
}
}
_getTemplate(template, ...args) {
if (!template) {
return null;
}
const { templates, classNames } = this.config;
return templates[template].call(this, classNames, ...args);
}
_createTemplates() {
const { callbackOnCreateTemplates } = this.config;
let userTemplates = {};
if (
callbackOnCreateTemplates &&
isType('Function', callbackOnCreateTemplates)
) {
userTemplates = callbackOnCreateTemplates.call(this, strToEl);
}
this.config.templates = merge(TEMPLATES, userTemplates);
}
_createElements() {
this.containerOuter = new Container({
element: this._getTemplate(
'containerOuter',
this._direction,
this._isSelectElement,
this._isSelectOneElement,
this.config.searchEnabled,
this.passedElement.element.type,
),
classNames: this.config.classNames,
type: this.passedElement.element.type,
position: this.config.position,
});
this.containerInner = new Container({
element: this._getTemplate('containerInner'),
classNames: this.config.classNames,
type: this.passedElement.element.type,
position: this.config.position,
});
this.input = new Input({
element: this._getTemplate('input', this._placeholderValue),
classNames: this.config.classNames,
type: this.passedElement.element.type,
});
this.choiceList = new List({
element: this._getTemplate('choiceList', this._isSelectOneElement),
});
this.itemList = new List({
element: this._getTemplate('itemList', this._isSelectOneElement),
});
this.dropdown = new Dropdown({
element: this._getTemplate('dropdown'),
classNames: this.config.classNames,
type: this.passedElement.element.type,
});
}
_createStructure() {
// Hide original element
this.passedElement.conceal();
// Wrap input in container preserving DOM ordering
this.containerInner.wrap(this.passedElement.element);
// Wrapper inner container with outer container
this.containerOuter.wrap(this.containerInner.element);
if (this._isSelectOneElement) {
this.input.placeholder = this.config.searchPlaceholderValue || '';
} else if (this._placeholderValue) {
this.input.placeholder = this._placeholderValue;
this.input.setWidth(true);
}
this.containerOuter.element.appendChild(this.containerInner.element);
this.containerOuter.element.appendChild(this.dropdown.element);
this.containerInner.element.appendChild(this.itemList.element);
if (!this._isTextElement) {
this.dropdown.element.appendChild(this.choiceList.element);
}
if (!this._isSelectOneElement) {
this.containerInner.element.appendChild(this.input.element);
} else if (this.config.searchEnabled) {
this.dropdown.element.insertBefore(
this.input.element,
this.dropdown.element.firstChild,
);
}
if (this._isSelectElement) {
this._addPredefinedChoices();
} else if (this._isTextElement) {
this._addPredefinedItems();
}
}
_addPredefinedChoices() {
const passedGroups = this.passedElement.optionGroups;
this._highlightPosition = 0;
this._isSearching = false;
this._setLoading(true);
if (passedGroups && passedGroups.length) {
// If we have a placeholder option
const placeholderChoice = this.passedElement.placeholderOption;
if (
placeholderChoice &&
placeholderChoice.parentNode.tagName === 'SELECT'
) {
this._addChoice({
value: placeholderChoice.value,
label: placeholderChoice.innerHTML,
isSelected: placeholderChoice.selected,
isDisabled: placeholderChoice.disabled,
placeholder: true,
});
}
passedGroups.forEach(group =>
this._addGroup({
group,
id: group.id || null,
}),
);
} else {
const passedOptions = this.passedElement.options;
const filter = this.config.sortFn;
const allChoices = this._presetChoices;
// Create array of options from option elements
passedOptions.forEach(o => {
allChoices.push({
value: o.value,
label: o.innerHTML,
selected: o.selected,
disabled: o.disabled || o.parentNode.disabled,
placeholder: o.hasAttribute('placeholder'),
customProperties: o.getAttribute('data-custom-properties'),
});
});
// If sorting is enabled or the user is searching, filter choices
if (this.config.shouldSort) allChoices.sort(filter);
// Determine whether there is a selected choice
const hasSelectedChoice = allChoices.some(choice => choice.selected);
const handleChoice = (choice, index) => {
const { value, label, customProperties, placeholder } = choice;
if (this._isSelectElement) {
// If the choice is actually a group
if (choice.choices) {
this._addGroup({
group: choice,
id: choice.id || null,
});
} else {
// If there is a selected choice already or the choice is not
// the first in the array, add each choice normally
// Otherwise pre-select the first choice in the array if it's a single select
const shouldPreselect =
this._isSelectOneElement && !hasSelectedChoice && index === 0;
const isSelected = shouldPreselect ? true : choice.selected;
const isDisabled = shouldPreselect ? false : choice.disabled;
this._addChoice({
value,
label,
isSelected,
isDisabled,
customProperties,
placeholder,
});
}
} else {
this._addChoice({
value,
label,
isSelected: choice.selected,
isDisabled: choice.disabled,
customProperties,
placeholder,
});
}
};
// Add each choice
allChoices.forEach((choice, index) => handleChoice(choice, index));
}
this._setLoading(false);
}
_addPredefinedItems() {
const handlePresetItem = item => {
const itemType = getType(item);
if (itemType === 'Object' && item.value) {
this._addItem({
value: item.value,
label: item.label,
choiceId: item.id,
customProperties: item.customProperties,
placeholder: item.placeholder,
});
} else if (itemType === 'String') {
this._addItem({
value: item,
});
}
};
this._presetItems.forEach(item => handlePresetItem(item));
}
_setChoiceOrItem(item) {
const itemType = getType(item).toLowerCase();
const handleType = {
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 (!this._isTextElement) {
this._addChoice({
value: item.value,
label: item.label,
isSelected: true,
isDisabled: false,
customProperties: item.customProperties,
placeholder: item.placeholder,
});
} else {
this._addItem({
value: item.value,
label: item.label,
choiceId: item.id,
customProperties: item.customProperties,
placeholder: item.placeholder,
});
}
},
string: () => {
if (!this._isTextElement) {
this._addChoice({
value: item,
label: item,
isSelected: true,
isDisabled: false,
});
} else {
this._addItem({
value: item,
});
}
},
};
handleType[itemType]();
}
_findAndSelectChoiceByValue(val) {
const choices = this._store.choices;
// Check 'value' property exists and the choice isn't already selected
const foundChoice = choices.find(choice =>
this.config.itemComparer(choice.value, val),
);
if (foundChoice && !foundChoice.selected) {
this._addItem({
value: foundChoice.value,
label: foundChoice.label,
choiceId: foundChoice.id,
groupId: foundChoice.groupId,
customProperties: foundChoice.customProperties,
placeholder: foundChoice.placeholder,
keyCode: foundChoice.keyCode,
});
}
}
_generateInstances(elements, config) {
return elements.reduce(
(instances, element) => {
instances.push(new Choices(element, config));
return instances;
},
[this],
);
}
_generatePlaceholderValue() {
if (this._isSelectOneElement) {
return false;
}
return this.config.placeholder
? this.config.placeholderValue ||
this.passedElement.element.getAttribute('placeholder')
: false;
}
/* ===== End of Private functions ====== */
}
Choices.userDefaults = {};
// We cannot export default here due to Webpack: https://github.com/webpack/webpack/issues/3929
module.exports = Choices;