mirror of
https://github.com/Choices-js/Choices.git
synced 2024-06-07 16:32:32 +02:00
Merge branch 'master' of github.com:jshjohnson/Choices into gh-pages
This commit is contained in:
commit
b288261b86
36
README.md
36
README.md
|
@ -1,5 +1,5 @@
|
|||
# Choices.js [![Build Status](https://travis-ci.org/jshjohnson/Choices.svg?branch=master)](https://travis-ci.org/jshjohnson/Choices)
|
||||
A lightweight, configurable select box/text input plugin. Similar to Select2 and Selectize but without the jQuery dependency.
|
||||
A vanilla, lightweight (~15kb gzipped 🎉), configurable select box/text input plugin. Similar to Select2 and Selectize but without the jQuery dependency.
|
||||
|
||||
[Demo](https://joshuajohnson.co.uk/Choices/)
|
||||
|
||||
|
@ -48,6 +48,8 @@ A lightweight, configurable select box/text input plugin. Similar to Select2 and
|
|||
prependValue: null,
|
||||
appendValue: null,
|
||||
loadingText: 'Loading...',
|
||||
noResultsText: 'No results round',
|
||||
noChoicesText: 'No choices to choose from',
|
||||
classNames: {
|
||||
containerOuter: 'choices',
|
||||
containerInner: 'choices__inner',
|
||||
|
@ -181,6 +183,13 @@ Pass an array of objects:
|
|||
|
||||
<strong>Usage:</strong> Whether a user can edit items. An items value can be edited by pressing the backspace.
|
||||
|
||||
### duplicateItems
|
||||
<strong>Type:</strong> `Boolean` <strong>Default:</strong> `true`
|
||||
|
||||
<strong>Input types affected:</strong> `text`, `select-multiple`
|
||||
|
||||
<strong>Usage:</strong> Whether a user can input/choose a duplicate item.
|
||||
|
||||
### delimiter
|
||||
<strong>Type:</strong> `String` <strong>Default:</strong> `,`
|
||||
|
||||
|
@ -188,13 +197,6 @@ Pass an array of objects:
|
|||
|
||||
<strong>Usage:</strong> What divides each value. By default the delimited value would be `"Value 1, Value 2, Value 3"`.
|
||||
|
||||
### duplicates
|
||||
<strong>Type:</strong> `Boolean` <strong>Default:</strong> `true`
|
||||
|
||||
<strong>Input types affected:</strong> `text`
|
||||
|
||||
<strong>Usage:</strong> Whether a user can input a duplicate item.
|
||||
|
||||
### paste
|
||||
<strong>Type:</strong> `Boolean` <strong>Default:</strong> `true`
|
||||
|
||||
|
@ -274,14 +276,28 @@ const example = new Choices(element, {
|
|||
|
||||
<strong>Input types affected:</strong> `text`, `select-one`, `select-multiple`
|
||||
|
||||
<strong>Usage:</strong> Append a value to each item added/selected
|
||||
<strong>Usage:</strong> Append a value to each item added/selected.
|
||||
|
||||
### loadingText
|
||||
<strong>Type:</strong> `String` <strong>Default:</strong> `Loading...`
|
||||
|
||||
<strong>Input types affected:</strong> `select-one`, `select-multiple`
|
||||
|
||||
<strong>Usage:</strong> The loading text that is shown when options are populated via an AJAX callback.
|
||||
<strong>Usage:</strong> The text that is shown whilst choices are being populated via AJAX.
|
||||
|
||||
### noResultsText
|
||||
<strong>Type:</strong> `String` <strong>Default:</strong> `No results round`
|
||||
|
||||
<strong>Input types affected:</strong> `select-one`, `select-multiple`
|
||||
|
||||
<strong>Usage:</strong> The text that is shown when a user's search has returned no results.
|
||||
|
||||
### noChoicesText
|
||||
<strong>Type:</strong> `String` <strong>Default:</strong> `No choices to choose from`
|
||||
|
||||
<strong>Input types affected:</strong> `select-multiple`
|
||||
|
||||
<strong>Usage:</strong> The text that is shown when a user has selected all possible choices.
|
||||
|
||||
### classNames
|
||||
<strong>Type:</strong> `Object` <strong>Default:</strong>
|
||||
|
|
6
assets/scripts/dist/choices.min.js
vendored
6
assets/scripts/dist/choices.min.js
vendored
File diff suppressed because one or more lines are too long
1
assets/scripts/dist/choices.min.js.map
vendored
Normal file
1
assets/scripts/dist/choices.min.js.map
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
{"version":3,"file":"choices.min.js","sources":[],"mappings":";;","sourceRoot":""}
|
|
@ -45,6 +45,8 @@ export class Choices {
|
|||
prependValue: null,
|
||||
appendValue: null,
|
||||
loadingText: 'Loading...',
|
||||
noResultsText: 'No results round',
|
||||
noChoicesText: 'No choices to choose from',
|
||||
classNames: {
|
||||
containerOuter: 'choices',
|
||||
containerInner: 'choices__inner',
|
||||
|
@ -407,7 +409,6 @@ export class Choices {
|
|||
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
|
||||
|
@ -508,8 +509,8 @@ export class Choices {
|
|||
/**
|
||||
* Direct populate choices
|
||||
* @param {Array} choices - Choices to insert
|
||||
* @param {string} value - Name of 'value' property
|
||||
* @param {string} label - Name of 'label' property
|
||||
* @param {String} value - Name of 'value' property
|
||||
* @param {String} label - Name of 'label' property
|
||||
* @return {Object} Class instance
|
||||
* @public
|
||||
*/
|
||||
|
@ -545,6 +546,19 @@ export class Choices {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set value of input to blank
|
||||
* @return {Object} Class instance
|
||||
* @public
|
||||
*/
|
||||
clearInput() {
|
||||
if (this.input.value) this.input.value = '';
|
||||
if(this.passedElement.type !== 'select-one') {
|
||||
this.input.style.width = getWidthOfInput(this.input);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Disable interaction with Choices
|
||||
* @return {Object} Class instance
|
||||
|
@ -596,13 +610,20 @@ export class Choices {
|
|||
if(this.passedElement.type === 'select-one') {
|
||||
const placeholderItem = this._getTemplate('item', { id: -1, value: 'Loading', label: this.config.loadingText, active: true});
|
||||
this.itemList.appendChild(placeholderItem);
|
||||
} else {
|
||||
this.input.placeholder = this.config.loadingText;
|
||||
}
|
||||
|
||||
const callback = (results, value, label) => {
|
||||
if(!isType('Array', results) || !value) return;
|
||||
if(results && results.length) {
|
||||
// Remove loading states/text
|
||||
this.containerOuter.classList.remove(this.config.classNames.loadingState);
|
||||
const filter = this.config.sortFilter;
|
||||
|
||||
if(this.passedElement.type === 'select-multiple') {
|
||||
this.input.placeholder = this.config.placeholderValue || this.passedElement.getAttribute('placeholder');
|
||||
}
|
||||
|
||||
// Add each result as a choice
|
||||
results.forEach((result, index) => {
|
||||
// Select first choice in list if single select input
|
||||
if(index === 0 && this.passedElement.type === 'select-one') {
|
||||
|
@ -620,22 +641,9 @@ export class Choices {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set value of input to blank
|
||||
* @return {Object} Class instance
|
||||
* @public
|
||||
*/
|
||||
clearInput() {
|
||||
if (this.input.value) this.input.value = '';
|
||||
if(this.passedElement.type !== 'select-one') {
|
||||
this.input.style.width = getWidthOfInput(this.input);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Call change callback
|
||||
* @param {string} value - last added/deleted/selected value
|
||||
* @param {String} value - last added/deleted/selected value
|
||||
* @return
|
||||
* @private
|
||||
*/
|
||||
|
@ -659,48 +667,99 @@ export class Choices {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Process enter key event
|
||||
* @param {Array} activeItems Items that are currently active
|
||||
|
||||
/**
|
||||
* Process enter/click of an item button
|
||||
* @param {Array} activeItems The currently active items
|
||||
* @param {Element} element Button being interacted with
|
||||
* @return
|
||||
* @private
|
||||
*/
|
||||
_handleEnter(activeItems, value) {
|
||||
let canUpdate = true;
|
||||
_handleButtonAction(activeItems, element) {
|
||||
if(!activeItems || !element) return;
|
||||
// If we are clicking on a button
|
||||
if(this.config.removeItems && this.config.removeItemButton) {
|
||||
const itemId = element.parentNode.getAttribute('data-id');
|
||||
const itemToRemove = activeItems.find((item) => item.id === parseInt(itemId));
|
||||
|
||||
if(this.config.addItems) {
|
||||
if (this.config.maxItemCount && this.config.maxItemCount > 0 && this.config.maxItemCount <= this.itemList.children.length) {
|
||||
// If there is a max entry limit and we have reached that limit
|
||||
// don't update
|
||||
canUpdate = false;
|
||||
} else if(this.config.duplicateItems === false && this.passedElement.value) {
|
||||
// If no duplicates are allowed, and the value already exists
|
||||
// in the array, don't update
|
||||
canUpdate = !activeItems.some((item) => item.value === value);
|
||||
}
|
||||
} else {
|
||||
canUpdate = false;
|
||||
// Remove item associated with button
|
||||
this._removeItem(itemToRemove);
|
||||
this._triggerChange(itemToRemove.value);
|
||||
}
|
||||
}
|
||||
|
||||
if (canUpdate) {
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
_handleItemAction(activeItems, element, hasShiftKey = false) {
|
||||
if(!activeItems || !element) return;
|
||||
|
||||
// If we are clicking on an item
|
||||
if(this.config.removeItems && this.passedElement.type !== 'select-one') {
|
||||
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) && !item.highlighted) {
|
||||
this.highlightItem(item);
|
||||
} else if(!hasShiftKey) {
|
||||
if(item.highlighted) {
|
||||
this.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 {[type]} [description]
|
||||
*/
|
||||
_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);
|
||||
|
||||
if(choice && !choice.selected && !choice.disabled) {
|
||||
|
||||
const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState);
|
||||
let canAddItem = true;
|
||||
|
||||
// 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(this.config.maxItemCount > 0 && this.config.maxItemCount <= activeItems.length && this.passedElement.type === 'select-multiple') {
|
||||
canAddItem = false;
|
||||
}
|
||||
|
||||
// All is good, add
|
||||
|
||||
if(canAddItem) {
|
||||
this.toggleDropdown();
|
||||
this._addItem(value);
|
||||
this._triggerChange(value);
|
||||
this.clearInput(this.passedElement);
|
||||
this._addItem(choice.value, choice.label, choice.id);
|
||||
this._triggerChange(choice.value);
|
||||
}
|
||||
|
||||
if(this.passedElement.type === 'select-one') {
|
||||
if(this.canSearch) {
|
||||
this.input.value = "";
|
||||
}
|
||||
this.isSearching = false;
|
||||
this.store.dispatch(activateChoices(true));
|
||||
this.hideDropdown();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Process back space event
|
||||
|
@ -726,6 +785,50 @@ export class Choices {
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
_canAddItem(activeItems, value) {
|
||||
let canAddItem = true;
|
||||
let notice = `Press Enter to add "${ value }"`;
|
||||
|
||||
if(this.passedElement.type === 'select-multiple' || this.passedElement.type === 'text') {
|
||||
if (this.config.maxItemCount > 0 && this.config.maxItemCount <= this.itemList.children.length) {
|
||||
// If there is a max entry limit and we have reached that limit
|
||||
// don't update
|
||||
canAddItem = false;
|
||||
notice = `Only ${ this.config.maxItemCount } values can be added.`;
|
||||
}
|
||||
}
|
||||
|
||||
if(this.passedElement.type === 'text' && this.config.addItems) {
|
||||
const isUnique = !activeItems.some((item) => item.value === value);
|
||||
|
||||
// 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
|
||||
if(this.config.duplicateItems === false && !isUnique) {
|
||||
canAddItem = false;
|
||||
notice = `Only unique values can be added.`;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
response: canAddItem,
|
||||
notice: notice,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter choices based on search value
|
||||
* @param {String} value Value to filter by
|
||||
|
@ -747,14 +850,13 @@ export class Choices {
|
|||
if(newValue.length >= 1 && newValue !== currentValue + ' ') {
|
||||
const haystack = this.store.getChoicesFilteredBySelectable();
|
||||
const needle = newValue;
|
||||
|
||||
const keys = isType('Array', this.config.sortFields) ? this.config.sortFields : [this.config.sortFields];
|
||||
const fuse = new Fuse(haystack, {
|
||||
const keys = isType('Array', this.config.sortFields) ? this.config.sortFields : [this.config.sortFields];
|
||||
const fuse = new Fuse(haystack, {
|
||||
keys: keys,
|
||||
shouldSort: true,
|
||||
include: 'score',
|
||||
});
|
||||
const results = fuse.search(needle);
|
||||
const results = fuse.search(needle);
|
||||
|
||||
this.currentValue = newValue;
|
||||
this.highlightPosition = 0;
|
||||
|
@ -772,6 +874,56 @@ export class Choices {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger event listeners
|
||||
* @return
|
||||
* @private
|
||||
*/
|
||||
_addEventListeners() {
|
||||
document.addEventListener('keyup', this._onKeyUp);
|
||||
document.addEventListener('keydown', this._onKeyDown);
|
||||
document.addEventListener('click', this._onClick);
|
||||
document.addEventListener('touchmove', this._onTouchMove);
|
||||
document.addEventListener('touchend', this._onTouchEnd);
|
||||
document.addEventListener('mousedown', this._onMouseDown);
|
||||
document.addEventListener('mouseover', this._onMouseOver);
|
||||
|
||||
if(this.passedElement.type && this.passedElement.type === 'select-one') {
|
||||
this.containerOuter.addEventListener('focus', this._onFocus);
|
||||
this.containerOuter.addEventListener('blur', this._onBlur);
|
||||
}
|
||||
|
||||
this.input.addEventListener('input', this._onInput);
|
||||
this.input.addEventListener('paste', this._onPaste);
|
||||
this.input.addEventListener('focus', this._onFocus);
|
||||
this.input.addEventListener('blur', this._onBlur);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy event listeners
|
||||
* @return
|
||||
* @private
|
||||
*/
|
||||
_removeEventListeners() {
|
||||
document.removeEventListener('keyup', this._onKeyUp);
|
||||
document.removeEventListener('keydown', this._onKeyDown);
|
||||
document.removeEventListener('click', this._onClick);
|
||||
document.removeEventListener('touchmove', this._onTouchMove);
|
||||
document.removeEventListener('touchend', this._onTouchEnd);
|
||||
document.removeEventListener('mousedown', this._onMouseDown);
|
||||
document.removeEventListener('mouseover', this._onMouseOver);
|
||||
|
||||
if(this.passedElement.type && this.passedElement.type === 'select-one') {
|
||||
this.containerOuter.removeEventListener('focus', this._onFocus);
|
||||
this.containerOuter.removeEventListener('blur', this._onBlur);
|
||||
}
|
||||
|
||||
this.input.removeEventListener('input', this._onInput);
|
||||
this.input.removeEventListener('paste', this._onPaste);
|
||||
this.input.removeEventListener('focus', this._onFocus);
|
||||
this.input.removeEventListener('blur', this._onBlur);
|
||||
}
|
||||
|
||||
/**
|
||||
* Key down event
|
||||
* @param {Object} e Event
|
||||
|
@ -820,44 +972,37 @@ export class Choices {
|
|||
|
||||
case enterKey:
|
||||
// If enter key is pressed and the input has a value
|
||||
if(target.value && this.passedElement.type === 'text') {
|
||||
if(this.passedElement.type === 'text' && target.value) {
|
||||
const value = this.input.value;
|
||||
this._handleEnter(activeItems, value);
|
||||
}
|
||||
const canAddItem = this._canAddItem(activeItems, value);
|
||||
|
||||
// Show dropdown if focus
|
||||
if(!hasActiveDropdown && this.passedElement.type === 'select-one') {
|
||||
e.preventDefault();
|
||||
this.showDropdown();
|
||||
if(this.canSearch) {
|
||||
this.input.focus();
|
||||
// All is good, add
|
||||
if(canAddItem.response) {
|
||||
this.toggleDropdown();
|
||||
this._addItem(value);
|
||||
this._triggerChange(value);
|
||||
this.clearInput(this.passedElement);
|
||||
}
|
||||
}
|
||||
|
||||
if(target.hasAttribute('data-button')) {
|
||||
// If we are clicking on a button
|
||||
if(this.config.removeItems && this.config.removeItemButton) {
|
||||
const itemId = target.parentNode.getAttribute('data-id');
|
||||
const itemToRemove = activeItems.find((item) => item.id === parseInt(itemId));
|
||||
|
||||
// Remove item associated with button
|
||||
this._removeItem(itemToRemove);
|
||||
this._triggerChange(itemToRemove.value);
|
||||
|
||||
e.preventDefault();
|
||||
}
|
||||
this._handleButtonAction(activeItems, target);
|
||||
}
|
||||
|
||||
if(hasActiveDropdown) {
|
||||
const highlighted = this.dropdown.querySelector(`.${this.config.classNames.highlightedState}`);
|
||||
|
||||
if(highlighted) {
|
||||
const value = highlighted.getAttribute('data-value');
|
||||
const label = highlighted.innerHTML;
|
||||
const id = highlighted.getAttribute('data-id');
|
||||
this._addItem(value, label, id);
|
||||
this._triggerChange(value);
|
||||
this.clearInput(this.passedElement);
|
||||
const value = highlighted.getAttribute('data-value');
|
||||
const label = highlighted.innerHTML;
|
||||
const id = highlighted.getAttribute('data-id');
|
||||
const canAddItem = this._canAddItem(activeItems, value);
|
||||
|
||||
if(canAddItem.response) {
|
||||
this._addItem(value, label, id);
|
||||
this._triggerChange(value);
|
||||
this.clearInput(this.passedElement);
|
||||
}
|
||||
|
||||
if(this.passedElement.type === 'select-one') {
|
||||
this.isSearching = false;
|
||||
|
@ -865,6 +1010,13 @@ export class Choices {
|
|||
this.toggleDropdown();
|
||||
}
|
||||
}
|
||||
} else if(this.passedElement.type === 'select-one') {
|
||||
// Show dropdown if focus
|
||||
e.preventDefault();
|
||||
this.showDropdown();
|
||||
if(this.canSearch) {
|
||||
this.input.focus();
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
|
@ -941,34 +1093,25 @@ export class Choices {
|
|||
// notice. Otherwise hide the dropdown
|
||||
if(this.passedElement.type === 'text') {
|
||||
const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState);
|
||||
let dropdownItem;
|
||||
if(this.input.value) {
|
||||
const activeItems = this.store.getItemsFilteredByActive();
|
||||
const isUnique = !activeItems.some((item) => item.value === this.input.value);
|
||||
let canAddItem = true;
|
||||
const value = this.input.value;
|
||||
|
||||
// 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(this.input.value);
|
||||
}
|
||||
|
||||
if(this.config.maxItemCount && this.config.maxItemCount > 0 && this.config.maxItemCount <= this.itemList.children.length) {
|
||||
dropdownItem = this._getTemplate('notice', `Only ${ this.config.maxItemCount } values can be added.`);
|
||||
} else if(!this.config.duplicateItems && !isUnique) {
|
||||
dropdownItem = this._getTemplate('notice', `Only unique values can be added.`);
|
||||
} else if(canAddItem) {
|
||||
dropdownItem = this._getTemplate('notice', `Press Enter to add "${ this.input.value }"`);
|
||||
}
|
||||
|
||||
if(canAddItem !== false) {
|
||||
let dropdownItem;
|
||||
|
||||
if(value) {
|
||||
const activeItems = this.store.getItemsFilteredByActive();
|
||||
const canAddItem = this._canAddItem(activeItems, value);
|
||||
|
||||
if(canAddItem.notice) {
|
||||
const dropdownItem = this._getTemplate('notice', canAddItem.notice);
|
||||
this.dropdown.innerHTML = dropdownItem.outerHTML;
|
||||
if(!this.dropdown.classList.contains(this.config.classNames.activeState)) {
|
||||
}
|
||||
|
||||
if(canAddItem.response === true) {
|
||||
if(!hasActiveDropdown) {
|
||||
this.showDropdown();
|
||||
}
|
||||
} else {
|
||||
if(hasActiveDropdown) {
|
||||
if(!canAddItem.notice && hasActiveDropdown) {
|
||||
this.hideDropdown();
|
||||
}
|
||||
}
|
||||
|
@ -977,6 +1120,7 @@ export class Choices {
|
|||
this.hideDropdown();
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
const backKey = 46;
|
||||
const deleteKey = 8;
|
||||
|
@ -994,6 +1138,7 @@ export class Choices {
|
|||
this._searchChoices(this.input.value);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1078,63 +1223,21 @@ export class Choices {
|
|||
if(this.containerOuter.contains(target) && target !== this.input) {
|
||||
|
||||
const activeItems = this.store.getItemsFilteredByActive();
|
||||
const hasShiftKey = e.shiftKey ? true : false;
|
||||
|
||||
// Prevent input mouse down triggering focus event
|
||||
if(target !== this.input) {
|
||||
e.preventDefault();
|
||||
}
|
||||
if(target !== this.input) e.preventDefault();
|
||||
|
||||
if(target.hasAttribute('data-button')) {
|
||||
// If we are clicking on a button
|
||||
if(this.config.removeItems && this.config.removeItemButton) {
|
||||
const itemId = target.parentNode.getAttribute('data-id');
|
||||
const itemToRemove = activeItems.find((item) => item.id === parseInt(itemId));
|
||||
|
||||
// Remove item associated with button
|
||||
this._removeItem(itemToRemove);
|
||||
this._triggerChange(itemToRemove.value);
|
||||
}
|
||||
this._handleButtonAction(activeItems, target);
|
||||
} else if(target.hasAttribute('data-item')) {
|
||||
// If we are clicking on an item
|
||||
if(this.config.removeItems && this.passedElement.type !== 'select-one') {
|
||||
const passedId = target.getAttribute('data-id');
|
||||
const hasShiftKey = e.shiftKey ? true : false;
|
||||
|
||||
// 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) && !item.highlighted) {
|
||||
this.highlightItem(item);
|
||||
} else if(!hasShiftKey) {
|
||||
if(item.highlighted) {
|
||||
this.unhighlightItem(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
this._handleItemAction(activeItems, target, hasShiftKey);
|
||||
} else if(target.hasAttribute('data-choice')) {
|
||||
// If we are clicking on an option
|
||||
const id = target.getAttribute('data-id');
|
||||
const choice = this.store.getChoiceById(id);
|
||||
|
||||
if(choice && !choice.selected && !choice.disabled) {
|
||||
this._addItem(choice.value, choice.label, choice.id);
|
||||
this._triggerChange(choice.value);
|
||||
if(this.passedElement.type === 'select-one') {
|
||||
if(this.canSearch) {
|
||||
this.input.value = "";
|
||||
}
|
||||
this.isSearching = false;
|
||||
this.store.dispatch(activateChoices(true));
|
||||
this.hideDropdown();
|
||||
}
|
||||
}
|
||||
this._handleChoiceAction(activeItems, target);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Click event
|
||||
* @param {Object} e Event
|
||||
|
@ -1163,7 +1266,7 @@ export class Choices {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
if(this.passedElement.type === 'select-one' && hasActiveDropdown) {
|
||||
if(this.passedElement.type === 'select-one') {
|
||||
if(target !== this.input) {
|
||||
this.hideDropdown();
|
||||
}
|
||||
|
@ -1172,7 +1275,7 @@ export class Choices {
|
|||
|
||||
} else {
|
||||
// Click is outside of our element so close dropdown and de-select items
|
||||
|
||||
|
||||
const activeItems = this.store.getItemsFilteredByActive();
|
||||
const hasHighlightedItems = activeItems.some((item) => item.highlighted === true);
|
||||
|
||||
|
@ -1218,7 +1321,6 @@ export class Choices {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Focus event
|
||||
* @param {Object} e Event
|
||||
|
@ -1260,10 +1362,12 @@ export class Choices {
|
|||
* @private
|
||||
*/
|
||||
_onBlur(e) {
|
||||
const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState);
|
||||
|
||||
// If the blurred element is this input or the outer container
|
||||
if(e.target === this.input || (e.target === this.containerOuter && this.passedElement.type === 'select-one')) {
|
||||
const activeItems = this.store.getItemsFilteredByActive();
|
||||
const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState);
|
||||
const hasHighlightedItems = activeItems.some((item) => item.highlighted === true);
|
||||
|
||||
// Remove the focus state
|
||||
this.containerOuter.classList.remove(this.config.classNames.focusState);
|
||||
|
||||
|
@ -1272,10 +1376,14 @@ export class Choices {
|
|||
if(hasActiveDropdown && (e.target === this.input || e.target === this.containerOuter && !this.canSearch)) {
|
||||
this.hideDropdown();
|
||||
}
|
||||
|
||||
// De-select any highlighted items
|
||||
if(hasHighlightedItems) {
|
||||
this.unhighlightAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Tests value against a regular expression
|
||||
* @param {string} value Value to test
|
||||
|
@ -1388,7 +1496,6 @@ export class Choices {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add item to store with correct value
|
||||
* @param {String} value Value to add to store
|
||||
|
@ -1877,7 +1984,7 @@ export class Choices {
|
|||
this._highlightChoice();
|
||||
} else {
|
||||
// Otherwise show a notice
|
||||
const dropdownItem = this.isSearching ? this._getTemplate('notice', 'No results found') : this._getTemplate('notice', 'No choices to choose from');
|
||||
const dropdownItem = this.isSearching ? this._getTemplate('notice', this.config.noResultsText) : this._getTemplate('notice', this.config.noChoicesText);
|
||||
this.choiceList.appendChild(dropdownItem);
|
||||
}
|
||||
}
|
||||
|
@ -1904,56 +2011,6 @@ export class Choices {
|
|||
this.prevState = this.currentState;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Trigger event listeners
|
||||
* @return
|
||||
* @private
|
||||
*/
|
||||
_addEventListeners() {
|
||||
document.addEventListener('keyup', this._onKeyUp);
|
||||
document.addEventListener('keydown', this._onKeyDown);
|
||||
document.addEventListener('click', this._onClick);
|
||||
document.addEventListener('touchmove', this._onTouchMove);
|
||||
document.addEventListener('touchend', this._onTouchEnd);
|
||||
document.addEventListener('mousedown', this._onMouseDown);
|
||||
document.addEventListener('mouseover', this._onMouseOver);
|
||||
|
||||
if(this.passedElement.type && this.passedElement.type === 'select-one') {
|
||||
this.containerOuter.addEventListener('focus', this._onFocus);
|
||||
this.containerOuter.addEventListener('blur', this._onBlur);
|
||||
}
|
||||
|
||||
this.input.addEventListener('input', this._onInput);
|
||||
this.input.addEventListener('paste', this._onPaste);
|
||||
this.input.addEventListener('focus', this._onFocus);
|
||||
this.input.addEventListener('blur', this._onBlur);
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroy event listeners
|
||||
* @return
|
||||
* @private
|
||||
*/
|
||||
_removeEventListeners() {
|
||||
document.removeEventListener('keyup', this._onKeyUp);
|
||||
document.removeEventListener('keydown', this._onKeyDown);
|
||||
document.removeEventListener('click', this._onClick);
|
||||
document.removeEventListener('touchmove', this._onTouchMove);
|
||||
document.removeEventListener('touchend', this._onTouchEnd);
|
||||
document.removeEventListener('mousedown', this._onMouseDown);
|
||||
document.removeEventListener('mouseover', this._onMouseOver);
|
||||
|
||||
if(this.passedElement.type && this.passedElement.type === 'select-one') {
|
||||
this.containerOuter.removeEventListener('focus', this._onFocus);
|
||||
this.containerOuter.removeEventListener('blur', this._onBlur);
|
||||
}
|
||||
|
||||
this.input.removeEventListener('input', this._onInput);
|
||||
this.input.removeEventListener('paste', this._onPaste);
|
||||
this.input.removeEventListener('focus', this._onFocus);
|
||||
this.input.removeEventListener('blur', this._onBlur);
|
||||
}
|
||||
};
|
||||
|
||||
window.Choices = module.exports = Choices;
|
33
index.html
33
index.html
|
@ -58,13 +58,6 @@
|
|||
<option value="Dropdown item 3" selected>Dropdown item 3</option>
|
||||
<option value="Dropdown item 4" disabled>Dropdown item 4</option>
|
||||
</select>
|
||||
|
||||
<label for="choices-8">With pre-selected option</label>
|
||||
<select class="form-control" data-choice name="choices-8" id="choices-8" placeholder="Choose an option" multiple>
|
||||
<option value="Dropdown item 1">Dropdown item 1</option>
|
||||
<option value="Dropdown item 2" selected>Dropdown item 2</option>
|
||||
<option value="Dropdown item 3">Dropdown item 3</option>
|
||||
</select>
|
||||
|
||||
<label for="choices-9">Option groups</label>
|
||||
<select class="form-control" data-choice name="choices-9" id="choices-9" placeholder="This is a placeholder" multiple>
|
||||
|
@ -100,8 +93,8 @@
|
|||
</optgroup>
|
||||
</select>
|
||||
|
||||
<label for="choices-10">Options from remote source (Fetch API)</label>
|
||||
<select class="form-control" name="choices-10" id="choices-10" placeholder="Pick an Arctic Monkeys record" multiple></select>
|
||||
<label for="choices-10">Options from remote source (Fetch API) & limited to 5</label>
|
||||
<select class="form-control" name="choices-10" id="choices-10" multiple></select>
|
||||
|
||||
<hr>
|
||||
|
||||
|
@ -199,8 +192,6 @@
|
|||
},
|
||||
});
|
||||
|
||||
console.log(example1.getValue());
|
||||
|
||||
var example2 = new Choices('#choices-2', {
|
||||
paste: false,
|
||||
duplicateItems: false,
|
||||
|
@ -208,7 +199,6 @@
|
|||
});
|
||||
|
||||
var example3 = new Choices('#choices-3', {
|
||||
duplicates: false,
|
||||
editItems: true,
|
||||
regexFilter: /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
|
||||
});
|
||||
|
@ -227,9 +217,10 @@
|
|||
items: ['josh@joshuajohnson.co.uk', { value: 'joe@bloggs.co.uk', label: 'Joe Bloggs' } ],
|
||||
});
|
||||
|
||||
var example8 = new Choices('#choices-10', {
|
||||
var example9 = new Choices('#choices-10', {
|
||||
placeholder: true,
|
||||
placeholderValue: 'Pick an Strokes record',
|
||||
maxItemCount: 5,
|
||||
callbackOnChange: function(value, passedInput) { console.log(value) }
|
||||
}).ajax(function(callback) {
|
||||
fetch('https://api.discogs.com/artists/55980/releases?token=QBRmstCkwXEvCjTclCpumbtNwvVkEzGAdELXyRyW')
|
||||
|
@ -243,7 +234,7 @@
|
|||
});
|
||||
});
|
||||
|
||||
var example9 = new Choices('#choices-12', {
|
||||
var example10 = new Choices('#choices-12', {
|
||||
placeholder: true,
|
||||
placeholderValue: 'Pick an Arctic Monkeys record'
|
||||
}).ajax(function(callback) {
|
||||
|
@ -251,7 +242,7 @@
|
|||
.then(function(response) {
|
||||
response.json().then(function(data) {
|
||||
callback(data.releases, 'title', 'title');
|
||||
example9.setValueByChoice('Fake Tales Of San Francisco');
|
||||
example10.setValueByChoice('Fake Tales Of San Francisco');
|
||||
});
|
||||
})
|
||||
.catch(function(error) {
|
||||
|
@ -259,7 +250,7 @@
|
|||
});
|
||||
});
|
||||
|
||||
var example10 = new Choices('#choices-14', {
|
||||
var example11 = new Choices('#choices-14', {
|
||||
removeItemButton: true,
|
||||
}).ajax(function(callback) {
|
||||
var request = new XMLHttpRequest();
|
||||
|
@ -272,7 +263,7 @@
|
|||
if (status == 200) {
|
||||
data = JSON.parse(request.responseText);
|
||||
callback(data.releases, 'title', 'title');
|
||||
example10.setValueByChoice('How Soon Is Now?');
|
||||
example11.setValueByChoice('How Soon Is Now?');
|
||||
} else {
|
||||
console.error(status);
|
||||
}
|
||||
|
@ -281,12 +272,12 @@
|
|||
request.send();
|
||||
});
|
||||
|
||||
var example11 = new Choices('[data-choice]', {
|
||||
var example12 = new Choices('[data-choice]', {
|
||||
placeholderValue: 'This is a placeholder set in the config',
|
||||
removeButton: true,
|
||||
});
|
||||
|
||||
var example12 = new Choices('#choices-15', {
|
||||
var example13 = new Choices('#choices-15', {
|
||||
search: false,
|
||||
choices: [
|
||||
{value: 'One', label: 'Label One'},
|
||||
|
@ -299,7 +290,7 @@
|
|||
{value: 'Six', label: 'Label Six', selected: true},
|
||||
], 'value', 'label');
|
||||
|
||||
var example13 = new Choices('#choices-16', {
|
||||
var example14 = new Choices('#choices-16', {
|
||||
placeholder: true,
|
||||
}).setChoices([{
|
||||
label: 'Group one',
|
||||
|
@ -322,7 +313,7 @@
|
|||
]
|
||||
}], 'value', 'label');
|
||||
|
||||
var example14 = new Choices('#choices-17', {
|
||||
var example15 = new Choices('#choices-17', {
|
||||
choices: [
|
||||
{value: 'One', label: 'Label One'},
|
||||
{value: 'Two', label: 'Label Two', disabled: true},
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "choices.js",
|
||||
"version": "1.1.0",
|
||||
"version": "1.1.1",
|
||||
"description": "A vanilla JS customisable text input/select box plugin",
|
||||
"main": "./assets/scripts/dist/choices.min.js",
|
||||
"scripts": {
|
||||
|
@ -48,7 +48,8 @@
|
|||
"postcss-cli": "^2.5.1",
|
||||
"webpack": "^1.12.14",
|
||||
"webpack-dev-server": "^1.14.1",
|
||||
"whatwg-fetch": "^1.0.0"
|
||||
"whatwg-fetch": "^1.0.0",
|
||||
"wrapper-webpack-plugin": "^0.1.7"
|
||||
},
|
||||
"dependencies": {
|
||||
"redux": "^3.3.1"
|
||||
|
|
|
@ -242,6 +242,35 @@ describe('Choices', function() {
|
|||
expect(this.choices.config.callbackOnChange).toHaveBeenCalledWith(jasmine.any(String), jasmine.any(HTMLElement));
|
||||
});
|
||||
|
||||
it('should open the dropdown on click', function() {
|
||||
const container = this.choices.containerOuter;
|
||||
this.choices._onClick({
|
||||
target: container,
|
||||
ctrlKey: false,
|
||||
preventDefault: () => {}
|
||||
});
|
||||
|
||||
expect(document.activeElement === this.choices.input && container.classList.contains('is-open')).toBe(true);
|
||||
});
|
||||
|
||||
it('should open the dropdown on click', function() {
|
||||
const container = this.choices.containerOuter;
|
||||
|
||||
this.choices._onClick({
|
||||
target: container,
|
||||
ctrlKey: false,
|
||||
preventDefault: () => {}
|
||||
});
|
||||
|
||||
this.choices._onClick({
|
||||
target: container,
|
||||
ctrlKey: false,
|
||||
preventDefault: () => {}
|
||||
});
|
||||
|
||||
expect(document.activeElement === this.choices.input && container.classList.contains('is-open')).toBe(false);
|
||||
});
|
||||
|
||||
it('should filter choices when searching', function() {
|
||||
this.choices.input.focus();
|
||||
this.choices.input.value = 'Value 3';
|
||||
|
|
|
@ -1,5 +1,8 @@
|
|||
var path = require('path');
|
||||
var package = require('./package.json');
|
||||
var webpack = require('webpack');
|
||||
var wrapperPlugin = require('wrapper-webpack-plugin');
|
||||
var banner = `/*! ${ package.name } v${ package.version } | (c) ${ new Date().getFullYear() } ${ package.author } | ${ package.homepage } */ \n`
|
||||
|
||||
module.exports = {
|
||||
devtool: 'cheap-module-source-map',
|
||||
|
@ -29,6 +32,9 @@ module.exports = {
|
|||
'NODE_ENV': JSON.stringify('production'),
|
||||
}
|
||||
}),
|
||||
new wrapperPlugin({
|
||||
header: banner,
|
||||
}),
|
||||
],
|
||||
module: {
|
||||
loaders: [{
|
||||
|
|
Loading…
Reference in a new issue