mirror of
https://github.com/Choices-js/Choices.git
synced 2024-06-08 00:42:15 +02:00
Break out public functions into more private methods + housekeeping
This commit is contained in:
parent
d3a18e255b
commit
a591c32a24
|
@ -17,7 +17,6 @@ import {
|
|||
getAdjacentEl,
|
||||
getType,
|
||||
isType,
|
||||
isElement,
|
||||
strToEl,
|
||||
extend,
|
||||
sortByAlpha,
|
||||
|
@ -25,6 +24,7 @@ import {
|
|||
generateId,
|
||||
findAncestorByAttrName,
|
||||
regexFilter,
|
||||
isIE11,
|
||||
} from './lib/utils';
|
||||
import './lib/polyfills';
|
||||
|
||||
|
@ -33,29 +33,17 @@ import './lib/polyfills';
|
|||
*/
|
||||
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 (isType('String', element)) {
|
||||
const elements = document.querySelectorAll(element);
|
||||
if (elements.length > 1) {
|
||||
for (let i = 1; i < elements.length; i += 1) {
|
||||
const el = elements[i];
|
||||
/* eslint-disable no-new */
|
||||
new Choices(el, userConfig);
|
||||
}
|
||||
return this._generateInstances(elements, userConfig);
|
||||
}
|
||||
}
|
||||
|
||||
const defaultConfig = {
|
||||
...DEFAULT_CONFIG,
|
||||
items: [],
|
||||
choices: [],
|
||||
classNames: DEFAULT_CLASSNAMES,
|
||||
sortFn: sortByAlpha,
|
||||
};
|
||||
|
||||
// Merge options with user options
|
||||
this.config = extend(defaultConfig, Choices.userDefaults, userConfig);
|
||||
this.config = Choices._generateConfig(userConfig);
|
||||
|
||||
if (!['auto', 'always'].includes(this.config.renderSelectedChoices)) {
|
||||
this.config.renderSelectedChoices = 'auto';
|
||||
|
@ -69,6 +57,8 @@ class Choices {
|
|||
this.currentState = {};
|
||||
this.prevState = {};
|
||||
this.currentValue = '';
|
||||
this.isScrollingOnIe = false;
|
||||
this.wasTap = true;
|
||||
|
||||
// Retrieve triggering element (i.e. element with 'data-choice' trigger)
|
||||
const passedElement = isType('String', element) ? document.querySelector(element) : element;
|
||||
|
@ -77,7 +67,6 @@ class Choices {
|
|||
this.isSelectOneElement = passedElement.type === 'select-one';
|
||||
this.isSelectMultipleElement = passedElement.type === 'select-multiple';
|
||||
this.isSelectElement = this.isSelectOneElement || this.isSelectMultipleElement;
|
||||
this.isValidElementType = this.isTextElement || this.isSelectElement;
|
||||
|
||||
if (this.isTextElement) {
|
||||
this.passedElement = new WrappedInput({
|
||||
|
@ -93,32 +82,15 @@ class Choices {
|
|||
}
|
||||
|
||||
if (!this.passedElement) {
|
||||
if (!this.config.silent) {
|
||||
console.error('Passed element not found');
|
||||
}
|
||||
return false;
|
||||
throw new Error('Could not wrap passed element');
|
||||
}
|
||||
|
||||
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.',
|
||||
);
|
||||
}
|
||||
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.highlightPosition = 0;
|
||||
this.canSearch = this.config.searchEnabled;
|
||||
|
||||
this.placeholderValue = false;
|
||||
if (!this.isSelectOneElement) {
|
||||
this.placeholderValue = this.config.placeholder ?
|
||||
(this.config.placeholderValue || this.passedElement.element.getAttribute('placeholder')) :
|
||||
false;
|
||||
}
|
||||
this.placeholderValue = this._generatePlaceholderValue();
|
||||
|
||||
// Assign preset choices from passed object
|
||||
this.presetChoices = this.config.choices;
|
||||
|
@ -139,10 +111,7 @@ class Choices {
|
|||
itemChoice: 'item-choice',
|
||||
};
|
||||
|
||||
// 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);
|
||||
|
@ -153,28 +122,13 @@ class Choices {
|
|||
this._onMouseDown = this._onMouseDown.bind(this);
|
||||
this._onMouseOver = this._onMouseOver.bind(this);
|
||||
|
||||
// Monitor touch taps/scrolls
|
||||
this.wasTap = true;
|
||||
|
||||
// Cutting the mustard
|
||||
const cuttingTheMustard = 'classList' in document.documentElement;
|
||||
if (!cuttingTheMustard && !this.config.silent) {
|
||||
console.error('Choices: Your browser doesn\'t support Choices');
|
||||
}
|
||||
|
||||
const canInit = isElement(this.passedElement.element) && this.isValidElementType;
|
||||
|
||||
if (canInit) {
|
||||
// If element has already been initialised with Choices
|
||||
// If element has already been initialised with Choices, fail silently
|
||||
if (this.passedElement.element.getAttribute('data-choice') === 'active') {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Let's go
|
||||
this.init();
|
||||
} else if (!this.config.silent) {
|
||||
console.error('Incompatible input passed');
|
||||
}
|
||||
}
|
||||
|
||||
/* ========================================
|
||||
|
@ -283,142 +237,6 @@ class Choices {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
createItemsFragment(items, fragment = null) {
|
||||
// Create fragment to add elements to
|
||||
const itemListFragment = fragment || document.createDocumentFragment();
|
||||
|
||||
// If sorting is enabled, filter items
|
||||
if (this.config.shouldSortItems && !this.isSelectOneElement) {
|
||||
items.sort(this.config.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, this.config.removeItemButton);
|
||||
// Append it to list
|
||||
itemListFragment.appendChild(listItem);
|
||||
};
|
||||
|
||||
// Add each list item to list
|
||||
items.forEach(item => addItemToFragment(item));
|
||||
|
||||
return itemListFragment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render DOM with values
|
||||
* @return
|
||||
|
@ -436,95 +254,12 @@ class Choices {
|
|||
return;
|
||||
}
|
||||
|
||||
/* Choices */
|
||||
|
||||
if (this.isSelectElement) {
|
||||
// Get active groups/choices
|
||||
const activeGroups = this.store.activeGroups;
|
||||
const activeChoices = this.store.activeChoices;
|
||||
|
||||
let choiceListFragment = document.createDocumentFragment();
|
||||
|
||||
// Clear choices
|
||||
this.choiceList.clear();
|
||||
|
||||
// Scroll back to top of choices list
|
||||
if (this.config.resetScrollPosition) {
|
||||
this.choiceList.scrollTo(0);
|
||||
this._renderChoices();
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
|
||||
/* Items */
|
||||
if (this.currentState.items !== this.prevState.items) {
|
||||
// Get active items (items that can be selected)
|
||||
const activeItems = this.store.activeItems || [];
|
||||
// Clear list
|
||||
this.itemList.clear();
|
||||
|
||||
if (activeItems.length) {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
this._renderItems();
|
||||
}
|
||||
|
||||
this.prevState = this.currentState;
|
||||
|
@ -590,8 +325,7 @@ class Choices {
|
|||
* @public
|
||||
*/
|
||||
highlightAll() {
|
||||
const items = this.store.items;
|
||||
items.forEach(item => this.highlightItem(item));
|
||||
this.store.items.forEach(item => this.highlightItem(item));
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -601,8 +335,7 @@ class Choices {
|
|||
* @public
|
||||
*/
|
||||
unhighlightAll() {
|
||||
const items = this.store.items;
|
||||
items.forEach(item => this.unhighlightItem(item));
|
||||
this.store.items.forEach(item => this.unhighlightItem(item));
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -610,6 +343,7 @@ class Choices {
|
|||
* Remove an item from the store by its value
|
||||
* @param {String} value Value to search for
|
||||
* @return {Object} Class instance
|
||||
* @todo Merge with removeActiveItems
|
||||
* @public
|
||||
*/
|
||||
removeActiveItemsByValue(value) {
|
||||
|
@ -617,13 +351,9 @@ class Choices {
|
|||
return this;
|
||||
}
|
||||
|
||||
const items = this.store.activeItems;
|
||||
|
||||
items.forEach((item) => {
|
||||
if (item.value === value) {
|
||||
this._removeItem(item);
|
||||
}
|
||||
});
|
||||
this.store.activeItems
|
||||
.filter(item => item.value === value)
|
||||
.forEach(item => this._removeItem(item));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
@ -636,13 +366,9 @@ class Choices {
|
|||
* @public
|
||||
*/
|
||||
removeActiveItems(excludedId) {
|
||||
const items = this.store.activeItems;
|
||||
|
||||
items.forEach((item) => {
|
||||
if (excludedId !== item.id) {
|
||||
this._removeItem(item);
|
||||
}
|
||||
});
|
||||
this.store.activeItems
|
||||
.filter(({ id }) => id !== excludedId)
|
||||
.forEach(item => this._removeItem(item));
|
||||
|
||||
return this;
|
||||
}
|
||||
|
@ -654,9 +380,8 @@ class Choices {
|
|||
* @public
|
||||
*/
|
||||
removeHighlightedItems(runEvent = false) {
|
||||
const items = this.store.highlightedActiveItems;
|
||||
|
||||
items.forEach((item) => {
|
||||
this.store.highlightedActiveItems
|
||||
.forEach((item) => {
|
||||
this._removeItem(item);
|
||||
// If this action was performed by the user
|
||||
// trigger the event
|
||||
|
@ -678,14 +403,16 @@ class Choices {
|
|||
return this;
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this.dropdown.show();
|
||||
this.containerOuter.open(this.dropdown.distanceFromTopWindow());
|
||||
|
||||
if (focusInput && this.canSearch) {
|
||||
if (focusInput && this.config.searchEnabled) {
|
||||
this.input.focus();
|
||||
}
|
||||
|
||||
this.passedElement.triggerEvent(EVENTS.showDropdown, {});
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
@ -700,15 +427,17 @@ class Choices {
|
|||
return this;
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this.dropdown.hide();
|
||||
this.containerOuter.close();
|
||||
|
||||
if (blurInput && this.canSearch) {
|
||||
if (blurInput && this.config.searchEnabled) {
|
||||
this.input.removeActiveDescendant();
|
||||
this.input.blur();
|
||||
}
|
||||
|
||||
this.passedElement.triggerEvent(EVENTS.hideDropdown, {});
|
||||
});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
@ -887,6 +616,142 @@ class Choices {
|
|||
= Private functions =
|
||||
============================================= */
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
_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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
_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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
_createItemsFragment(items, fragment = null) {
|
||||
// Create fragment to add elements to
|
||||
const itemListFragment = fragment || document.createDocumentFragment();
|
||||
|
||||
// If sorting is enabled, filter items
|
||||
if (this.config.shouldSortItems && !this.isSelectOneElement) {
|
||||
items.sort(this.config.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, this.config.removeItemButton);
|
||||
// Append it to list
|
||||
itemListFragment.appendChild(listItem);
|
||||
};
|
||||
|
||||
// Add each list item to list
|
||||
items.forEach(item => addItemToFragment(item));
|
||||
|
||||
return itemListFragment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call change callback
|
||||
* @param {String} value - last added/deleted/selected value
|
||||
|
@ -1352,12 +1217,12 @@ class Choices {
|
|||
this.showDropdown(true);
|
||||
}
|
||||
|
||||
this.canSearch = this.config.searchEnabled;
|
||||
this.config.searchEnabled = this.config.searchEnabled;
|
||||
|
||||
const onAKey = () => {
|
||||
// If CTRL + A or CMD + A have been pressed and there are items to select
|
||||
if (ctrlDownKey && hasItems) {
|
||||
this.canSearch = false;
|
||||
this.config.searchEnabled = false;
|
||||
if (
|
||||
this.config.removeItems &&
|
||||
!this.input.value &&
|
||||
|
@ -1421,7 +1286,7 @@ class Choices {
|
|||
// Show dropdown if focus
|
||||
this.showDropdown(true);
|
||||
|
||||
this.canSearch = false;
|
||||
this.config.searchEnabled = false;
|
||||
|
||||
const directionInt = e.keyCode === downKey || e.keyCode === pageDownKey ? 1 : -1;
|
||||
const skipKey = e.metaKey || e.keyCode === pageDownKey || e.keyCode === pageUpKey;
|
||||
|
@ -1531,12 +1396,12 @@ class Choices {
|
|||
this.isSearching = false;
|
||||
this.store.dispatch(activateChoices(true));
|
||||
}
|
||||
} else if (this.canSearch && canAddItem.response) {
|
||||
} else if (this.config.searchEnabled && canAddItem.response) {
|
||||
this._handleSearch(this.input.value);
|
||||
}
|
||||
}
|
||||
// Re-establish canSearch value from changes in _onKeyDown
|
||||
this.canSearch = this.config.searchEnabled;
|
||||
this.config.searchEnabled = this.config.searchEnabled;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1591,7 +1456,7 @@ class Choices {
|
|||
const target = e.target;
|
||||
|
||||
// If we have our mouse down on the scrollbar and are on IE11...
|
||||
if (target === this.choiceList && this.isIe11) {
|
||||
if (target === this.choiceList && isIE11()) {
|
||||
this.isScrollingOnIe = true;
|
||||
}
|
||||
|
||||
|
@ -1651,7 +1516,7 @@ class Choices {
|
|||
if (document.activeElement !== this.input.element) {
|
||||
this.input.focus();
|
||||
}
|
||||
} else if (this.canSearch) {
|
||||
} else if (this.config.searchEnabled) {
|
||||
this.showDropdown(true);
|
||||
} else {
|
||||
this.showDropdown();
|
||||
|
@ -1746,7 +1611,7 @@ class Choices {
|
|||
'select-one': () => {
|
||||
this.containerOuter.removeFocusState();
|
||||
if (target === this.input.element ||
|
||||
(target === this.containerOuter.element && !this.canSearch)) {
|
||||
(target === this.containerOuter.element && !this.config.searchEnabled)) {
|
||||
this.hideDropdown();
|
||||
}
|
||||
},
|
||||
|
@ -2242,7 +2107,7 @@ class Choices {
|
|||
|
||||
if (!this.isSelectOneElement) {
|
||||
this.containerInner.element.appendChild(this.input.element);
|
||||
} else if (this.canSearch) {
|
||||
} else if (this.config.searchEnabled) {
|
||||
this.dropdown.element.insertBefore(this.input.element, this.dropdown.element.firstChild);
|
||||
}
|
||||
|
||||
|
@ -2430,6 +2295,123 @@ class Choices {
|
|||
}
|
||||
}
|
||||
|
||||
_generateInstances(elements, config) {
|
||||
return elements.reduce((instances, element) => {
|
||||
instances.push(new Choices(element, config));
|
||||
return instances;
|
||||
}, [this]);
|
||||
}
|
||||
|
||||
static _generateConfig(userConfig) {
|
||||
const defaultConfig = {
|
||||
...DEFAULT_CONFIG,
|
||||
items: [],
|
||||
choices: [],
|
||||
classNames: DEFAULT_CLASSNAMES,
|
||||
sortFn: sortByAlpha,
|
||||
};
|
||||
|
||||
return extend(defaultConfig, Choices.userDefaults, userConfig);
|
||||
}
|
||||
|
||||
_generatePlaceholderValue() {
|
||||
if (!this.isSelectOneElement) {
|
||||
return this.config.placeholder ?
|
||||
(this.config.placeholderValue || this.passedElement.element.getAttribute('placeholder')) :
|
||||
false;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
_renderChoices() {
|
||||
// Get active groups/choices
|
||||
const activeGroups = this.store.activeGroups;
|
||||
const activeChoices = this.store.activeChoices;
|
||||
|
||||
let choiceListFragment = document.createDocumentFragment();
|
||||
|
||||
// Clear choices
|
||||
this.choiceList.clear();
|
||||
|
||||
// Scroll back to top of choices list
|
||||
if (this.config.resetScrollPosition) {
|
||||
this.choiceList.scrollTo(0);
|
||||
}
|
||||
|
||||
// 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() {
|
||||
// Get active items (items that can be selected)
|
||||
const activeItems = this.store.activeItems || [];
|
||||
// Clear list
|
||||
this.itemList.clear();
|
||||
|
||||
if (activeItems.length) {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* ===== End of Private functions ====== */
|
||||
}
|
||||
|
||||
|
|
|
@ -152,27 +152,33 @@ describe('choices', () => {
|
|||
});
|
||||
|
||||
it('removes event listeners', () => {
|
||||
requestAnimationFrame;
|
||||
expect(removeEventListenersSpy.called).to.equal(true);
|
||||
});
|
||||
|
||||
it('reveals passed element', () => {
|
||||
requestAnimationFrame;
|
||||
expect(passedElementRevealSpy.called).to.equal(true);
|
||||
});
|
||||
|
||||
it('reverts outer container', () => {
|
||||
requestAnimationFrame;
|
||||
expect(containerOuterUnwrapSpy.called).to.equal(true);
|
||||
expect(containerOuterUnwrapSpy.lastCall.args[0]).to.equal(instance.passedElement.element);
|
||||
});
|
||||
|
||||
it('clears store', () => {
|
||||
requestAnimationFrame;
|
||||
expect(clearStoreSpy.called).to.equal(true);
|
||||
});
|
||||
|
||||
it('nullifys templates config', () => {
|
||||
requestAnimationFrame;
|
||||
expect(instance.config.templates).to.equal(null);
|
||||
});
|
||||
|
||||
it('resets initialise flag', () => {
|
||||
requestAnimationFrame;
|
||||
expect(instance.initialised).to.equal(false);
|
||||
});
|
||||
});
|
||||
|
@ -374,18 +380,27 @@ describe('choices', () => {
|
|||
expect(output).to.eql(instance);
|
||||
});
|
||||
|
||||
it('opens containerOuter', () => {
|
||||
it('opens containerOuter', (done) => {
|
||||
requestAnimationFrame(() => {
|
||||
expect(containerOuterOpenSpy.called).to.equal(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('shows dropdown with blurInput flag', () => {
|
||||
it('shows dropdown with blurInput flag', (done) => {
|
||||
requestAnimationFrame(() => {
|
||||
expect(dropdownShowSpy.called).to.equal(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('triggers event on passedElement', () => {
|
||||
it('triggers event on passedElement', (done) => {
|
||||
requestAnimationFrame(() => {
|
||||
expect(passedElementTriggerEventStub.called).to.equal(true);
|
||||
expect(passedElementTriggerEventStub.lastCall.args[0]).to.eql(EVENTS.showDropdown);
|
||||
expect(passedElementTriggerEventStub.lastCall.args[1]).to.eql({});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('passing true focusInput flag with canSearch set to true', () => {
|
||||
|
@ -395,8 +410,11 @@ describe('choices', () => {
|
|||
output = instance.showDropdown(true);
|
||||
});
|
||||
|
||||
it('focuses input', () => {
|
||||
it('focuses input', (done) => {
|
||||
requestAnimationFrame(() => {
|
||||
expect(inputFocusSpy.called).to.equal(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -453,18 +471,27 @@ describe('choices', () => {
|
|||
expect(output).to.eql(instance);
|
||||
});
|
||||
|
||||
it('closes containerOuter', () => {
|
||||
it('closes containerOuter', (done) => {
|
||||
requestAnimationFrame(() => {
|
||||
expect(containerOuterCloseSpy.called).to.equal(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('hides dropdown with blurInput flag', () => {
|
||||
it('hides dropdown with blurInput flag', (done) => {
|
||||
requestAnimationFrame(() => {
|
||||
expect(dropdownHideSpy.called).to.equal(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('triggers event on passedElement', () => {
|
||||
it('triggers event on passedElement', (done) => {
|
||||
requestAnimationFrame(() => {
|
||||
expect(passedElementTriggerEventStub.called).to.equal(true);
|
||||
expect(passedElementTriggerEventStub.lastCall.args[0]).to.eql(EVENTS.hideDropdown);
|
||||
expect(passedElementTriggerEventStub.lastCall.args[1]).to.eql({});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('passing true blurInput flag with canSearch set to true', () => {
|
||||
|
@ -474,12 +501,18 @@ describe('choices', () => {
|
|||
output = instance.hideDropdown(true);
|
||||
});
|
||||
|
||||
it('removes active descendants', () => {
|
||||
it('removes active descendants', (done) => {
|
||||
requestAnimationFrame(() => {
|
||||
expect(inputRemoveActiveDescendantSpy.called).to.equal(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('blurs input', () => {
|
||||
it('blurs input', (done) => {
|
||||
requestAnimationFrame(() => {
|
||||
expect(inputBlurSpy.called).to.equal(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1477,8 +1510,8 @@ describe('choices', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('createGroupsFragment', () => {
|
||||
let createChoicesFragmentStub;
|
||||
describe('_createGroupsFragment', () => {
|
||||
let _createChoicesFragmentStub;
|
||||
const choices = [
|
||||
{
|
||||
id: 1,
|
||||
|
@ -1519,12 +1552,12 @@ describe('choices', () => {
|
|||
];
|
||||
|
||||
beforeEach(() => {
|
||||
createChoicesFragmentStub = stub();
|
||||
instance.createChoicesFragment = createChoicesFragmentStub;
|
||||
_createChoicesFragmentStub = stub();
|
||||
instance._createChoicesFragment = _createChoicesFragmentStub;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
instance.createChoicesFragment.reset();
|
||||
instance._createChoicesFragment.reset();
|
||||
});
|
||||
|
||||
describe('returning a fragment of groups', () => {
|
||||
|
@ -1534,7 +1567,7 @@ describe('choices', () => {
|
|||
const childElement = document.createElement('div');
|
||||
fragment.appendChild(childElement);
|
||||
|
||||
output = instance.createGroupsFragment(groups, choices, fragment);
|
||||
output = instance._createGroupsFragment(groups, choices, fragment);
|
||||
const elementToWrapFragment = document.createElement('div');
|
||||
elementToWrapFragment.appendChild(output);
|
||||
|
||||
|
@ -1546,7 +1579,7 @@ describe('choices', () => {
|
|||
|
||||
describe('not passing fragment argument', () => {
|
||||
it('returns new groups fragment', () => {
|
||||
output = instance.createGroupsFragment(groups, choices);
|
||||
output = instance._createGroupsFragment(groups, choices);
|
||||
const elementToWrapFragment = document.createElement('div');
|
||||
elementToWrapFragment.appendChild(output);
|
||||
|
||||
|
@ -1570,7 +1603,7 @@ describe('choices', () => {
|
|||
|
||||
it('sorts groups by config.sortFn', () => {
|
||||
expect(sortFnStub.called).to.equal(false);
|
||||
instance.createGroupsFragment(groups, choices);
|
||||
instance._createGroupsFragment(groups, choices);
|
||||
expect(sortFnStub.called).to.equal(true);
|
||||
});
|
||||
});
|
||||
|
@ -1589,7 +1622,7 @@ describe('choices', () => {
|
|||
});
|
||||
|
||||
it('does not sort groups', () => {
|
||||
instance.createGroupsFragment(groups, choices);
|
||||
instance._createGroupsFragment(groups, choices);
|
||||
expect(sortFnStub.called).to.equal(false);
|
||||
});
|
||||
});
|
||||
|
@ -1599,11 +1632,11 @@ describe('choices', () => {
|
|||
instance.isSelectOneElement = true;
|
||||
});
|
||||
|
||||
it('calls createChoicesFragment with choices that belong to each group', () => {
|
||||
expect(createChoicesFragmentStub.called).to.equal(false);
|
||||
instance.createGroupsFragment(groups, choices);
|
||||
expect(createChoicesFragmentStub.called).to.equal(true);
|
||||
expect(createChoicesFragmentStub.firstCall.args[0]).to.eql([
|
||||
it('calls _createChoicesFragment with choices that belong to each group', () => {
|
||||
expect(_createChoicesFragmentStub.called).to.equal(false);
|
||||
instance._createGroupsFragment(groups, choices);
|
||||
expect(_createChoicesFragmentStub.called).to.equal(true);
|
||||
expect(_createChoicesFragmentStub.firstCall.args[0]).to.eql([
|
||||
{
|
||||
id: 1,
|
||||
selected: true,
|
||||
|
@ -1619,7 +1652,7 @@ describe('choices', () => {
|
|||
label: 'Choice 3',
|
||||
},
|
||||
]);
|
||||
expect(createChoicesFragmentStub.secondCall.args[0]).to.eql([
|
||||
expect(_createChoicesFragmentStub.secondCall.args[0]).to.eql([
|
||||
{
|
||||
id: 2,
|
||||
selected: false,
|
||||
|
@ -1638,11 +1671,11 @@ describe('choices', () => {
|
|||
instance.config.renderSelectedChoices = 'always';
|
||||
});
|
||||
|
||||
it('calls createChoicesFragment with choices that belong to each group', () => {
|
||||
expect(createChoicesFragmentStub.called).to.equal(false);
|
||||
instance.createGroupsFragment(groups, choices);
|
||||
expect(createChoicesFragmentStub.called).to.equal(true);
|
||||
expect(createChoicesFragmentStub.firstCall.args[0]).to.eql([
|
||||
it('calls _createChoicesFragment with choices that belong to each group', () => {
|
||||
expect(_createChoicesFragmentStub.called).to.equal(false);
|
||||
instance._createGroupsFragment(groups, choices);
|
||||
expect(_createChoicesFragmentStub.called).to.equal(true);
|
||||
expect(_createChoicesFragmentStub.firstCall.args[0]).to.eql([
|
||||
{
|
||||
id: 1,
|
||||
selected: true,
|
||||
|
@ -1658,7 +1691,7 @@ describe('choices', () => {
|
|||
label: 'Choice 3',
|
||||
},
|
||||
]);
|
||||
expect(createChoicesFragmentStub.secondCall.args[0]).to.eql([
|
||||
expect(_createChoicesFragmentStub.secondCall.args[0]).to.eql([
|
||||
{
|
||||
id: 2,
|
||||
selected: false,
|
||||
|
@ -1676,11 +1709,11 @@ describe('choices', () => {
|
|||
instance.config.renderSelectedChoices = false;
|
||||
});
|
||||
|
||||
it('calls createChoicesFragment with choices that belong to each group that are not already selected', () => {
|
||||
expect(createChoicesFragmentStub.called).to.equal(false);
|
||||
instance.createGroupsFragment(groups, choices);
|
||||
expect(createChoicesFragmentStub.called).to.equal(true);
|
||||
expect(createChoicesFragmentStub.firstCall.args[0]).to.eql([
|
||||
it('calls _createChoicesFragment with choices that belong to each group that are not already selected', () => {
|
||||
expect(_createChoicesFragmentStub.called).to.equal(false);
|
||||
instance._createGroupsFragment(groups, choices);
|
||||
expect(_createChoicesFragmentStub.called).to.equal(true);
|
||||
expect(_createChoicesFragmentStub.firstCall.args[0]).to.eql([
|
||||
{
|
||||
id: 3,
|
||||
selected: false,
|
||||
|
@ -1689,7 +1722,7 @@ describe('choices', () => {
|
|||
label: 'Choice 3',
|
||||
},
|
||||
]);
|
||||
expect(createChoicesFragmentStub.secondCall.args[0]).to.eql([
|
||||
expect(_createChoicesFragmentStub.secondCall.args[0]).to.eql([
|
||||
{
|
||||
id: 2,
|
||||
selected: false,
|
||||
|
@ -1704,16 +1737,6 @@ describe('choices', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('createChoicesFragment', () => {
|
||||
beforeEach(() => {});
|
||||
it('returns a fragment of choices', () => {});
|
||||
});
|
||||
|
||||
describe('createItemsFragment', () => {
|
||||
beforeEach(() => {});
|
||||
it('returns a fragment of items', () => {});
|
||||
});
|
||||
|
||||
describe('render', () => {
|
||||
beforeEach(() => {});
|
||||
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
import { dispatchEvent } from '../lib/utils';
|
||||
import { dispatchEvent, isElement } from '../lib/utils';
|
||||
|
||||
export default class WrappedElement {
|
||||
constructor({ element, classNames }) {
|
||||
Object.assign(this, { element, classNames });
|
||||
|
||||
if (!isElement(element)) {
|
||||
throw new TypeError('Invalid element passed');
|
||||
}
|
||||
|
||||
this.isDisabled = false;
|
||||
}
|
||||
|
||||
|
|
|
@ -603,3 +603,7 @@ export const reduceToValues = (items, key = 'value') => {
|
|||
|
||||
return values;
|
||||
}
|
||||
|
||||
export const isIE11 = () => {
|
||||
return !!(navigator.userAgent.match(/Trident/) && navigator.userAgent.match(/rv[ :]11/));
|
||||
};
|
2
types/index.d.ts
vendored
2
types/index.d.ts
vendored
|
@ -917,7 +917,7 @@ export default class Choices {
|
|||
private createChoicesFragment(choices: any[], fragment: DocumentFragment, withinGroup?: boolean): DocumentFragment;
|
||||
|
||||
/** Render items into a DOM fragment and append to items list */
|
||||
private createItemsFragment(items: any[], fragment?: DocumentFragment): void;
|
||||
private _createItemsFragment(items: any[], fragment?: DocumentFragment): void;
|
||||
|
||||
/** Render DOM with values */
|
||||
private render(): void;
|
||||
|
|
Loading…
Reference in a new issue