Merge master

This commit is contained in:
Josh Johnson 2017-04-07 09:06:34 +01:00
commit df4dc82e2c
10 changed files with 112 additions and 49 deletions

1
.gitignore vendored
View file

@ -1,6 +1,7 @@
node_modules node_modules
npm-debug.log npm-debug.log
.DS_Store .DS_Store
.vscode
# Test # Test
tests/reports tests/reports

View file

@ -3,7 +3,7 @@ A vanilla, lightweight (~15kb gzipped 🎉), configurable select box/text input
[Demo](https://joshuajohnson.co.uk/Choices/) [Demo](https://joshuajohnson.co.uk/Choices/)
## TL;DR ## TL;DR
* Lightweight * Lightweight
* No jQuery dependency * No jQuery dependency
* Configurable sorting * Configurable sorting
@ -14,7 +14,7 @@ A vanilla, lightweight (~15kb gzipped 🎉), configurable select box/text input
* Custom templates * Custom templates
---- ----
### Interested in writing your own ES6 JavaScript plugins? Check out [ES6.io](https://ES6.io/friend/JOHNSON) for great tutorials! 💪🏼 ### Interested in writing your own ES6 JavaScript plugins? Check out [ES6.io](https://ES6.io/friend/JOHNSON) for great tutorials! 💪🏼
---- ----
## Installation ## Installation
@ -43,17 +43,17 @@ Or include Choices directly:
```js ```js
// Pass multiple elements: // Pass multiple elements:
const choices = new Choices(elements); const choices = new Choices(elements);
// Pass single element: // Pass single element:
const choices = new Choices(element); const choices = new Choices(element);
// Pass reference // Pass reference
const choices = new Choices('[data-trigger']); const choices = new Choices('[data-trigger']);
const choices = new Choices('.js-choice'); const choices = new Choices('.js-choice');
// Pass jQuery element // Pass jQuery element
const choices = new Choices($('.js-choice')[0]); const choices = new Choices($('.js-choice')[0]);
// Passing options (with default options) // Passing options (with default options)
const choices = new Choices(elements, { const choices = new Choices(elements, {
items: [], items: [],
@ -67,13 +67,15 @@ Or include Choices directly:
delimiter: ',', delimiter: ',',
paste: true, paste: true,
search: true, search: true,
searchChoices: true,
searchFloor: 1, searchFloor: 1,
searchFields: ['label', 'value'],
position: 'auto', position: 'auto',
resetScrollPosition: true, resetScrollPosition: true,
regexFilter: null, regexFilter: null,
shouldSort: true, shouldSort: true,
sortFilter: () => {...}, sortFilter: () => {...},
sortFields: ['label', 'value'], placeholder: true,
placeholderValue: null, placeholderValue: null,
prependValue: null, prependValue: null,
appendValue: null, appendValue: null,
@ -136,23 +138,23 @@ Or include Choices directly:
**Input types affected:** `text` **Input types affected:** `text`
**Usage:** Add pre-selected items (see terminology) to text input. **Usage:** Add pre-selected items (see terminology) to text input.
Pass an array of strings: Pass an array of strings:
`['value 1', 'value 2', 'value 3']` `['value 1', 'value 2', 'value 3']`
Pass an array of objects: Pass an array of objects:
``` ```
[{ [{
value: 'Value 1', value: 'Value 1',
label: 'Label 1', label: 'Label 1',
id: 1 id: 1
}, },
{ {
value: 'Value 2', value: 'Value 2',
label: 'Label 2', label: 'Label 2',
id: 2 id: 2
}] }]
``` ```
@ -162,7 +164,7 @@ Pass an array of objects:
**Input types affected:** `select-one`, `select-multiple` **Input types affected:** `select-one`, `select-multiple`
**Usage:** Add choices (see terminology) to select input. **Usage:** Add choices (see terminology) to select input.
Pass an array of objects: Pass an array of objects:
@ -244,6 +246,21 @@ Pass an array of objects:
**Usage:** Whether a user should be allowed to search avaiable choices. Note that multiple select boxes will always show search inputs. **Usage:** Whether a user should be allowed to search avaiable choices. Note that multiple select boxes will always show search inputs.
### searchChoices
**Type:** `Boolean` **Default:** `true`
**Input types affected:** `select-one`
**Usage:** Whether the plugin should filter the choices by input or not. If `false`, the search event will still emit.
### searchFields
**Type:** `Array/String` **Default:** `['label', 'value']`
**Input types affected:**`select-one`, `select-multiple`
**Usage:** Specify which fields should be used when a user is searching.
### searchFloor ### searchFloor
**Type:** `Number` **Default:** `1` **Type:** `Number` **Default:** `1`
@ -256,7 +273,7 @@ Pass an array of objects:
**Input types affected:** `select-one`, `select-multiple` **Input types affected:** `select-one`, `select-multiple`
**Usage:** Whether the dropdown should appear above (`top`) or below (`bottom`) the input. By default, if there is not enough space within the window the dropdown will appear above the input, otherwise below it. **Usage:** Whether the dropdown should appear above (`top`) or below (`bottom`) the input. By default, if there is not enough space within the window the dropdown will appear above the input, otherwise below it.
### resetScrollPosition ### resetScrollPosition
**Type:** `Boolean` **Default:** `true` **Type:** `Boolean` **Default:** `true`
@ -277,7 +294,7 @@ Pass an array of objects:
**Input types affected:** `select-one`, `select-multiple` **Input types affected:** `select-one`, `select-multiple`
**Usage:** Whether choices should be sorted. If false, choices will appear in the order they were given. **Usage:** Whether choices should be sorted. If false, choices will appear in the order they were given.
### sortFilter ### sortFilter
**Type:** `Function` **Default:** sortByAlpha **Type:** `Function` **Default:** sortByAlpha
@ -297,12 +314,12 @@ const example = new Choices(element, {
}; };
``` ```
### sortFields ### placeholder
**Type:** `Array/String` **Default:** `['label', 'value']` **Type:** `Boolean` **Default:** `true`
**Input types affected:**`select-one`, `select-multiple` **Input types affected:** `text`, `select-one`, `select-multiple`
**Usage:** Specify which fields should be used for sorting when a user is searching. If a user is not searching and sorting is enabled, only the choice's label will be sorted. **Usage:** 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
**Type:** `String` **Default:** `null` **Type:** `String` **Default:** `null`
@ -457,7 +474,7 @@ const example = new Choices(element, {
``` ```
## Events ## Events
**Note:** Events fired by Choices behave the same as standard events. Each event is triggered on the element passed to Choices (accessible via `this.passedElement`. Arguments are accessible within the `event.detail` object. **Note:** Events fired by Choices behave the same as standard events. Each event is triggered on the element passed to Choices (accessible via `this.passedElement`. Arguments are accessible within the `event.detail` object.
**Example:** **Example:**
@ -473,7 +490,7 @@ element.addEventListener('addItem', function(event) {
console.log(event.detail.groupValue); console.log(event.detail.groupValue);
}, false); }, false);
// or // or
const example = new Choices(document.getElementById('example')); const example = new Choices(document.getElementById('example'));
example.passedElement.addEventListener('addItem', function(event) { example.passedElement.addEventListener('addItem', function(event) {
@ -573,7 +590,7 @@ choices.disable();
**Usage:** Creates a new instance of Choices, adds event listeners, creates templates and renders a Choices element to the DOM. **Usage:** Creates a new instance of Choices, adds event listeners, creates templates and renders a Choices element to the DOM.
**Note:** This is called implicitly when a new instance of Choices is created. This would be used after a Choices instance had already been destroyed (using `destroy()`). **Note:** This is called implicitly when a new instance of Choices is created. This would be used after a Choices instance had already been destroyed (using `destroy()`).
### highlightAll(); ### highlightAll();
**Input types affected:** `text`, `select-multiple` **Input types affected:** `text`, `select-multiple`
@ -653,7 +670,7 @@ example.setChoices([{
{value: 'Child Two', label: 'Child Two', disabled: true}, {value: 'Child Two', label: 'Child Two', disabled: true},
{value: 'Child Three', label: 'Child Three'}, {value: 'Child Three', label: 'Child Three'},
] ]
}, },
{ {
label: 'Group two', label: 'Group two',
id: 2, id: 2,
@ -804,7 +821,7 @@ To setup a local environment: clone this repo, navigate into it's directory in a
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
## License ## License
MIT License MIT License
## Misc ## Misc
Thanks to [@mikefrancis](https://github.com/mikefrancis/) for [sending me on a hunt](https://twitter.com/_mikefrancis/status/701797835826667520) for a non-jQuery solution for select boxes that eventually led to this being built! Thanks to [@mikefrancis](https://github.com/mikefrancis/) for [sending me on a hunt](https://twitter.com/_mikefrancis/status/701797835826667520) for a non-jQuery solution for select boxes that eventually led to this being built!

View file

@ -1,4 +1,4 @@
/*! choices.js v2.7.7 | (c) 2017 Josh Johnson | https://github.com/jshjohnson/Choices#readme */ /*! choices.js v2.7.8 | (c) 2017 Josh Johnson | https://github.com/jshjohnson/Choices#readme */
(function webpackUniversalModuleDefinition(root, factory) { (function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object') if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory(); module.exports = factory();
@ -124,13 +124,14 @@ return /******/ (function(modules) { // webpackBootstrap
delimiter: ',', delimiter: ',',
paste: true, paste: true,
search: true, search: true,
searchChoices: true,
searchFloor: 1, searchFloor: 1,
searchFields: ['label', 'value'],
position: 'auto', position: 'auto',
resetScrollPosition: true, resetScrollPosition: true,
regexFilter: null, regexFilter: null,
shouldSort: true, shouldSort: true,
sortFilter: _utils.sortByAlpha, sortFilter: _utils.sortByAlpha,
sortFields: ['label', 'value'],
placeholder: true, placeholder: true,
placeholderValue: null, placeholderValue: null,
prependValue: null, prependValue: null,
@ -1045,6 +1046,9 @@ return /******/ (function(modules) { // webpackBootstrap
this.input.removeAttribute('disabled'); this.input.removeAttribute('disabled');
this.containerOuter.classList.remove(this.config.classNames.disabledState); this.containerOuter.classList.remove(this.config.classNames.disabledState);
this.containerOuter.removeAttribute('aria-disabled'); this.containerOuter.removeAttribute('aria-disabled');
if (this.passedElement.type === 'select-one') {
this.containerOuter.setAttribute('tabindex', '0');
}
} }
return this; return this;
} }
@ -1066,6 +1070,9 @@ return /******/ (function(modules) { // webpackBootstrap
this.input.setAttribute('disabled', ''); this.input.setAttribute('disabled', '');
this.containerOuter.classList.add(this.config.classNames.disabledState); this.containerOuter.classList.add(this.config.classNames.disabledState);
this.containerOuter.setAttribute('aria-disabled', 'true'); this.containerOuter.setAttribute('aria-disabled', 'true');
if (this.passedElement.type === 'select-one') {
this.containerOuter.setAttribute('tabindex', '-1');
}
} }
return this; return this;
} }
@ -1402,7 +1409,7 @@ return /******/ (function(modules) { // webpackBootstrap
if (newValue.length >= 1 && newValue !== currentValue + ' ') { if (newValue.length >= 1 && newValue !== currentValue + ' ') {
var haystack = this.store.getChoicesFilteredBySelectable(); var haystack = this.store.getChoicesFilteredBySelectable();
var needle = newValue; var needle = newValue;
var keys = (0, _utils.isType)('Array', this.config.sortFields) ? this.config.sortFields : [this.config.sortFields]; var keys = (0, _utils.isType)('Array', this.config.searchFields) ? this.config.searchFields : [this.config.searchFields];
var options = Object.assign(this.config.fuseOptions, { keys: keys }); var options = Object.assign(this.config.fuseOptions, { keys: keys });
var fuse = new _fuse2.default(haystack, options); var fuse = new _fuse2.default(haystack, options);
var results = fuse.search(needle); var results = fuse.search(needle);
@ -1434,8 +1441,11 @@ return /******/ (function(modules) { // webpackBootstrap
if (this.input === document.activeElement) { if (this.input === document.activeElement) {
// 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
if (value && value.length > this.config.searchFloor) { if (value && value.length > this.config.searchFloor) {
// Filter available choices // Check flag to filter search input
this._searchChoices(value); if (this.config.searchChoices) {
// Filter available choices
this._searchChoices(value);
}
// Trigger search event // Trigger search event
(0, _utils.triggerEvent)(this.passedElement, 'search', { (0, _utils.triggerEvent)(this.passedElement, 'search', {
value: value value: value

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -60,15 +60,16 @@ class Choices {
delimiter: ',', delimiter: ',',
paste: true, paste: true,
search: true, search: true,
searchChoices: true,
searchFloor: 1, searchFloor: 1,
searchPlaceholderValue: null, searchPlaceholderValue: null,
flip: true, searchFields: ['label', 'value'],
position: 'auto', position: 'auto',
resetScrollPosition: true, resetScrollPosition: true,
regexFilter: null, regexFilter: null,
shouldSort: true, shouldSort: true,
sortFilter: sortByAlpha, sortFilter: sortByAlpha,
sortFields: ['label', 'value'], placeholder: true,
placeholderValue: null, placeholderValue: null,
prependValue: null, prependValue: null,
appendValue: null, appendValue: null,
@ -1224,7 +1225,7 @@ class Choices {
if (newValue.length >= 1 && newValue !== `${currentValue} `) { if (newValue.length >= 1 && newValue !== `${currentValue} `) {
const haystack = this.store.getChoicesFilteredBySelectable(); const haystack = this.store.getChoicesFilteredBySelectable();
const needle = newValue; const needle = newValue;
const keys = isType('Array', this.config.sortFields) ? this.config.sortFields : [this.config.sortFields]; const keys = isType('Array', this.config.searchFields) ? this.config.searchFields : [this.config.searchFields];
const options = Object.assign(this.config.fuseOptions, { keys }); const options = Object.assign(this.config.fuseOptions, { keys });
const fuse = new Fuse(haystack, options); const fuse = new Fuse(haystack, options);
const results = fuse.search(needle); const results = fuse.search(needle);
@ -1251,8 +1252,11 @@ class Choices {
if (this.input === document.activeElement) { if (this.input === document.activeElement) {
// 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
if (value && value.length > this.config.searchFloor) { if (value && value.length > this.config.searchFloor) {
// Filter available choices // Check flag to filter search input
this._searchChoices(value); if (this.config.searchChoices) {
// Filter available choices
this._searchChoices(value);
}
// Trigger search event // Trigger search event
triggerEvent(this.passedElement, 'search', { triggerEvent(this.passedElement, 'search', {
value, value,

View file

@ -1,6 +1,6 @@
{ {
"name": "choices.js", "name": "choices.js",
"version": "2.7.7", "version": "2.7.8",
"description": "A vanilla JS customisable text input/select box plugin", "description": "A vanilla JS customisable text input/select box plugin",
"main": [ "main": [
"./assets/scripts/dist/choices.js", "./assets/scripts/dist/choices.js",

View file

@ -15,7 +15,7 @@
<meta name="theme-color" content="#ffffff"> <meta name="theme-color" content="#ffffff">
<!-- Ignore these --> <!-- Ignore these -->
<link rel="stylesheet" href="assets/styles/css/base.min.css?version=2.7.7"> <link rel="stylesheet" href="assets/styles/css/base.min.css?version=2.7.8">
<!-- End ignore these --> <!-- End ignore these -->
<!-- Optional includes --> <!-- Optional includes -->
@ -23,8 +23,8 @@
<!-- End optional includes --> <!-- End optional includes -->
<!-- Choices includes --> <!-- Choices includes -->
<link rel="stylesheet" href="assets/styles/css/choices.min.css?version=2.7.7"> <link rel="stylesheet" href="assets/styles/css/choices.min.css?version=2.7.8">
<script src="assets/scripts/dist/choices.min.js?version=2.7.7"></script> <script src="assets/scripts/dist/choices.min.js?version=2.7.8"></script>
<!-- End Choices includes --> <!-- End Choices includes -->
<!--[if lt IE 9]> <!--[if lt IE 9]>

View file

@ -1,6 +1,6 @@
{ {
"name": "choices.js", "name": "choices.js",
"version": "2.7.7", "version": "2.7.8",
"description": "A vanilla JS customisable text input/select box plugin", "description": "A vanilla JS customisable text input/select box plugin",
"main": "./assets/scripts/dist/choices.min.js", "main": "./assets/scripts/dist/choices.min.js",
"scripts": { "scripts": {

View file

@ -77,11 +77,10 @@ describe('Choices', () => {
expect(this.choices.config.search).toEqual(jasmine.any(Boolean)); expect(this.choices.config.search).toEqual(jasmine.any(Boolean));
expect(this.choices.config.searchFloor).toEqual(jasmine.any(Number)); expect(this.choices.config.searchFloor).toEqual(jasmine.any(Number));
expect(this.choices.config.searchPlaceholderValue).toEqual(null); expect(this.choices.config.searchPlaceholderValue).toEqual(null);
expect(this.choices.config.flip).toEqual(jasmine.any(Boolean)); expect(this.choices.config.searchFields).toEqual(jasmine.any(Array) || jasmine.any(String));
expect(this.choices.config.position).toEqual(jasmine.any(String)); expect(this.choices.config.position).toEqual(jasmine.any(String));
expect(this.choices.config.regexFilter).toEqual(null); expect(this.choices.config.regexFilter).toEqual(null);
expect(this.choices.config.sortFilter).toEqual(jasmine.any(Function)); expect(this.choices.config.sortFilter).toEqual(jasmine.any(Function));
expect(this.choices.config.sortFields).toEqual(jasmine.any(Array) || jasmine.any(String));
expect(this.choices.config.shouldSort).toEqual(jasmine.any(Boolean)); expect(this.choices.config.shouldSort).toEqual(jasmine.any(Boolean));
expect(this.choices.config.placeholderValue).toEqual(null); expect(this.choices.config.placeholderValue).toEqual(null);
expect(this.choices.config.prependValue).toEqual(null); expect(this.choices.config.prependValue).toEqual(null);
@ -441,7 +440,7 @@ describe('Choices', () => {
passedElement.addEventListener('search', searchSpy); passedElement.addEventListener('search', searchSpy);
this.choices.input.focus(); this.choices.input.focus();
this.choices.input.value = 'Value 3'; this.choices.input.value = '3 ';
// Key down to search // Key down to search
this.choices._onKeyUp({ this.choices._onKeyUp({
@ -450,9 +449,41 @@ describe('Choices', () => {
ctrlKey: false ctrlKey: false
}); });
const mostAccurateResult = this.choices.currentState.choices[0]; const mostAccurateResult = this.choices.currentState.choices.filter(function (choice) {
return choice.active;
});
expect(this.choices.isSearching && mostAccurateResult.value === 'Value 3').toBeTruthy; expect(this.choices.isSearching && mostAccurateResult[0].value === 'Value 3').toBe(true);
expect(searchSpy).toHaveBeenCalled();
});
it('shouldn\'t filter choices when searching', function() {
this.choices = new Choices(this.input, {
searchChoices: false
});
this.choices.setValue(['Javascript', 'HTML', 'Jasmine']);
const searchSpy = jasmine.createSpy('searchSpy');
const passedElement = this.choices.passedElement;
passedElement.addEventListener('search', searchSpy);
this.choices.input.focus();
this.choices.input.value = 'Javascript';
// Key down to search
this.choices._onKeyUp({
target: this.choices.input,
keyCode: 13,
ctrlKey: false
});
const activeOptions = this.choices.currentState.choices.filter(function (choice) {
return choice.active;
});
expect(activeOptions.length).toEqual(this.choices.currentState.choices.length);
expect(searchSpy).toHaveBeenCalled(); expect(searchSpy).toHaveBeenCalled();
}); });