Documentation + minor fixes

This commit is contained in:
Josh Johnson 2016-06-21 23:06:23 +01:00
parent c9671c4625
commit 6a9f2cb354
4 changed files with 264 additions and 181 deletions

210
README.md
View file

@ -7,18 +7,18 @@ Coming soon.
```html
<script src="/assets/js/dist/choices.min.js"></script>
<script>
// Pass multiple elements:
var choices = new Choices(elements);
// Pass single element:
var choice = new Choices(element);
// Pass reference
var choice = new Choices('[data-choice']);
var choice = new Choices('.js-choice');
// Passing options
var choices = new Choices(elements, {
// Pass multiple elements:
var choices = new Choices(elements);
// Pass single element:
var choice = new Choices(element);
// Pass reference
var choice = new Choices('[data-choice']);
var choice = new Choices('.js-choice');
// Passing options
var choices = new Choices(elements, {
items: [],
addItems: true,
removeItems: true,
@ -45,126 +45,206 @@ To install via NPM, run `npm install --save-dev choices.js`
## Options
#### items
Type: `` Default: ``
<strong>Type:</strong> `Array` <strong>Default:</strong> `[]`
Usage:
Usage: Add pre-selected items to input.
Pass an array of strings:
`['value 1', 'value 2', 'value 3']`
Pass an array of objects:
```
[{
value: 'Value 1',
label: 'Label 1',
id: 1
},
{
value: 'Value 2',
label: 'Label 2',
id: 2
}]
```
#### addItems
Type: `` Default: ``
<strong>Type:</strong> `Boolean` <strong>Default:</strong>`true`
Usage:
<strong>Usage:</strong> Whether a user can add items.
#### removeItems
Type: `` Default: ``
<strong>Type:</strong> `Boolean` <strong>Default:</strong>`true`
Usage:
<strong>Usage:</strong> Whether a user can remove items (only affects text and multiple select input types).
#### removeButton
Type: `` Default: ``
<strong>Type:</strong> `Boolean` <strong>Default:</strong>`false`
Usage:
<strong>Usage:</strong> Whether a button should show that, when clicked, will remove an item (only affects text and multiple select input types).
#### editItems
Type: `` Default: ``
<strong>Type:</strong> `Boolean` <strong>Default:</strong>`false`
Usage:
<strong>Usage:</strong> Whether a user can edit selected items (only affects text input types).
#### maxItems
Type: `` Default: ``
<strong>Type:</strong> `Boolean` <strong>Default:</strong>`null`
Usage:
<strong>Usage:</strong> Optionally set an item limit.
#### delimiter
Type: `` Default: ``
<strong>Type:</strong> `String` <strong>Default:</strong>`,`
Usage:
<strong>Usage:</strong> What divides each value (only affects text input types).
#### allowDuplicates
Type: `` Default: ``
<strong>Type:</strong> `Boolean` <strong>Default:</strong>`true`
Usage:
<strong>Usage:</strong> Whether a user can input a duplicate item (only affects text input types).
#### allowPaste
Type: `` Default: ``
<strong>Type:</strong> `Boolean` <strong>Default:</strong>`true`
Usage:
<strong>Usage:</strong> Whether a user can paste into the input.
#### allowSearch
Type: `` Default: ``
<strong>Type:</strong> `Boolean` <strong>Default:</strong>`true`
Usage:
<strong>Usage:</strong> Whether a user can filter options by searching (only affects select input types).
#### regexFilter
Type: `` Default: ``
<strong>Type:</strong> `Regex` <strong>Default:</strong>`null`
Usage:
<strong>Usage:</strong> A filter that will need to pass for a user to successfully add an item (only affects text input types).
#### placeholder
Type: `` Default: ``
<strong>Type:</strong> `Boolean` <strong>Default:</strong>`true`
Usage:
<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
Type: `` Default: ``
<strong>Type:</strong> `String` <strong>Default:</strong>`null`
Usage:
<strong>Usage:</strong> The value of the inputs placeholder.
#### prependValue
Type: `` Default: ``
<strong>Type:</strong> `String` <strong>Default:</strong>`null`
Usage:
<strong>Usage:</strong> Prepend a value to each item added to input (only affects text input types).
#### appendValue
Type: `` Default: ``
<strong>Type:</strong> `String` <strong>Default:</strong>`null`
Usage:
<strong>Usage:</strong> Append a value to each item added to input (only affects text input types).
#### selectAll
Type: `` Default: ``
#### highlightAll
<strong>Type:</strong> `Boolean` <strong>Default:</strong>`true`
Usage:
<strong>Usage:</strong> Whether a user can highlight items.
#### loadingText
Type: `` Default: ``
<strong>Type:</strong> `String` <strong>Default:</strong>`Loading...`
Usage:
#### templates
Type: `` Default: ``
Usage:
<strong>Usage:</strong> The loading text that is shown when options are populated via an AJAX callback.
#### classNames
Type: `` Default: ``
<strong>Type:</strong> `Object` <strong>Default:</strong>
Usage:
```
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',
}
```
<strong>Usage:</strong> Classes added to HTML generated by Choices.
#### callbackOnInit
Type: `` Default: ``
<strong>Type:</strong> `Function` <strong>Default:</strong>`() => {}`
Usage:
<strong>Usage:</strong> Function to run once Choices initialises.
#### callbackOnAddItem
Type: `` Default: ``
<strong>Type:</strong> `Function` <strong>Default:</strong>`(id, value, passedInput) => {}`
Usage:
<strong>Usage:</strong> Function to run each time an item is added.
#### callbackOnRemoveItem
Type: `` Default: ``
<strong>Type:</strong> `Function` <strong>Default:</strong>`(id, value, passedInput) => {}`
Usage:
<strong>Usage:</strong> Function to run each time an item is removed.
## Methods
#### method();
Usage:
#### `highlightAll();`
<strong>Usage:</strong> Highlight each chosen item (selected items can be removed).
#### `unhighlightAll();`
<strong>Usage:</strong> Un-highlight each chosen item.
#### `removeItemsByValue(value);`
<strong>Usage:</strong> Remove each item by a given value.
#### `removeActiveItems(excludedId);`
<strong>Usage:</strong> Remove each selectable item.
#### `removeSelectedItems();`
<strong>Usage:</strong> Remove each item the user has selected.
#### `showDropdown();`
<strong>Usage:</strong> Show option list dropdown
#### `hideDropdown();`
<strong>Usage:</strong> Hide option list dropdown
#### `toggleDropdown();`
<strong>Usage:</strong> Toggle dropdown between showing/hidden.
#### `setValue(args);`
<strong>Usage:</strong> Set value of input based on an array of objects or strings.
#### `clearValue();`
<strong>Usage:</strong> Clear value of input.
#### `clearInput();`
<strong>Usage:</strong> Clear input.
#### `disable();`
<strong>Usage:</strong> Disable input from selecting further options.
#### `ajax(fn);`
<strong>Usage:</strong> Populate options via a callback.
## Browser compatibility
Coming soon
ES5 browsers and above (http://caniuse.com/#feat=es5).
## Development
To setup a local environment: clone this repo, navigate into it's directory in a terminal window and run the following command:
* ```npm install```
```npm install```
### NPM tasks
* ```npm start```
@ -172,7 +252,7 @@ To setup a local environment: clone this repo, navigate into it's directory in a
* ```npm run css:watch```
## 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 Gulp...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
## License
MIT License

File diff suppressed because one or more lines are too long

View file

@ -33,17 +33,17 @@ export class Choices {
removeItems: true,
removeButton: false,
editItems: false,
maxItems: false,
maxItems: null,
delimiter: ',',
allowDuplicates: true,
allowPaste: true,
allowSearch: true,
_regexFilter: false,
regexFilter: null,
placeholder: true,
placeholderValue: '',
prependValue: false,
appendValue: false,
selectAll: true,
placeholderValue: null,
prependValue: null,
appendValue: null,
highlightAll: true,
loadingText: 'Loading...',
templates: {},
classNames: {
@ -80,7 +80,7 @@ export class Choices {
this.options = extend(defaultOptions, userOptions);
// Create data store
this.store = new Store(this._render);
this.store = new Store(this.render);
// State tracking
this.initialised = false;
@ -103,7 +103,7 @@ export class Choices {
// Bind methods
this.init = this.init.bind(this);
this._render = this._render.bind(this);
this.render = this.render.bind(this);
this.destroy = this.destroy.bind(this);
this.disable = this.disable.bind(this);
@ -148,10 +148,10 @@ export class Choices {
// Generate input markup
this._createInput();
this.store.subscribe(this._render);
this.store.subscribe(this.render);
// Render any items
this._render();
this.render();
// Trigger event listeners
this._addEventListeners();
@ -217,11 +217,11 @@ export class Choices {
}
/**
* Select items within store
* Highlight items within store
* @return {Object} Class instance
* @public
*/
selectAll() {
highlightAll() {
const items = this.store.getItems();
items.forEach((item) => {
this.selectItem(item);
@ -244,76 +244,6 @@ export class Choices {
return this;
}
/**
* Add item to store with correct value
* @param {String} value Value to add to store
* @return {Object} Class instance
* @public
*/
addItem(value, label, optionId = -1, callback = this.options.callbackOnAddItem) {
const items = this.store.getItems();
let passedValue = value.trim();
let passedLabel = label || passedValue;
let passedOptionId = optionId || -1;
// If a prepended value has been passed, prepend it
if(this.options.prependValue) {
passedValue = this.options.prependValue + passedValue.toString();
}
// If an appended value has been passed, append it
if(this.options.appendValue) {
passedValue = passedValue + this.options.appendValue.toString();
}
// Generate unique id
const id = items ? items.length + 1 : 1;
this.store.dispatch(addItem(passedValue, passedLabel, id, passedOptionId));
if(this.passedElement.type === 'select-one') {
this.removeActiveItems(id);
}
// Run callback if it is a function
if(callback){
if(isType('Function', callback)) {
callback(id, passedValue, this.passedElement);
} else {
console.error('callbackOnAddItem: Callback is not a function');
}
}
return this;
}
/**
* Remove item from store
* @param
* @return {Object} Class instance
* @public
*/
removeItem(item, callback = this.options.callbackOnRemoveItem) {
if(!item || !isType('Object', item)) {
console.error('removeItem: No item object was passed to be removed');
return;
}
const id = item.id;
const value = item.value;
const optionId = item.optionId;
this.store.dispatch(removeItem(id, optionId));
// Run callback
if(callback){
if(!isType('Function', callback)) console.error('callbackOnRemoveItem: Callback is not a function'); return;
callback(id, value, this.passedElement);
}
return this;
}
/**
* Remove an item from the store by its value
* @param {String} value Value to search for
@ -327,7 +257,7 @@ export class Choices {
items.forEach((item) => {
if(item.value === value) {
this.removeItem(item);
this._removeItem(item);
}
});
@ -337,7 +267,7 @@ export class Choices {
/**
* Remove all items from store array
* Note: removed items are soft deleted
* @param {Boolean} selectedOnly Optionally remove only selected items
* @param {Number} excludedId Optionally exclude item by ID
* @return {Object} Class instance
* @public
*/
@ -346,7 +276,7 @@ export class Choices {
items.forEach((item) => {
if(item.active && excludedId !== item.id) {
this.removeItem(item);
this._removeItem(item);
}
});
@ -364,7 +294,7 @@ export class Choices {
items.forEach((item) => {
if(item.selected && item.active) {
this.removeItem(item);
this._removeItem(item);
}
});
@ -443,13 +373,13 @@ export class Choices {
if(this.passedElement.type !== 'text') {
this._addOption(true, false, item.value, item.label, -1);
} else {
this.addItem(item.value, item.label, item.id);
this._addItem(item.value, item.label, item.id);
}
} else if(isType('String', item)) {
if(this.passedElement.type !== 'text') {
this._addOption(true, false, item, item, -1);
} else {
this.addItem(item);
this._addItem(item);
}
}
});
@ -501,7 +431,7 @@ export class Choices {
results.forEach((result, index) => {
// Add each result to option dropdown
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]);
});
@ -553,7 +483,7 @@ export class Choices {
let canAddItem = true;
// If a user has supplied a regular expression filter
if(this.options._regexFilter) {
if(this.options.regexFilter) {
// Determine whether we can update based on whether
// our regular expression passes
canAddItem = this._regexFilter(value);
@ -562,7 +492,7 @@ export class Choices {
// All is good, add
if(canAddItem) {
this.toggleDropdown();
this.addItem(value);
this._addItem(value);
this.clearInput(this.passedElement);
}
}
@ -583,7 +513,7 @@ export class Choices {
// we can edit the item value. Otherwise if we can remove items, remove all selected items
if(this.options.editItems && !hasSelectedItems && lastItem) {
this.input.value = lastItem.value;
this.removeItem(lastItem);
this._removeItem(lastItem);
} else {
if(!hasSelectedItems) { this.selectItem(lastItem); }
this.removeSelectedItems();
@ -628,8 +558,9 @@ export class Choices {
// If CTRL + A or CMD + A have been pressed and there are items to select
if(ctrlDownKey && hasItems) {
this.canSearch = false;
if(this.options.removeItems && !this.input.value && this.options.selectAll && this.input === document.activeElement) {
this.selectAll(this.itemList.children);
if(this.options.removeItems && !this.input.value && this.options.highlightAll && this.input === document.activeElement) {
// Highlight items
this.highlightAll(this.itemList.children);
}
}
break;
@ -648,7 +579,7 @@ export class Choices {
const value = highlighted.getAttribute('data-value');
const label = highlighted.innerHTML;
const id = highlighted.getAttribute('data-id');
this.addItem(value, label, id);
this._addItem(value, label, id);
this.clearInput(this.passedElement);
if(this.passedElement.type === 'select-one') {
@ -738,7 +669,7 @@ export class Choices {
dropdownItem = this._getTemplate('notice', `Add "${ this.input.value }"`);
}
if((this.options._regexFilter && this._regexFilter(this.input.value)) || !this.options._regexFilter) {
if((this.options.regexFilter && this._regexFilter(this.input.value)) || !this.options.regexFilter) {
this.dropdown.innerHTML = dropdownItem.outerHTML;
if(!this.dropdown.classList.contains(this.options.classNames.activeState)) {
this.showDropdown();
@ -835,7 +766,7 @@ export class Choices {
if(this.options.removeItems && this.options.removeButton) {
const itemId = e.target.parentNode.getAttribute('data-id');
const itemToRemove = activeItems.find((item) => item.id === parseInt(itemId));
this.removeItem(itemToRemove);
this._removeItem(itemToRemove);
}
} else if(e.target.hasAttribute('data-item')) {
// If we are clicking on an item
@ -860,7 +791,7 @@ export class Choices {
const option = options.find((option) => option.id === parseInt(id));
if(!option.selected && !option.disabled) {
this.addItem(option.value, option.label, option.id);
this._addItem(option.value, option.label, option.id);
if(this.passedElement.type === 'select-one') {
this.input.value = "";
this.isSearching = false;
@ -875,8 +806,8 @@ export class Choices {
const hasActiveDropdown = this.dropdown.classList.contains(this.options.classNames.activeState);
const hasSelectedItems = activeItems.some((item) => item.selected === true);
// De-select any selected items
if(hasSelectedItems) this.deselectAll();
// De-select any highlighted items
if(hasSelectedItems) this.unhighlightAll();
// Remove focus state
this.containerOuter.classList.remove(this.options.classNames.focusState);
@ -957,7 +888,7 @@ export class Choices {
*/
_regexFilter(value) {
if(!value) return;
const expression = new RegExp(this.options._regexFilter, 'i');
const expression = new RegExp(this.options.regexFilter, 'i');
return expression.test(value);
}
@ -1058,6 +989,77 @@ export class Choices {
}
}
/**
* Add item to store with correct value
* @param {String} value Value to add to store
* @return {Object} Class instance
* @public
*/
_addItem(value, label, optionId = -1, callback = this.options.callbackOnAddItem) {
const items = this.store.getItems();
let passedValue = value.trim();
let passedLabel = label || passedValue;
let passedOptionId = optionId || -1;
// If a prepended value has been passed, prepend it
if(this.options.prependValue) {
passedValue = this.options.prependValue + passedValue.toString();
}
// If an appended value has been passed, append it
if(this.options.appendValue) {
passedValue = passedValue + this.options.appendValue.toString();
}
// Generate unique id
const id = items ? items.length + 1 : 1;
this.store.dispatch(addItem(passedValue, passedLabel, id, passedOptionId));
if(this.passedElement.type === 'select-one') {
this.removeActiveItems(id);
}
// Run callback if it is a function
if(callback){
if(isType('Function', callback)) {
callback(id, passedValue, this.passedElement);
} else {
console.error('callbackOnAddItem: Callback is not a function');
}
}
return this;
}
/**
* Remove item from store
* @param
* @return {Object} Class instance
* @public
*/
_removeItem(item, callback = this.options.callbackOnRemoveItem) {
if(!item || !isType('Object', item)) {
console.error('removeItem: No item object was passed to be removed');
return;
}
const id = item.id;
const value = item.value;
const optionId = item.optionId;
this.store.dispatch(removeItem(id, optionId));
// Run callback
if(callback){
if(!isType('Function', callback)) console.error('callbackOnRemoveItem: Callback is not a function'); return;
callback(id, value, this.passedElement);
}
return this;
}
/**
* Add option to dropdown
* @param {Object} option Option to add
@ -1077,7 +1079,7 @@ export class Choices {
this.store.dispatch(addOption(value, label, id, groupId, isDisabled));
if(isSelected && !isDisabled) {
this.addItem(value, label, id);
this._addItem(value, label, id);
}
}
@ -1163,7 +1165,7 @@ export class Choices {
`);
},
item: (data) => {
if(this.options.removeButton) {
if(this.options.removeButton && this.passedElement.type !== 'select-one') {
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>
${ data.label }
@ -1263,9 +1265,9 @@ export class Choices {
this.presetItems.forEach((item) => {
if(isType('Object', item)) {
if(!item.value) return;
this.addItem(item.value, item.label, item.id);
this._addItem(item.value, item.label, item.id);
} else if(isType('String', item)) {
this.addItem(item);
this._addItem(item);
}
});
}
@ -1279,7 +1281,7 @@ export class Choices {
* @return {DocumentFragment} Populated options fragment
* @private
*/
_renderGroups(groups, options, fragment) {
renderGroups(groups, options, fragment) {
const groupFragment = fragment || document.createDocumentFragment();
groups.forEach((group, i) => {
@ -1297,7 +1299,7 @@ export class Choices {
groupFragment.appendChild(dropdownGroup);
this._renderOptions(groupOptions, groupFragment);
this.renderOptions(groupOptions, groupFragment);
}
});
@ -1311,7 +1313,7 @@ export class Choices {
* @return {DocumentFragment} Populated options fragment
* @private
*/
_renderOptions(options, fragment) {
renderOptions(options, fragment) {
// Create a fragment to store our list items (so we don't have to update the DOM for each item)
const optsFragment = fragment || document.createDocumentFragment();
@ -1335,7 +1337,7 @@ export class Choices {
* @return
* @private
*/
_renderItems(items, fragment) {
renderItems(items, fragment) {
// Create fragment to add elements to
const itemListFragment = fragment || document.createDocumentFragment();
// Simplify store data to just values
@ -1378,10 +1380,10 @@ export class Choices {
* @return
* @private
*/
_render() {
render() {
this.currentState = this.store.getState();
// Only _render if our state has actually changed
// Only render if our state has actually changed
if(this.currentState !== this.prevState) {
// Options
@ -1398,9 +1400,9 @@ export class Choices {
// If we have grouped options
if(activeGroups.length >= 1 && this.isSearching !== true) {
optListFragment = this._renderGroups(activeGroups, activeOptions, optListFragment);
optListFragment = this.renderGroups(activeGroups, activeOptions, optListFragment);
} else if(activeOptions.length >= 1) {
optListFragment = this._renderOptions(activeOptions, optListFragment);
optListFragment = this.renderOptions(activeOptions, optListFragment);
}
if(optListFragment.children.length) {
@ -1421,7 +1423,7 @@ export class Choices {
const activeItems = this.store.getItemsFilteredByActive();
if(activeItems) {
// Create a fragment to store our list items (so we don't have to update the DOM for each item)
const itemListFragment = this._renderItems(activeItems);
const itemListFragment = this.renderItems(activeItems);
// Clear list
this.itemList.innerHTML = '';

View file

@ -182,6 +182,7 @@
const choicesMultiple = new Choices('[data-choice]', {
placeholderValue: 'This is a placeholder set in the config',
removeButton: true
});
});