Terminology updates (options -> choices) && documentation

This commit is contained in:
Josh Johnson 2016-06-27 14:46:12 +01:00
parent 2fa45b2eee
commit a2e45209a7
9 changed files with 223 additions and 173 deletions

173
README.md
View file

@ -1,4 +1,4 @@
# Choices.js - in development # Choices.js - beta
A lightweight, configurable select box/text input plugin. Similar to Select2 and Selectize but without the jQuery dependency. A lightweight, configurable select box/text input plugin. Similar to Select2 and Selectize but without the jQuery dependency.
Coming soon. Coming soon.
@ -17,35 +17,70 @@ Coming soon.
var choice = new Choices('[data-choice']); var choice = new Choices('[data-choice']);
var choice = new Choices('.js-choice'); var choice = new Choices('.js-choice');
// Passing options // Passing options (with default options)
var choices = new Choices(elements, { var choices = new Choices(elements, {
items: [], items: [],
maxItemCount: -1,
addItems: true, addItems: true,
removeItems: true, removeItems: true,
removeButton: false, removeItemButton: false,
editItems: false, editItems: false,
maxItems: false, duplicateItems: true,
delimiter: ',', delimiter: ',',
allowDuplicates: true, paste: true,
allowPaste: true, searchOptions: true,
allowSearch: true, regexFilter: null,
regexFilter: false,
placeholder: true, placeholder: true,
placeholderValue: '', placeholderValue: null,
prependValue: false, prependValue: null,
appendValue: false, appendValue: null,
highlightAll: true,
loadingText: 'Loading...', loadingText: 'Loading...',
templates: {},
classNames: {
containerOuter: 'choices',
containerInner: 'choices__inner',
input: 'choices__input',
inputCloned: 'choices__input--cloned',
list: 'choices__list',
listItems: 'choices__list--multiple',
listSingle: 'choices__list--single',
listDropdown: 'choices__list--dropdown',
item: 'choices__item',
itemSelectable: 'choices__item--selectable',
itemDisabled: 'choices__item--disabled',
itemOption: 'choices__item--option',
group: 'choices__group',
groupHeading : 'choices__heading',
button: 'choices__button',
activeState: 'is-active',
focusState: 'is-focused',
openState: 'is-open',
disabledState: 'is-disabled',
highlightedState: 'is-highlighted',
hiddenState: 'is-hidden',
flippedState: 'is-flipped',
selectedState: 'is-selected',
},
callbackOnInit: () => {},
callbackOnAddItem: (id, value, passedInput) => {},
callbackOnRemoveItem: (id, value, passedInput) => {},
}); });
</script> </script>
``` ```
## Installation ## Installation
To install via NPM, run `npm install --save-dev choices.js` To install via NPM, run `npm install --save-dev choices.js`
## Terminology
| Word | Definition |
| ------ | ---------- |
| Choice | A choice is a value a user can select. A choice would be equivelant to the `<option></option>` element within a select input. |
| Group | A group is a collection of choices. A group should be seen as equivalent to a `<optgroup></optgroup>` element within a select input.|
| Item | An item is an inputted value (if you are using Choices with a text input) or a selected choice (if you are using Choices with a select element). |
## Options ## Options
#### items ### items
<strong>Type:</strong> `Array` <strong>Default:</strong> `[]` <strong>Type:</strong> <strong>Default:</strong> `[]`
<strong>Usage:</strong> Add pre-selected items to input. <strong>Usage:</strong> Add pre-selected items to input.
@ -68,87 +103,89 @@ Pass an array of objects:
}] }]
``` ```
#### addItems ### maxItemCount
<strong>Type:</strong> `Number` <strong>Default:</strong>`-1`
<strong>Usage:</strong> The amount of items a user can input/select ("-1" indicates no limit).
### addItems
<strong>Type:</strong> `Boolean` <strong>Default:</strong>`true` <strong>Type:</strong> `Boolean` <strong>Default:</strong>`true`
<strong>Usage:</strong> Whether a user can add items. <strong>Usage:</strong> Whether a user can add items to the passed input's value.
#### removeItems ### removeItems
<strong>Type:</strong> `Boolean` <strong>Default:</strong>`true` <strong>Type:</strong> `Boolean` <strong>Default:</strong>`true`
<strong>Usage:</strong> Whether a user can remove items (only affects text and multiple select input types). <strong>Usage:</strong> Whether a user can remove items (only affects text and multiple select input types).
#### removeButton ### removeButton
<strong>Type:</strong> `Boolean` <strong>Default:</strong>`false` <strong>Type:</strong> `Boolean` <strong>Default:</strong>`false`
<strong>Usage:</strong> Whether a button should show that, when clicked, will remove an item (only affects text and multiple select input types). <strong>Usage:</strong> Whether a button should show that, when clicked, will remove an item (only affects text and multiple select input types).
#### editItems ### editItems
<strong>Type:</strong> `Boolean` <strong>Default:</strong>`false` <strong>Type:</strong> `Boolean` <strong>Default:</strong>`false`
<strong>Usage:</strong> Whether a user can edit selected items (only affects text input types). <strong>Usage:</strong> Whether a user can edit selected items (only affects text input types).
#### maxItems <strong>Usage:</strong> Optionally set an item limit (`-1` indicates no limit).
<strong>Type:</strong> `Boolean` <strong>Default:</strong>`null`
<strong>Usage:</strong> Optionally set an item limit. ### delimiter
#### delimiter
<strong>Type:</strong> `String` <strong>Default:</strong>`,` <strong>Type:</strong> `String` <strong>Default:</strong>`,`
<strong>Usage:</strong> What divides each value (only affects text input types). <strong>Usage:</strong> What divides each value (only affects text input types).
#### allowDuplicates ### allowDuplicates
<strong>Type:</strong> `Boolean` <strong>Default:</strong>`true` <strong>Type:</strong> `Boolean` <strong>Default:</strong>`true`
<strong>Usage:</strong> Whether a user can input a duplicate item (only affects text input types). <strong>Usage:</strong> Whether a user can input a duplicate item (only affects text input types).
#### allowPaste ### allowPaste
<strong>Type:</strong> `Boolean` <strong>Default:</strong>`true` <strong>Type:</strong> `Boolean` <strong>Default:</strong>`true`
<strong>Usage:</strong> Whether a user can paste into the input. <strong>Usage:</strong> Whether a user can paste into the input.
#### allowSearch ### allowSearch
<strong>Type:</strong> `Boolean` <strong>Default:</strong>`true` <strong>Type:</strong> `Boolean` <strong>Default:</strong>`true`
<strong>Usage:</strong> Whether a user can filter options by searching (only affects select input types). <strong>Usage:</strong> Whether a user can filter options by searching (only affects select input types).
#### regexFilter ### regexFilter
<strong>Type:</strong> `Regex` <strong>Default:</strong>`null` <strong>Type:</strong> `Regex` <strong>Default:</strong>`null`
<strong>Usage:</strong> A filter that will need to pass for a user to successfully add an item (only affects text input types). <strong>Usage:</strong> A filter that will need to pass for a user to successfully add an item (only affects text input types).
#### placeholder ### placeholder
<strong>Type:</strong> `Boolean` <strong>Default:</strong>`true` <strong>Type:</strong> `Boolean` <strong>Default:</strong>`true`
<strong>Usage:</strong> Whether the input should show a placeholder. Used in conjunction with `placeholderValue`. If `placeholder` is set to true and no value is passed to `placeholderValue`, the passed input's placeholder attribute will be used as the placeholder value. <strong>Usage:</strong> Whether the input should show a placeholder. Used in conjunction with `placeholderValue`. If `placeholder` is set to true and no value is passed to `placeholderValue`, the passed input's placeholder attribute will be used as the placeholder value.
#### placeholderValue ### placeholderValue
<strong>Type:</strong> `String` <strong>Default:</strong>`null` <strong>Type:</strong> `String` <strong>Default:</strong>`null`
<strong>Usage:</strong> The value of the inputs placeholder. <strong>Usage:</strong> The value of the inputs placeholder.
#### prependValue ### prependValue
<strong>Type:</strong> `String` <strong>Default:</strong>`null` <strong>Type:</strong> `String` <strong>Default:</strong>`null`
<strong>Usage:</strong> Prepend a value to each item added to input (only affects text input types). <strong>Usage:</strong> Prepend a value to each item added to input (only affects text input types).
#### appendValue ### appendValue
<strong>Type:</strong> `String` <strong>Default:</strong>`null` <strong>Type:</strong> `String` <strong>Default:</strong>`null`
<strong>Usage:</strong> Append a value to each item added to input (only affects text input types). <strong>Usage:</strong> Append a value to each item added to input (only affects text input types).
#### highlightAll ### highlightAll
<strong>Type:</strong> `Boolean` <strong>Default:</strong>`true` <strong>Type:</strong> `Boolean` <strong>Default:</strong>`true`
<strong>Usage:</strong> Whether a user can highlight items. <strong>Usage:</strong> Whether a user can highlight items.
#### loadingText ### loadingText
<strong>Type:</strong> `String` <strong>Default:</strong>`Loading...` <strong>Type:</strong> `String` <strong>Default:</strong>`Loading...`
<strong>Usage:</strong> The loading text that is shown when options are populated via an AJAX callback. <strong>Usage:</strong> The loading text that is shown when options are populated via an AJAX callback.
#### classNames ### classNames
<strong>Type:</strong> `Object` <strong>Default:</strong> <strong>Type:</strong> `Object` <strong>Default:</strong>
``` ```
@ -181,72 +218,86 @@ classNames: {
<strong>Usage:</strong> Classes added to HTML generated by Choices. <strong>Usage:</strong> Classes added to HTML generated by Choices.
#### callbackOnInit ### callbackOnInit
<strong>Type:</strong> `Function` <strong>Default:</strong>`() => {}` <strong>Type:</strong> `Function` <strong>Default:</strong>`() => {}`
<strong>Usage:</strong> Function to run once Choices initialises. <strong>Usage:</strong> Function to run once Choices initialises.
#### callbackOnAddItem ### callbackOnAddItem
<strong>Type:</strong> `Function` <strong>Default:</strong>`(id, value, passedInput) => {}` <strong>Type:</strong> `Function` <strong>Default:</strong>`(id, value, passedInput) => {}`
<strong>Usage:</strong> Function to run each time an item is added. <strong>Usage:</strong> Function to run each time an item is added.
#### callbackOnRemoveItem ### callbackOnRemoveItem
<strong>Type:</strong> `Function` <strong>Default:</strong>`(id, value, passedInput) => {}` <strong>Type:</strong> `Function` <strong>Default:</strong>`(id, value, passedInput) => {}`
<strong>Usage:</strong> Function to run each time an item is removed. <strong>Usage:</strong> Function to run each time an item is removed.
## Methods ## Methods
#### highlightAll(); Methods can be called either directly or by chaining:
```js
// Calling a method by chaining
const choices = new Choices(element, {
addItems: false,
removeItems: false,
}).setValue(['Set value 1', 'Set value 2']).disable();
// Calling a method directly
choices.setValue(['Set value 1', 'Set value 2'])
choices.disable();
```
### highlightAll();
<strong>Usage:</strong> Highlight each chosen item (selected items can be removed). <strong>Usage:</strong> Highlight each chosen item (selected items can be removed).
#### unhighlightAll(); ### unhighlightAll();
<strong>Usage:</strong> Un-highlight each chosen item. <strong>Usage:</strong> Un-highlight each chosen item.
#### removeItemsByValue(value); ### removeItemsByValue(value);
<strong>Usage:</strong> Remove each item by a given value. <strong>Usage:</strong> Remove each item by a given value.
#### removeActiveItems(excludedId); ### removeActiveItems(excludedId);
<strong>Usage:</strong> Remove each selectable item. <strong>Usage:</strong> Remove each selectable item.
#### removeSelectedItems(); ### removeSelectedItems();
<strong>Usage:</strong> Remove each item the user has selected. <strong>Usage:</strong> Remove each item the user has selected.
#### showDropdown(); ### showDropdown();
<strong>Usage:</strong> Show option list dropdown. <strong>Usage:</strong> Show option list dropdown (only affects select inputs).
#### hideDropdown(); ### hideDropdown();
<strong>Usage:</strong> Hide option list dropdown. <strong>Usage:</strong> Hide option list dropdown (only affects select inputs).
#### toggleDropdown(); ### toggleDropdown();
<strong>Usage:</strong> Toggle dropdown between showing/hidden. <strong>Usage:</strong> Toggle dropdown between showing/hidden.
#### setValue(args); ### setValue(args);
<strong>Usage:</strong> Set value of input based on an array of objects or strings. <strong>Usage:</strong> Set value of input based on an array of objects or strings. This behaves exactly the same as passing items via the `items` option but can be called after initialising Choices on an text input (only affects text inputs).
#### clearValue(); ### clearValue();
<strong>Usage:</strong> Clear value of input. <strong>Usage:</strong> Clear value of input.
#### clearInput(); ### clearInput();
<strong>Usage:</strong> Clear input. <strong>Usage:</strong> Clear input of any user inputted text (only affects text inputs).
#### disable(); ### disable();
<strong>Usage:</strong> Disable input from selecting further options. <strong>Usage:</strong> Disable input from selecting further options.
#### ajax(fn); ### ajax(fn);
<strong>Usage:</strong> Populate options via a callback. <strong>Usage:</strong> Populate options via a callback.
@ -259,10 +310,12 @@ To setup a local environment: clone this repo, navigate into it's directory in a
```npm install``` ```npm install```
### NPM tasks ### NPM tasks
* ```npm start``` | Task | Usage |
* ```npm run js:build``` | ------------------- | ------------------------------------------------------------ |
* ```npm run css:watch``` | `npm start` | Fire up local server for development |
* ```npm run css:build``` | `npm run js:build` | Compile Choices to an uglified JavaScript file |
| `npm run css:watch` | Watch SCSS files for changes. On a change, run build process |
| `npm run css:build` | Compile, minify and prefix SCSS files to CSS |
## Contributions ## Contributions
In lieu of a formal styleguide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Lint and test your code using npm scripts...bla bla bla In lieu of a formal styleguide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Lint and test your code using npm scripts...bla bla bla

File diff suppressed because one or more lines are too long

View file

@ -1,18 +1,18 @@
export const addItem = (value, label, id, optionId) => { export const addItem = (value, label, id, choiceId) => {
return { return {
type: 'ADD_ITEM', type: 'ADD_ITEM',
value, value,
label, label,
id, id,
optionId, choiceId,
} }
}; };
export const removeItem = (id, optionId) => { export const removeItem = (id, choiceId) => {
return { return {
type: 'REMOVE_ITEM', type: 'REMOVE_ITEM',
id, id,
optionId, choiceId,
} }
}; };
@ -24,9 +24,9 @@ export const selectItem = (id, selected) => {
} }
}; };
export const addOption = (value, label, id, groupId, disabled) => { export const addChoice = (value, label, id, groupId, disabled) => {
return { return {
type: 'ADD_OPTION', type: 'ADD_CHOICE',
value, value,
label, label,
id, id,
@ -35,14 +35,14 @@ export const addOption = (value, label, id, groupId, disabled) => {
} }
}; };
export const filterOptions = (results) => { export const filterChoices = (results) => {
return { return {
type: 'FILTER_OPTIONS', type: 'FILTER_CHOICES',
results, results,
} }
}; };
export const activateOptions = (active = true) => { export const activateChoices = (active = true) => {
return { return {
type: 'ACTIVATE_OPTIONS', type: 'ACTIVATE_OPTIONS',
active, active,

View file

@ -1,6 +1,6 @@
'use strict'; 'use strict';
import { addItem, removeItem, selectItem, addOption, filterOptions, activateOptions, addGroup, clearAll } from './actions/index'; import { addItem, removeItem, selectItem, addChoice, filterChoices, activateChoices, addGroup, clearAll } from './actions/index';
import { isScrolledIntoView, getAdjacentEl, findAncestor, wrap, isType, strToEl, extend, getWidthOfInput, debounce } from './lib/utils.js'; import { isScrolledIntoView, getAdjacentEl, findAncestor, wrap, isType, strToEl, extend, getWidthOfInput, debounce } from './lib/utils.js';
import Fuse from 'fuse.js'; import Fuse from 'fuse.js';
import Store from './store/index.js'; import Store from './store/index.js';
@ -29,21 +29,20 @@ export class Choices {
const defaultOptions = { const defaultOptions = {
items: [], items: [],
maxItemCount: -1,
addItems: true, addItems: true,
removeItems: true, removeItems: true,
removeButton: false, removeItemButton: false,
editItems: false, editItems: false,
maxItems: null, duplicateItems: true,
delimiter: ',', delimiter: ',',
allowDuplicates: true, paste: true,
allowPaste: true, searchOptions: true,
allowSearch: true,
regexFilter: null, regexFilter: null,
placeholder: true, placeholder: true,
placeholderValue: null, placeholderValue: null,
prependValue: null, prependValue: null,
appendValue: null, appendValue: null,
highlightAll: true,
loadingText: 'Loading...', loadingText: 'Loading...',
templates: {}, templates: {},
classNames: { classNames: {
@ -72,8 +71,8 @@ export class Choices {
selectedState: 'is-selected', selectedState: 'is-selected',
}, },
callbackOnInit: () => {}, callbackOnInit: () => {},
callbackOnRemoveItem: () => {}, callbackOnAddItem: (id, value, passedInput) => {},
callbackOnAddItem: () => {} callbackOnRemoveItem: (id, value, passedInput) => {},
}; };
// Merge options with user options // Merge options with user options
@ -92,7 +91,7 @@ export class Choices {
this.passedElement = isType('String', element) ? document.querySelector(element) : element; this.passedElement = isType('String', element) ? document.querySelector(element) : element;
this.highlightPosition = 0; this.highlightPosition = 0;
this.canSearch = this.options.allowSearch; this.canSearch = this.options.searchOptions;
// Assign preset items from passed object first // Assign preset items from passed object first
this.presetItems = this.options.items; this.presetItems = this.options.items;
@ -371,13 +370,13 @@ export class Choices {
// If we are dealing with a select input, we need to create an option first // 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. // that is then selected. For text inputs we can just add items normally.
if(this.passedElement.type !== 'text') { if(this.passedElement.type !== 'text') {
this._addOption(true, false, item.value, item.label, -1); this._addChoice(true, false, item.value, item.label, -1);
} else { } else {
this._addItem(item.value, item.label, item.id); this._addItem(item.value, item.label, item.id);
} }
} else if(isType('String', item)) { } else if(isType('String', item)) {
if(this.passedElement.type !== 'text') { if(this.passedElement.type !== 'text') {
this._addOption(true, false, item, item, -1); this._addChoice(true, false, item, item, -1);
} else { } else {
this._addItem(item); this._addItem(item);
} }
@ -433,7 +432,7 @@ export class Choices {
if(index === 0) { if(index === 0) {
this._addItem(result[value], result[label], index); this._addItem(result[value], result[label], index);
} }
this._addOption(false, false, result[value], result[label]); this._addChoice(false, false, result[value], result[label]);
}); });
} }
}; };
@ -466,11 +465,11 @@ export class Choices {
let canUpdate = true; let canUpdate = true;
if(this.options.addItems) { if(this.options.addItems) {
if (this.options.maxItems && this.options.maxItems <= this.itemList.children.length) { if (this.options.maxItemCount && this.options.maxItemCount > 0 && this.options.maxItemCount <= this.itemList.children.length) {
// If there is a max entry limit and we have reached that limit // If there is a max entry limit and we have reached that limit
// don't update // don't update
canUpdate = false; canUpdate = false;
} else if(this.options.allowDuplicates === false && this.passedElement.value) { } else if(this.options.duplicateItems === false && this.passedElement.value) {
// If no duplicates are allowed, and the value already exists // If no duplicates are allowed, and the value already exists
// in the array, don't update // in the array, don't update
canUpdate = !activeItems.some((item) => item.value === value ); canUpdate = !activeItems.some((item) => item.value === value );
@ -539,7 +538,7 @@ export class Choices {
const downKey = 40; const downKey = 40;
const activeItems = this.store.getItemsFilteredByActive(); const activeItems = this.store.getItemsFilteredByActive();
const activeOptions = this.store.getOptionsFilteredByActive(); const activeOptions = this.store.getChoicesFilteredByActive();
const hasFocusedInput = this.input === document.activeElement; const hasFocusedInput = this.input === document.activeElement;
const hasActiveDropdown = this.dropdown.classList.contains(this.options.classNames.activeState); const hasActiveDropdown = this.dropdown.classList.contains(this.options.classNames.activeState);
@ -551,14 +550,14 @@ export class Choices {
this.showDropdown(); this.showDropdown();
} }
this.canSearch = this.options.allowSearch; this.canSearch = this.options.searchOptions;
switch (e.keyCode) { switch (e.keyCode) {
case aKey: case aKey:
// If CTRL + A or CMD + A have been pressed and there are items to select // If CTRL + A or CMD + A have been pressed and there are items to select
if(ctrlDownKey && hasItems) { if(ctrlDownKey && hasItems) {
this.canSearch = false; this.canSearch = false;
if(this.options.removeItems && !this.input.value && this.options.highlightAll && this.input === document.activeElement) { if(this.options.removeItems && !this.input.value && this.input === document.activeElement) {
// Highlight items // Highlight items
this.highlightAll(this.itemList.children); this.highlightAll(this.itemList.children);
} }
@ -584,7 +583,7 @@ export class Choices {
if(this.passedElement.type === 'select-one') { if(this.passedElement.type === 'select-one') {
this.isSearching = false; this.isSearching = false;
this.store.dispatch(activateOptions()); this.store.dispatch(activateChoices());
this.toggleDropdown(); this.toggleDropdown();
} }
} }
@ -616,10 +615,10 @@ export class Choices {
if(nextEl) { if(nextEl) {
// We prevent default to stop the cursor moving // We prevent default to stop the cursor moving
// when pressing the arrow // when pressing the arrow
if(!isScrolledIntoView(nextEl, this.optionList, directionInt)) { if(!isScrolledIntoView(nextEl, this.choiceList, directionInt)) {
this._scrollToOption(nextEl, directionInt); this._scrollToChoice(nextEl, directionInt);
} }
this._highlightOption(nextEl); this._highlightChoice(nextEl);
} }
// Prevent default to maintain cursor position whilst // Prevent default to maintain cursor position whilst
@ -661,9 +660,9 @@ export class Choices {
const activeItems = this.store.getItemsFilteredByActive(); const activeItems = this.store.getItemsFilteredByActive();
const isUnique = !activeItems.some((item) => item.value === this.input.value); const isUnique = !activeItems.some((item) => item.value === this.input.value);
if (this.options.maxItems && this.options.maxItems <= this.itemList.children.length) { if (this.options.maxItemCount && this.options.maxItemCount > 0 && this.options.maxItemCount <= this.itemList.children.length) {
dropdownItem = this._getTemplate('notice', `Only ${ this.options.maxItems } options can be added.`); dropdownItem = this._getTemplate('notice', `Only ${ this.options.maxItemCount } options can be added.`);
} else if(!this.options.allowDuplicates && !isUnique) { } else if(!this.options.duplicateItems && !isUnique) {
dropdownItem = this._getTemplate('notice', `Only unique values can be added.`); dropdownItem = this._getTemplate('notice', `Only unique values can be added.`);
} else { } else {
dropdownItem = this._getTemplate('notice', `Add "${ this.input.value }"`); dropdownItem = this._getTemplate('notice', `Add "${ this.input.value }"`);
@ -684,7 +683,7 @@ export class Choices {
// If we have enabled text search // If we have enabled text search
if(this.canSearch) { if(this.canSearch) {
if(this.input === document.activeElement) { if(this.input === document.activeElement) {
const options = this.store.getOptions(); const options = this.store.getChoices();
const hasUnactiveOptions = options.some((option) => option.active !== true); const hasUnactiveOptions = options.some((option) => option.active !== true);
// Check that we have a value to search and the input was an alphanumeric character // Check that we have a value to search and the input was an alphanumeric character
@ -694,7 +693,7 @@ export class Choices {
const currentValue = this.currentValue.trim(); const currentValue = this.currentValue.trim();
if(newValue.length >= 1 && newValue !== currentValue + ' ') { if(newValue.length >= 1 && newValue !== currentValue + ' ') {
const haystack = this.store.getOptionsFiltedBySelectable(); const haystack = this.store.getChoicesFiltedBySelectable();
const needle = newValue; const needle = newValue;
const fuse = new Fuse(haystack, { const fuse = new Fuse(haystack, {
keys: ['label', 'value'], keys: ['label', 'value'],
@ -706,7 +705,7 @@ export class Choices {
this.currentValue = newValue; this.currentValue = newValue;
this.highlightPosition = 0; this.highlightPosition = 0;
this.isSearching = true; this.isSearching = true;
this.store.dispatch(filterOptions(results)); this.store.dispatch(filterChoices(results));
} }
}; };
@ -714,7 +713,7 @@ export class Choices {
} else if(hasUnactiveOptions) { } else if(hasUnactiveOptions) {
// Otherwise reset options to active // Otherwise reset options to active
this.isSearching = false; this.isSearching = false;
this.store.dispatch(activateOptions()); this.store.dispatch(activateChoices());
} }
} }
} }
@ -763,7 +762,7 @@ export class Choices {
} }
if(e.target.hasAttribute('data-button')) { if(e.target.hasAttribute('data-button')) {
if(this.options.removeItems && this.options.removeButton) { if(this.options.removeItems && this.options.removeItemButton) {
const itemId = e.target.parentNode.getAttribute('data-id'); const itemId = e.target.parentNode.getAttribute('data-id');
const itemToRemove = activeItems.find((item) => item.id === parseInt(itemId)); const itemToRemove = activeItems.find((item) => item.id === parseInt(itemId));
this._removeItem(itemToRemove); this._removeItem(itemToRemove);
@ -786,7 +785,7 @@ export class Choices {
} }
} else if(e.target.hasAttribute('data-option')) { } else if(e.target.hasAttribute('data-option')) {
// If we are clicking on an option // If we are clicking on an option
const options = this.store.getOptionsFilteredByActive(); const options = this.store.getChoicesFilteredByActive();
const id = e.target.getAttribute('data-id'); const id = e.target.getAttribute('data-id');
const option = options.find((option) => option.id === parseInt(id)); const option = options.find((option) => option.id === parseInt(id));
@ -795,7 +794,7 @@ export class Choices {
if(this.passedElement.type === 'select-one') { if(this.passedElement.type === 'select-one') {
this.input.value = ""; this.input.value = "";
this.isSearching = false; this.isSearching = false;
this.store.dispatch(activateOptions(true)); this.store.dispatch(activateChoices(true));
this.toggleDropdown(); this.toggleDropdown();
} }
} }
@ -828,7 +827,7 @@ export class Choices {
// If the dropdown is either the target or one of its children is the target // If the dropdown is either the target or one of its children is the target
if((e.target === this.dropdown || findAncestor(e.target, this.options.classNames.listDropdown))) { if((e.target === this.dropdown || findAncestor(e.target, this.options.classNames.listDropdown))) {
if(e.target.hasAttribute('data-option')) { if(e.target.hasAttribute('data-option')) {
this._highlightOption(e.target); this._highlightChoice(e.target);
} }
} }
} }
@ -842,7 +841,7 @@ export class Choices {
_onPaste(e) { _onPaste(e) {
if(e.target !== this.input) return; if(e.target !== this.input) return;
// Disable pasting into the input if option has been set // Disable pasting into the input if option has been set
if(!this.options.allowPaste) { if(!this.options.paste) {
e.preventDefault(); e.preventDefault();
} }
} }
@ -899,20 +898,20 @@ export class Choices {
* @return * @return
* @private * @private
*/ */
_scrollToOption(option, direction) { _scrollToChoice(option, direction) {
if(!option) return; if(!option) return;
const dropdownHeight = this.optionList.offsetHeight; const dropdownHeight = this.choiceList.offsetHeight;
const optionHeight = option.offsetHeight; const optionHeight = option.offsetHeight;
// Distance from bottom of element to top of parent // Distance from bottom of element to top of parent
const optionPos = option.offsetTop + optionHeight; const choicePos = option.offsetTop + optionHeight;
// Scroll position of dropdown // Scroll position of dropdown
const containerScrollPos = this.optionList.scrollTop + dropdownHeight; const containerScrollPos = this.choiceList.scrollTop + dropdownHeight;
// Difference between the option and scroll position // Difference between the option and scroll position
let endPoint = direction > 0 ? ((this.optionList.scrollTop + optionPos) - containerScrollPos) : option.offsetTop; let endPoint = direction > 0 ? ((this.choiceList.scrollTop + choicePos) - containerScrollPos) : option.offsetTop;
const animateScroll = (time, endPoint, direction) => { const animateScroll = (time, endPoint, direction) => {
let continueAnimation = false; let continueAnimation = false;
@ -920,19 +919,19 @@ export class Choices {
const strength = 4; const strength = 4;
if(direction > 0) { if(direction > 0) {
easing = (endPoint - this.optionList.scrollTop)/strength; easing = (endPoint - this.choiceList.scrollTop)/strength;
distance = easing > 1 ? easing : 1; distance = easing > 1 ? easing : 1;
this.optionList.scrollTop = this.optionList.scrollTop + distance; this.choiceList.scrollTop = this.choiceList.scrollTop + distance;
if(this.optionList.scrollTop < endPoint) { if(this.choiceList.scrollTop < endPoint) {
continueAnimation = true; continueAnimation = true;
} }
} else { } else {
easing = (this.optionList.scrollTop - endPoint)/strength; easing = (this.choiceList.scrollTop - endPoint)/strength;
distance = easing > 1 ? easing : 1; distance = easing > 1 ? easing : 1;
this.optionList.scrollTop = this.optionList.scrollTop - distance; this.choiceList.scrollTop = this.choiceList.scrollTop - distance;
if(this.optionList.scrollTop > endPoint) { if(this.choiceList.scrollTop > endPoint) {
continueAnimation = true; continueAnimation = true;
} }
} }
@ -955,11 +954,11 @@ export class Choices {
* @return * @return
* @private * @private
*/ */
_highlightOption(el) { _highlightChoice(el) {
// Highlight first element in dropdown // Highlight first element in dropdown
const options = Array.from(this.dropdown.querySelectorAll('[data-option-selectable]')); const options = Array.from(this.dropdown.querySelectorAll('[data-option-selectable]'));
if(options.length) { if(options && options.length) {
const highlightedOptions = Array.from(this.dropdown.querySelectorAll(`.${this.options.classNames.highlightedState}`)); const highlightedOptions = Array.from(this.dropdown.querySelectorAll(`.${this.options.classNames.highlightedState}`));
// Remove any highlighted options // Remove any highlighted options
@ -996,11 +995,11 @@ export class Choices {
* @return {Object} Class instance * @return {Object} Class instance
* @public * @public
*/ */
_addItem(value, label, optionId = -1, callback = this.options.callbackOnAddItem) { _addItem(value, label, choiceId = -1, callback = this.options.callbackOnAddItem) {
const items = this.store.getItems(); const items = this.store.getItems();
let passedValue = value.trim(); let passedValue = value.trim();
let passedLabel = label || passedValue; let passedLabel = label || passedValue;
let passedOptionId = optionId || -1; let passedOptionId = choiceId || -1;
// If a prepended value has been passed, prepend it // If a prepended value has been passed, prepend it
if(this.options.prependValue) { if(this.options.prependValue) {
@ -1047,9 +1046,9 @@ export class Choices {
const id = item.id; const id = item.id;
const value = item.value; const value = item.value;
const optionId = item.optionId; const choiceId = item.choiceId;
this.store.dispatch(removeItem(id, optionId)); this.store.dispatch(removeItem(id, choiceId));
// Run callback // Run callback
if(callback){ if(callback){
@ -1061,22 +1060,20 @@ export class Choices {
} }
/** /**
* Add option to dropdown * Add choice to dropdoww
* @param {Object} option Option to add
* @param {Number} groupId ID of the options group
* @return * @return
* @private * @private
*/ */
_addOption(isSelected, isDisabled, value, label, groupId = -1) { _addChoice(isSelected, isDisabled, value, label, groupId = -1) {
if(!value) return if(!value) return
if(!label) { label = value; } if(!label) { label = value; }
// Generate unique id // Generate unique id
const options = this.store.getOptions(); const choices = this.store.getChoices();
const id = options.length + 1; const id = choices ? choices.length + 1 : 1;
this.store.dispatch(addOption(value, label, id, groupId, isDisabled)); this.store.dispatch(addChoice(value, label, id, groupId, isDisabled));
if(isSelected && !isDisabled) { if(isSelected && !isDisabled) {
this._addItem(value, label, id); this._addItem(value, label, id);
@ -1098,7 +1095,7 @@ export class Choices {
this.store.dispatch(addGroup(group.label, groupId, true, group.disabled)); this.store.dispatch(addGroup(group.label, groupId, true, group.disabled));
groupOptions.forEach((option, optionIndex) => { groupOptions.forEach((option, optionIndex) => {
const isDisabled = option.disabled || option.parentNode.disabled; const isDisabled = option.disabled || option.parentNode.disabled;
this._addOption(option.selected, isDisabled, option.value, option.innerHTML, groupId); this._addChoice(option.selected, isDisabled, option.value, option.innerHTML, groupId);
}); });
} else { } else {
this.store.dispatch(addGroup(group.label, group.id, false, group.disabled)); this.store.dispatch(addGroup(group.label, group.id, false, group.disabled));
@ -1135,7 +1132,7 @@ export class Choices {
itemList: () => { itemList: () => {
return strToEl(`<div class="${ classNames.list } ${ this.passedElement.type === 'select-one' ? classNames.listSingle : classNames.listItems }"></div>`); return strToEl(`<div class="${ classNames.list } ${ this.passedElement.type === 'select-one' ? classNames.listSingle : classNames.listItems }"></div>`);
}, },
optionList: () => { choiceList: () => {
return strToEl(`<div class="${ classNames.list }"></div>`); return strToEl(`<div class="${ classNames.list }"></div>`);
}, },
input: () => { input: () => {
@ -1165,7 +1162,7 @@ export class Choices {
`); `);
}, },
item: (data) => { item: (data) => {
if(this.options.removeButton && this.passedElement.type !== 'select-one') { if(this.options.removeItemButton && this.passedElement.type !== 'select-one') {
return strToEl(` return strToEl(`
<div class="${ classNames.item } ${ data.selected ? classNames.selectedState : ''} ${ !data.disabled ? classNames.itemSelectable : '' }" data-item data-id="${ data.id }" data-value="${ data.value }" data-deletable> <div class="${ classNames.item } ${ data.selected ? classNames.selectedState : ''} ${ !data.disabled ? classNames.itemSelectable : '' }" data-item data-id="${ data.id }" data-value="${ data.value }" data-deletable>
${ data.label } ${ data.label }
@ -1194,14 +1191,14 @@ export class Choices {
const containerOuter = this._getTemplate('containerOuter'); const containerOuter = this._getTemplate('containerOuter');
const containerInner = this._getTemplate('containerInner'); const containerInner = this._getTemplate('containerInner');
const itemList = this._getTemplate('itemList'); const itemList = this._getTemplate('itemList');
const optionList = this._getTemplate('optionList'); const choiceList = this._getTemplate('choiceList');
const input = this._getTemplate('input'); const input = this._getTemplate('input');
const dropdown = this._getTemplate('dropdown'); const dropdown = this._getTemplate('dropdown');
this.containerOuter = containerOuter; this.containerOuter = containerOuter;
this.containerInner = containerInner; this.containerInner = containerInner;
this.input = input; this.input = input;
this.optionList = optionList; this.choiceList = choiceList;
this.itemList = itemList; this.itemList = itemList;
this.dropdown = dropdown; this.dropdown = dropdown;
@ -1232,11 +1229,11 @@ export class Choices {
containerOuter.appendChild(containerInner); containerOuter.appendChild(containerInner);
containerOuter.appendChild(dropdown); containerOuter.appendChild(dropdown);
containerInner.appendChild(itemList); containerInner.appendChild(itemList);
dropdown.appendChild(optionList); dropdown.appendChild(choiceList);
if(this.passedElement.type === 'select-multiple' || this.passedElement.type === 'text') { if(this.passedElement.type === 'select-multiple' || this.passedElement.type === 'text') {
containerInner.appendChild(input); containerInner.appendChild(input);
} else if(this.options.allowSearch) { } else if(this.options.searchOptions) {
dropdown.insertBefore(input, dropdown.firstChild); dropdown.insertBefore(input, dropdown.firstChild);
} }
@ -1256,7 +1253,7 @@ export class Choices {
const passedOptions = Array.from(this.passedElement.options); const passedOptions = Array.from(this.passedElement.options);
passedOptions.forEach((option) => { passedOptions.forEach((option) => {
const isDisabled = option.disabled || option.parentNode.disabled; const isDisabled = option.disabled || option.parentNode.disabled;
this._addOption(option.selected, isDisabled, option.value, option.innerHTML); this._addChoice(option.selected, isDisabled, option.value, option.innerHTML);
}); });
} }
@ -1391,12 +1388,12 @@ export class Choices {
if(this.passedElement.type === 'select-multiple' || this.passedElement.type === 'select-one') { if(this.passedElement.type === 'select-multiple' || this.passedElement.type === 'select-one') {
// Get active groups/options // Get active groups/options
const activeGroups = this.store.getGroupsFilteredByActive(); const activeGroups = this.store.getGroupsFilteredByActive();
const activeOptions = this.store.getOptionsFilteredByActive(); const activeOptions = this.store.getChoicesFilteredByActive();
let optListFragment = document.createDocumentFragment(); let optListFragment = document.createDocumentFragment();
// Clear options // Clear options
this.optionList.innerHTML = ''; this.choiceList.innerHTML = '';
// If we have grouped options // If we have grouped options
if(activeGroups.length >= 1 && this.isSearching !== true) { if(activeGroups.length >= 1 && this.isSearching !== true) {
@ -1408,12 +1405,12 @@ export class Choices {
if(optListFragment.children.length) { if(optListFragment.children.length) {
// If we actually have anything to add to our dropdown // If we actually have anything to add to our dropdown
// append it and highlight the first option // append it and highlight the first option
this.optionList.appendChild(optListFragment); this.choiceList.appendChild(optListFragment);
this._highlightOption(); this._highlightChoice();
} else { } else {
// Otherwise show a notice // Otherwise show a notice
const dropdownItem = this.isSearching ? this._getTemplate('notice', 'No results found') : this._getTemplate('notice', 'No options to select'); const dropdownItem = this.isSearching ? this._getTemplate('notice', 'No results found') : this._getTemplate('notice', 'No options to select');
this.optionList.appendChild(dropdownItem); this.choiceList.appendChild(dropdownItem);
} }
} }
} }

View file

@ -1,6 +1,6 @@
const options = (state = [], action) => { const choices = (state = [], action) => {
switch (action.type) { switch (action.type) {
case 'ADD_OPTION': case 'ADD_CHOICE':
return [...state, { return [...state, {
id: action.id, id: action.id,
groupId: action.groupId, groupId: action.groupId,
@ -15,9 +15,9 @@ const options = (state = [], action) => {
case 'ADD_ITEM': case 'ADD_ITEM':
// When an item is added and it has an associated option, // When an item is added and it has an associated option,
// we want to disable it so it can't be chosen again // we want to disable it so it can't be chosen again
if(action.optionId > -1) { if(action.choiceId > -1) {
return state.map((option) => { return state.map((option) => {
if(option.id === parseInt(action.optionId)) { if(option.id === parseInt(action.choiceId)) {
option.selected = true; option.selected = true;
} }
return option; return option;
@ -29,9 +29,9 @@ const options = (state = [], action) => {
case 'REMOVE_ITEM': case 'REMOVE_ITEM':
// When an item is removed and it has an associated option, // When an item is removed and it has an associated option,
// we want to re-enable it so it can be chosen again // we want to re-enable it so it can be chosen again
if(action.optionId > -1) { if(action.choiceId > -1) {
return state.map((option) => { return state.map((option) => {
if(option.id === parseInt(action.optionId)) { if(option.id === parseInt(action.choiceId)) {
option.selected = false; option.selected = false;
} }
return option; return option;
@ -40,7 +40,7 @@ const options = (state = [], action) => {
return state; return state;
} }
case 'FILTER_OPTIONS': case 'FILTER_CHOICES':
const filteredResults = action.results; const filteredResults = action.results;
const filteredState = state.map((option, index) => { const filteredState = state.map((option, index) => {
// Set active state based on whether option is // Set active state based on whether option is
@ -60,7 +60,7 @@ const options = (state = [], action) => {
return filteredState; return filteredState;
case 'ACTIVATE_OPTIONS': case 'ACTIVATE_CHOICES':
return state.map((option) => { return state.map((option) => {
option.active = action.active; option.active = action.active;
@ -73,4 +73,4 @@ const options = (state = [], action) => {
} }
} }
export default options; export default choices;

View file

@ -1,12 +1,12 @@
import { combineReducers } from 'redux'; import { combineReducers } from 'redux';
import items from './items'; import items from './items';
import groups from './groups'; import groups from './groups';
import options from './options'; import choices from './choices';
const appReducer = combineReducers({ const appReducer = combineReducers({
items, items,
groups, groups,
options choices
}); });
const rootReducer = (state, action) => { const rootReducer = (state, action) => {

View file

@ -4,7 +4,7 @@ const items = (state = [], action) => {
// Add object to items array // Add object to items array
let newState = [...state, { let newState = [...state, {
id: action.id, id: action.id,
optionId: action.optionId, choiceId: action.choiceId,
value: action.value, value: action.value,
label: action.label, label: action.label,
active: true, active: true,

View file

@ -75,35 +75,35 @@ export class Store {
} }
/** /**
* Get options from store * Get choices from store
* @return {Array} Option objects * @return {Array} Option objects
*/ */
getOptions() { getChoices() {
const state = this.store.getState(); const state = this.store.getState();
return state.options; return state.choices;
} }
/** /**
* Get active options from store * Get active choices from store
* @return {Array} Option objects * @return {Array} Option objects
*/ */
getOptionsFilteredByActive() { getChoicesFilteredByActive() {
const options = this.getOptions(); const choices = this.getChoices();
const values = options.filter((option) => { const values = choices.filter((choice) => {
return option.active === true; return choice.active === true;
},[]); },[]);
return values; return values;
} }
/** /**
* Get selectable options from store * Get selectable choices from store
* @return {Array} Option objects * @return {Array} Option objects
*/ */
getOptionsFiltedBySelectable() { getChoicesFiltedBySelectable() {
const options = this.getOptions(); const choices = this.getChoices();
const values = options.filter((option) => { const values = choices.filter((choice) => {
return option.selected === false && option.disabled !== true; return choice.selected === false && choice.disabled !== true;
},[]); },[]);
return values; return values;
@ -124,11 +124,11 @@ export class Store {
*/ */
getGroupsFilteredByActive() { getGroupsFilteredByActive() {
const groups = this.getGroups(); const groups = this.getGroups();
const options = this.getOptions(); const choices = this.getChoices();
const values = groups.filter((group) => { const values = groups.filter((group) => {
const isActive = group.active === true && group.disabled === false; const isActive = group.active === true && group.disabled === false;
const hasActiveOptions = options.some((option) => { const hasActiveOptions = choices.some((option) => {
return option.active === true && option.disabled === false; return option.active === true && option.disabled === false;
}); });
return isActive && hasActiveOptions ? true : false; return isActive && hasActiveOptions ? true : false;

View file

@ -141,13 +141,13 @@
}); });
const choices2 = new Choices('#choices-2', { const choices2 = new Choices('#choices-2', {
allowPaste: false, paste: false,
allowDuplicates: false, duplicateItems: false,
editItems: true, editItems: true,
}); });
const choices3 = new Choices('#choices-3', { const choices3 = new Choices('#choices-3', {
allowDuplicates: false, duplicates: false,
editItems: true, 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,}))$/, 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,}))$/,
}); });
@ -166,7 +166,7 @@
items: ['josh@joshuajohnson.co.uk', { value: 'joe@bloggs.co.uk', label: 'Joe Bloggs' } ], items: ['josh@joshuajohnson.co.uk', { value: 'joe@bloggs.co.uk', label: 'Joe Bloggs' } ],
}); });
const choices7 = new Choices('#choices-7', { allowSearch: false }).setValue(['Set value 1', 'Set value 2']); const choices7 = new Choices('#choices-7', { Search: false }).setValue(['Set value 1', 'Set value 2']);
const choicesAjax = new Choices('#choices-12').ajax((callback) => { const choicesAjax = new Choices('#choices-12').ajax((callback) => {
fetch('https://api.discogs.com/artists/391170/releases?token=QBRmstCkwXEvCjTclCpumbtNwvVkEzGAdELXyRyW') fetch('https://api.discogs.com/artists/391170/releases?token=QBRmstCkwXEvCjTclCpumbtNwvVkEzGAdELXyRyW')