mirror of
https://github.com/Choices-js/Choices.git
synced 2024-05-10 01:26:34 +02:00
Merge branch 'adammockor-feature/select-placeholders' into feature/select-placeholders
This commit is contained in:
commit
4070b6842f
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -1,6 +1,7 @@
|
|||
node_modules
|
||||
npm-debug.log
|
||||
.DS_Store
|
||||
.vscode
|
||||
|
||||
# Test
|
||||
tests/reports
|
||||
|
|
120
README.md
120
README.md
|
@ -1,9 +1,9 @@
|
|||
# Choices.js [![Build Status](https://travis-ci.org/jshjohnson/Choices.svg?branch=master)](https://travis-ci.org/jshjohnson/Choices)
|
||||
# Choices.js ![Build Status](https://travis-ci.org/jshjohnson/Choices.svg?branch=master)
|
||||
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/)
|
||||
|
||||
## TL;DR
|
||||
## TL;DR
|
||||
* Lightweight
|
||||
* No jQuery dependency
|
||||
* Configurable sorting
|
||||
|
@ -13,6 +13,10 @@ A vanilla, lightweight (~15kb gzipped 🎉), configurable select box/text input
|
|||
* Right-to-left support
|
||||
* Custom templates
|
||||
|
||||
----
|
||||
### Interested in writing your own ES6 JavaScript plugins? Check out [ES6.io](https://ES6.io/friend/JOHNSON) for great tutorials! 💪🏼
|
||||
----
|
||||
|
||||
## Installation
|
||||
With [NPM](https://www.npmjs.com/package/choices.js):
|
||||
```zsh
|
||||
|
@ -39,17 +43,17 @@ Or include Choices directly:
|
|||
```js
|
||||
// Pass multiple elements:
|
||||
const choices = new Choices(elements);
|
||||
|
||||
|
||||
// Pass single element:
|
||||
const choices = new Choices(element);
|
||||
|
||||
|
||||
// Pass reference
|
||||
const choices = new Choices('[data-trigger']);
|
||||
const choices = new Choices('.js-choice');
|
||||
|
||||
// Pass jQuery element
|
||||
const choices = new Choices($('.js-choice')[0]);
|
||||
|
||||
|
||||
// Passing options (with default options)
|
||||
const choices = new Choices(elements, {
|
||||
items: [],
|
||||
|
@ -63,12 +67,15 @@ Or include Choices directly:
|
|||
delimiter: ',',
|
||||
paste: true,
|
||||
search: true,
|
||||
searchChoices: true,
|
||||
searchFloor: 1,
|
||||
flip: true,
|
||||
searchFields: ['label', 'value'],
|
||||
position: 'auto',
|
||||
resetScrollPosition: true,
|
||||
regexFilter: null,
|
||||
shouldSort: true,
|
||||
sortFilter: () => {...},
|
||||
sortFields: ['label', 'value'],
|
||||
placeholder: true,
|
||||
placeholderValue: null,
|
||||
prependValue: null,
|
||||
appendValue: null,
|
||||
|
@ -131,23 +138,23 @@ Or include Choices directly:
|
|||
|
||||
**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']`
|
||||
|
||||
Pass an array of objects:
|
||||
|
||||
```
|
||||
[{
|
||||
[{
|
||||
value: 'Value 1',
|
||||
label: 'Label 1',
|
||||
id: 1
|
||||
label: 'Label 1',
|
||||
id: 1
|
||||
},
|
||||
{
|
||||
{
|
||||
value: 'Value 2',
|
||||
label: 'Label 2',
|
||||
label: 'Label 2',
|
||||
id: 2
|
||||
}]
|
||||
```
|
||||
|
@ -157,7 +164,7 @@ Pass an array of objects:
|
|||
|
||||
**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:
|
||||
|
||||
|
@ -239,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.
|
||||
|
||||
### 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
|
||||
**Type:** `Number` **Default:** `1`
|
||||
|
||||
|
@ -246,12 +268,19 @@ Pass an array of objects:
|
|||
|
||||
**Usage:** The minimum length a search value should be before choices are searched.
|
||||
|
||||
### flip
|
||||
**Type:** `Boolean` **Default:** `true`
|
||||
### position
|
||||
**Type:** `String` **Default:** `auto`
|
||||
|
||||
**Input types affected:** `select-one`, `select-multiple`
|
||||
|
||||
**Usage:** Whether the dropdown should appear above the input (rather than beneath) if there is not enough space within the window.
|
||||
**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
|
||||
**Type:** `Boolean` **Default:** `true`
|
||||
|
||||
**Input types affected:** `select-multiple`
|
||||
|
||||
**Usage:** Whether the scroll position should reset after adding an item.
|
||||
|
||||
### regexFilter
|
||||
**Type:** `Regex` **Default:** `null`
|
||||
|
@ -265,7 +294,7 @@ Pass an array of objects:
|
|||
|
||||
**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
|
||||
**Type:** `Function` **Default:** sortByAlpha
|
||||
|
@ -285,12 +314,12 @@ const example = new Choices(element, {
|
|||
};
|
||||
```
|
||||
|
||||
### sortFields
|
||||
**Type:** `Array/String` **Default:** `['label', 'value']`
|
||||
### placeholder
|
||||
**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
|
||||
**Type:** `String` **Default:** `null`
|
||||
|
@ -332,18 +361,18 @@ const example = new Choices(element, {
|
|||
**Usage:** The text that is shown whilst choices are being populated via AJAX.
|
||||
|
||||
### noResultsText
|
||||
**Type:** `String` **Default:** `No results found`
|
||||
**Type:** `String/Function` **Default:** `No results found`
|
||||
|
||||
**Input types affected:** `select-one`, `select-multiple`
|
||||
|
||||
**Usage:** The text that is shown when a user's search has returned no results.
|
||||
**Usage:** The text that is shown when a user's search has returned no results. Optionally pass a function returning a string.
|
||||
|
||||
### noChoicesText
|
||||
**Type:** `String` **Default:** `No choices to choose from`
|
||||
**Type:** `String/Function` **Default:** `No choices to choose from`
|
||||
|
||||
**Input types affected:** `select-multiple`
|
||||
|
||||
**Usage:** The text that is shown when a user has selected all possible choices.
|
||||
**Usage:** The text that is shown when a user has selected all possible choices. Optionally pass a function returning a string.
|
||||
|
||||
### itemSelectText
|
||||
**Type:** `String` **Default:** `Press to select`
|
||||
|
@ -445,7 +474,7 @@ const example = new Choices(element, {
|
|||
```
|
||||
|
||||
## 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:**
|
||||
|
||||
|
@ -457,49 +486,58 @@ element.addEventListener('addItem', function(event) {
|
|||
// do something creative here...
|
||||
console.log(event.detail.id);
|
||||
console.log(event.detail.value);
|
||||
console.log(event.detail.label);
|
||||
console.log(event.detail.groupValue);
|
||||
}, false);
|
||||
|
||||
// or
|
||||
// or
|
||||
const example = new Choices(document.getElementById('example'));
|
||||
|
||||
example.passedElement.addEventListener('addItem', function(event) {
|
||||
// do something creative here...
|
||||
console.log(event.detail.id);
|
||||
console.log(event.detail.value);
|
||||
console.log(event.detail.label);
|
||||
console.log(event.detail.groupValue);
|
||||
}, false);
|
||||
|
||||
```
|
||||
|
||||
### addItem
|
||||
**Arguments:** `id, value, groupValue`
|
||||
**Arguments:** `id, value, label, groupValue`
|
||||
|
||||
**Input types affected:** `text`, `select-one`, `select-multiple`
|
||||
|
||||
**Usage:** Triggered each time an item is added (programmatically or by the user).
|
||||
|
||||
### removeItem
|
||||
**Arguments:** `id, value, groupValue`
|
||||
**Arguments:** `id, value, label, groupValue`
|
||||
|
||||
**Input types affected:** `text`, `select-one`, `select-multiple`
|
||||
|
||||
**Usage:** Triggered each time an item is removed (programmatically or by the user).
|
||||
|
||||
### highlightItem
|
||||
**Arguments:** `id, value, groupValue`
|
||||
**Arguments:** `id, value, label, groupValue`
|
||||
|
||||
**Input types affected:** `text`, `select-multiple`
|
||||
|
||||
**Usage:** Triggered each time an item is highlighted.
|
||||
|
||||
### unhighlightItem
|
||||
**Arguments:** `id, value, groupValue`
|
||||
**Arguments:** `id, value, label, groupValue`
|
||||
|
||||
**Input types affected:** `text`, `select-multiple`
|
||||
|
||||
**Usage:** Triggered each time an item is unhighlighted.
|
||||
|
||||
### choice
|
||||
**Arguments:** `value`
|
||||
|
||||
**Input types affected:** `select-one`, `select-multiple`
|
||||
|
||||
**Usage:** Triggered each time a choice is selected **by a user**, regardless if it changes the value of the input.
|
||||
|
||||
### change
|
||||
**Arguments:** `value`
|
||||
|
||||
|
@ -512,6 +550,16 @@ example.passedElement.addEventListener('addItem', function(event) {
|
|||
|
||||
**Usage:** Triggered when a user types into an input to search choices.
|
||||
|
||||
### showDropdown
|
||||
**Arguments:** - **Input types affected:** `select-one`, `select-multiple`
|
||||
|
||||
**Usage:** Triggered when the dropdown is shown.
|
||||
|
||||
### hideDropdown
|
||||
**Arguments:** - **Input types affected:** `select-one`, `select-multiple`
|
||||
|
||||
**Usage:** Triggered when the dropdown is hidden.
|
||||
|
||||
## Methods
|
||||
Methods can be called either directly or by chaining:
|
||||
|
||||
|
@ -542,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.
|
||||
|
||||
**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();
|
||||
**Input types affected:** `text`, `select-multiple`
|
||||
|
@ -622,7 +670,7 @@ example.setChoices([{
|
|||
{value: 'Child Two', label: 'Child Two', disabled: true},
|
||||
{value: 'Child Three', label: 'Child Three'},
|
||||
]
|
||||
},
|
||||
},
|
||||
{
|
||||
label: 'Group two',
|
||||
id: 2,
|
||||
|
@ -773,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
|
||||
|
||||
## License
|
||||
MIT License
|
||||
MIT License
|
||||
|
||||
## 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!
|
||||
|
|
1025
assets/scripts/dist/choices.js
vendored
1025
assets/scripts/dist/choices.js
vendored
File diff suppressed because it is too large
Load diff
2
assets/scripts/dist/choices.js.map
vendored
2
assets/scripts/dist/choices.js.map
vendored
File diff suppressed because one or more lines are too long
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
|
@ -16,6 +16,7 @@ import {
|
|||
isScrolledIntoView,
|
||||
getAdjacentEl,
|
||||
wrap,
|
||||
getType,
|
||||
isType,
|
||||
isElement,
|
||||
strToEl,
|
||||
|
@ -24,6 +25,7 @@ import {
|
|||
sortByAlpha,
|
||||
sortByScore,
|
||||
triggerEvent,
|
||||
findAncestorByAttrName
|
||||
}
|
||||
from './lib/utils.js';
|
||||
import './lib/polyfills.js';
|
||||
|
@ -58,13 +60,16 @@ class Choices {
|
|||
delimiter: ',',
|
||||
paste: true,
|
||||
search: true,
|
||||
searchChoices: true,
|
||||
searchFloor: 1,
|
||||
searchPlaceholderValue: null,
|
||||
flip: true,
|
||||
searchFields: ['label', 'value'],
|
||||
position: 'auto',
|
||||
resetScrollPosition: true,
|
||||
regexFilter: null,
|
||||
shouldSort: true,
|
||||
sortFilter: sortByAlpha,
|
||||
sortFields: ['label', 'value'],
|
||||
placeholder: true,
|
||||
placeholderValue: null,
|
||||
prependValue: null,
|
||||
appendValue: null,
|
||||
|
@ -308,12 +313,26 @@ class Choices {
|
|||
const choicesFragment = fragment || document.createDocumentFragment();
|
||||
const filter = this.isSearching ? sortByScore : this.config.sortFilter;
|
||||
|
||||
// Split array into placeholedrs and "normal" choices
|
||||
const { placeholderChoices, normalChoices } = choices.reduce((acc, choice) => {
|
||||
if (choice.placeholder) {
|
||||
acc.placeholderChoices.push(choice);
|
||||
} else {
|
||||
acc.normalChoices.push(choice);
|
||||
}
|
||||
return acc;
|
||||
}, { placeholderChoices: [], normalChoices: [] });
|
||||
|
||||
// If sorting is enabled or the user is searching, filter choices
|
||||
// Do not sort placeholder
|
||||
if (this.config.shouldSort || this.isSearching) {
|
||||
choices.sort(filter);
|
||||
normalChoices.sort(filter);
|
||||
}
|
||||
|
||||
choices.forEach((choice) => {
|
||||
// Prepend placeholedr
|
||||
const sortedChoices = [...placeholderChoices, ...normalChoices];
|
||||
|
||||
sortedChoices.forEach((choice) => {
|
||||
const dropdownItem = this._getTemplate('choice', choice);
|
||||
const shouldRender = this.passedElement.type === 'select-one' || !choice.selected;
|
||||
if (shouldRender) {
|
||||
|
@ -389,8 +408,11 @@ class Choices {
|
|||
|
||||
// Clear choices
|
||||
this.choiceList.innerHTML = '';
|
||||
|
||||
// Scroll back to top of choices list
|
||||
this.choiceList.scrollTop = 0;
|
||||
if(this.config.resetScrollPosition){
|
||||
this.choiceList.scrollTop = 0;
|
||||
}
|
||||
|
||||
// If we have grouped options
|
||||
if (activeGroups.length >= 1 && this.isSearching !== true) {
|
||||
|
@ -406,9 +428,17 @@ class Choices {
|
|||
this._highlightChoice();
|
||||
} else {
|
||||
// Otherwise show a notice
|
||||
const dropdownItem = this.isSearching ?
|
||||
this._getTemplate('notice', this.config.noResultsText) :
|
||||
this._getTemplate('notice', this.config.noChoicesText);
|
||||
let dropdownItem;
|
||||
let notice;
|
||||
|
||||
if (this.isSearching) {
|
||||
notice = isType('Function', this.config.noResultsText) ? this.config.noResultsText() : this.config.noResultsText;
|
||||
dropdownItem = this._getTemplate('notice', notice);
|
||||
} else {
|
||||
notice = isType('Function', this.config.noChoicesText) ? this.config.noChoicesText() : this.config.noChoicesText;
|
||||
dropdownItem = this._getTemplate('notice', notice);
|
||||
}
|
||||
|
||||
this.choiceList.appendChild(dropdownItem);
|
||||
}
|
||||
}
|
||||
|
@ -461,12 +491,14 @@ class Choices {
|
|||
triggerEvent(this.passedElement, 'highlightItem', {
|
||||
id,
|
||||
value: item.value,
|
||||
label: item.label,
|
||||
groupValue: group.value
|
||||
});
|
||||
} else {
|
||||
triggerEvent(this.passedElement, 'highlightItem', {
|
||||
id,
|
||||
value: item.value,
|
||||
label: item.label,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -492,12 +524,14 @@ class Choices {
|
|||
triggerEvent(this.passedElement, 'unhighlightItem', {
|
||||
id,
|
||||
value: item.value,
|
||||
label: item.label,
|
||||
groupValue: group.value
|
||||
});
|
||||
} else {
|
||||
triggerEvent(this.passedElement, 'unhighlightItem', {
|
||||
id,
|
||||
value: item.value,
|
||||
label: item.label,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -605,17 +639,29 @@ class Choices {
|
|||
showDropdown(focusInput = false) {
|
||||
const body = document.body;
|
||||
const html = document.documentElement;
|
||||
const winHeight = Math.max(body.scrollHeight, body.offsetHeight, html.clientHeight,
|
||||
html.scrollHeight, html.offsetHeight);
|
||||
const winHeight = Math.max(
|
||||
body.scrollHeight,
|
||||
body.offsetHeight,
|
||||
html.clientHeight,
|
||||
html.scrollHeight,
|
||||
html.offsetHeight
|
||||
);
|
||||
|
||||
this.containerOuter.classList.add(this.config.classNames.openState);
|
||||
this.containerOuter.setAttribute('aria-expanded', 'true');
|
||||
this.dropdown.classList.add(this.config.classNames.activeState);
|
||||
this.dropdown.setAttribute('aria-expanded', 'true');
|
||||
|
||||
const dimensions = this.dropdown.getBoundingClientRect();
|
||||
const dropdownPos = Math.ceil(dimensions.top + window.scrollY + dimensions.height);
|
||||
|
||||
// If flip is enabled and the dropdown bottom position is greater than the window height flip the dropdown.
|
||||
const shouldFlip = this.config.flip ? dropdownPos >= winHeight : false;
|
||||
let shouldFlip = false;
|
||||
if (this.config.position === 'auto') {
|
||||
shouldFlip = dropdownPos >= winHeight;
|
||||
} else if (this.config.position === 'top') {
|
||||
shouldFlip = true;
|
||||
}
|
||||
|
||||
if (shouldFlip) {
|
||||
this.containerOuter.classList.add(this.config.classNames.flippedState);
|
||||
|
@ -628,6 +674,8 @@ class Choices {
|
|||
this.input.focus();
|
||||
}
|
||||
|
||||
triggerEvent(this.passedElement, 'showDropdown', {});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -643,6 +691,7 @@ class Choices {
|
|||
this.containerOuter.classList.remove(this.config.classNames.openState);
|
||||
this.containerOuter.setAttribute('aria-expanded', 'false');
|
||||
this.dropdown.classList.remove(this.config.classNames.activeState);
|
||||
this.dropdown.setAttribute('aria-expanded', 'false');
|
||||
|
||||
if (isFlipped) {
|
||||
this.containerOuter.classList.remove(this.config.classNames.flippedState);
|
||||
|
@ -653,6 +702,8 @@ class Choices {
|
|||
this.input.blur();
|
||||
}
|
||||
|
||||
triggerEvent(this.passedElement, 'hideDropdown', {});
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
|
@ -700,34 +751,42 @@ class Choices {
|
|||
/**
|
||||
* Set value of input. If the input is a select box, a choice will be created and selected otherwise
|
||||
* an item will created directly.
|
||||
* @param {Array} args Array of value objects or value strings
|
||||
* @param {Array} args Array of value objects or value strings
|
||||
* @return {Object} Class instance
|
||||
* @public
|
||||
*/
|
||||
setValue(args) {
|
||||
if (this.initialised === true) {
|
||||
// Convert args to an itterable array
|
||||
// Convert args to an iterable array
|
||||
const values = [...args],
|
||||
passedElementType = this.passedElement.type;
|
||||
passedElementType = this.passedElement.type,
|
||||
handleValue = (item) => {
|
||||
const itemType = getType(item);
|
||||
if (itemType === 'Object') {
|
||||
if (!item.value) return;
|
||||
// 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.
|
||||
if (passedElementType !== 'text') {
|
||||
this._addChoice(true, false, item.value, item.label, -1);
|
||||
} else {
|
||||
this._addItem(item.value, item.label, item.id);
|
||||
}
|
||||
} else if (itemType === 'String') {
|
||||
if (passedElementType !== 'text') {
|
||||
this._addChoice(true, false, item, item, -1);
|
||||
} else {
|
||||
this._addItem(item);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
values.forEach((item) => {
|
||||
if (isType('Object', item)) {
|
||||
if (!item.value) return;
|
||||
// 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.
|
||||
if (passedElementType !== 'text') {
|
||||
this._addChoice(true, false, item.value, item.label, -1);
|
||||
} else {
|
||||
this._addItem(item.value, item.label, item.id);
|
||||
}
|
||||
} else if (isType('String', item)) {
|
||||
if (passedElementType !== 'text') {
|
||||
this._addChoice(true, false, item, item, -1);
|
||||
} else {
|
||||
this._addItem(item);
|
||||
}
|
||||
}
|
||||
});
|
||||
if (values.length > 1) {
|
||||
values.forEach((value) => {
|
||||
handleValue(value);
|
||||
});
|
||||
} else {
|
||||
handleValue(values[0]);
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
@ -791,7 +850,7 @@ class Choices {
|
|||
const isPlaceholder = result.placeholder ? result.placeholder : false;
|
||||
|
||||
if (result.choices) {
|
||||
this._addGroup(result, index, value, label);
|
||||
this._addGroup(result, (result.id || null), value, label);
|
||||
} else {
|
||||
this._addChoice(isSelected, isDisabled, result[value], result[label], -1, isPlaceholder);
|
||||
}
|
||||
|
@ -843,6 +902,9 @@ class Choices {
|
|||
this.input.removeAttribute('disabled');
|
||||
this.containerOuter.classList.remove(this.config.classNames.disabledState);
|
||||
this.containerOuter.removeAttribute('aria-disabled');
|
||||
if (this.passedElement.type === 'select-one') {
|
||||
this.containerOuter.setAttribute('tabindex', '0');
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
@ -861,6 +923,9 @@ class Choices {
|
|||
this.input.setAttribute('disabled', '');
|
||||
this.containerOuter.classList.add(this.config.classNames.disabledState);
|
||||
this.containerOuter.setAttribute('aria-disabled', 'true');
|
||||
if (this.passedElement.type === 'select-one') {
|
||||
this.containerOuter.setAttribute('tabindex', '-1');
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
@ -921,6 +986,21 @@ class Choices {
|
|||
// Remove item associated with button
|
||||
this._removeItem(itemToRemove);
|
||||
this._triggerChange(itemToRemove.value);
|
||||
|
||||
if (this.passedElement.type === 'select-one') {
|
||||
const placeholderChoice = this.store.getPlaceholderChoice();
|
||||
|
||||
if (placeholderChoice) {
|
||||
this._addItem(
|
||||
placeholderChoice.value,
|
||||
placeholderChoice.label,
|
||||
placeholderChoice.id,
|
||||
placeholderChoice.groupId,
|
||||
placeholderChoice.placeholder
|
||||
);
|
||||
this._triggerChange(placeholderChoice.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -972,6 +1052,10 @@ class Choices {
|
|||
const choice = this.store.getChoiceById(id);
|
||||
const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState);
|
||||
|
||||
triggerEvent(this.passedElement, 'choice', {
|
||||
choice,
|
||||
});
|
||||
|
||||
if (choice && !choice.selected && !choice.disabled) {
|
||||
const canAddItem = this._canAddItem(activeItems, choice.value);
|
||||
|
||||
|
@ -1101,6 +1185,7 @@ class Choices {
|
|||
_ajaxCallback() {
|
||||
return (results, value, label) => {
|
||||
if (!results || !value) return;
|
||||
|
||||
const parsedResults = isType('Object', results) ? [results] : results;
|
||||
|
||||
if (parsedResults && isType('Array', parsedResults) && parsedResults.length) {
|
||||
|
@ -1112,12 +1197,16 @@ class Choices {
|
|||
const isDisabled = result.disabled ? result.disabled : false;
|
||||
const isPlaceholder = result.placeholder ? result.placeholder : false;
|
||||
if (result.choices) {
|
||||
this._addGroup(result, index, value, label);
|
||||
this._addGroup(result, (result.id || null), value, label);
|
||||
} else {
|
||||
this._addChoice(isSelected, isDisabled, result[value], result[label], isPlaceholder);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
// No results, remove loading state
|
||||
this._handleLoadingState(false);
|
||||
}
|
||||
|
||||
this.containerOuter.removeAttribute('aria-busy');
|
||||
};
|
||||
}
|
||||
|
@ -1136,7 +1225,7 @@ 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 keys = isType('Array', this.config.searchFields) ? this.config.searchFields : [this.config.searchFields];
|
||||
const options = Object.assign(this.config.fuseOptions, { keys });
|
||||
const fuse = new Fuse(haystack, options);
|
||||
const results = fuse.search(needle);
|
||||
|
@ -1163,8 +1252,11 @@ class Choices {
|
|||
if (this.input === document.activeElement) {
|
||||
// Check that we have a value to search and the input was an alphanumeric character
|
||||
if (value && value.length > this.config.searchFloor) {
|
||||
// Filter available choices
|
||||
this._searchChoices(value);
|
||||
// Check flag to filter search input
|
||||
if (this.config.searchChoices) {
|
||||
// Filter available choices
|
||||
this._searchChoices(value);
|
||||
}
|
||||
// Trigger search event
|
||||
triggerEvent(this.passedElement, 'search', {
|
||||
value,
|
||||
|
@ -1268,6 +1360,8 @@ class Choices {
|
|||
const escapeKey = 27;
|
||||
const upKey = 38;
|
||||
const downKey = 40;
|
||||
const pageUpKey = 33;
|
||||
const pageDownKey = 34;
|
||||
const ctrlDownKey = e.ctrlKey || e.metaKey;
|
||||
|
||||
// If a user is typing and the dropdown is not active
|
||||
|
@ -1342,16 +1436,25 @@ class Choices {
|
|||
this.showDropdown(true);
|
||||
}
|
||||
|
||||
const currentEl = this.dropdown.querySelector(`.${this.config.classNames.highlightedState}`);
|
||||
const directionInt = e.keyCode === downKey ? 1 : -1;
|
||||
let nextEl;
|
||||
|
||||
this.canSearch = false;
|
||||
|
||||
if (currentEl) {
|
||||
nextEl = getAdjacentEl(currentEl, '[data-choice-selectable]', directionInt);
|
||||
const directionInt = e.keyCode === downKey || e.keyCode === pageDownKey ? 1 : -1;
|
||||
const skipKey = e.metaKey || e.keyCode === pageDownKey || e.keyCode === pageUpKey;
|
||||
|
||||
let nextEl;
|
||||
if (skipKey) {
|
||||
if (directionInt > 0) {
|
||||
nextEl = Array.from(this.dropdown.querySelectorAll('[data-choice-selectable]')).pop();
|
||||
} else {
|
||||
nextEl = this.dropdown.querySelector('[data-choice-selectable]');
|
||||
}
|
||||
} else {
|
||||
nextEl = this.dropdown.querySelector('[data-choice-selectable]');
|
||||
const currentEl = this.dropdown.querySelector(`.${this.config.classNames.highlightedState}`);
|
||||
if (currentEl) {
|
||||
nextEl = getAdjacentEl(currentEl, '[data-choice-selectable]', directionInt);
|
||||
} else {
|
||||
nextEl = this.dropdown.querySelector('[data-choice-selectable]');
|
||||
}
|
||||
}
|
||||
|
||||
if (nextEl) {
|
||||
|
@ -1383,7 +1486,9 @@ class Choices {
|
|||
[enterKey]: onEnterKey,
|
||||
[escapeKey]: onEscapeKey,
|
||||
[upKey]: onDirectionKey,
|
||||
[pageUpKey]: onDirectionKey,
|
||||
[downKey]: onDirectionKey,
|
||||
[pageDownKey]: onDirectionKey,
|
||||
[deleteKey]: onDeleteKey,
|
||||
[backKey]: onDeleteKey,
|
||||
};
|
||||
|
@ -1511,13 +1616,16 @@ class Choices {
|
|||
_onMouseDown(e) {
|
||||
const target = e.target;
|
||||
if (this.containerOuter.contains(target) && target !== this.input) {
|
||||
let foundTarget;
|
||||
const activeItems = this.store.getItemsFilteredByActive();
|
||||
const hasShiftKey = e.shiftKey;
|
||||
|
||||
if (target.hasAttribute('data-item')) {
|
||||
this._handleItemAction(activeItems, target, hasShiftKey);
|
||||
} else if (target.hasAttribute('data-choice')) {
|
||||
this._handleChoiceAction(activeItems, target);
|
||||
if(foundTarget = findAncestorByAttrName(target, 'data-button')) {
|
||||
this._handleButtonAction(activeItems, foundTarget);
|
||||
} else if (foundTarget = findAncestorByAttrName(target, 'data-item')) {
|
||||
this._handleItemAction(activeItems, foundTarget, hasShiftKey);
|
||||
} else if (foundTarget = findAncestorByAttrName(target, 'data-choice')) {
|
||||
this._handleChoiceAction(activeItems, foundTarget);
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
@ -1535,6 +1643,7 @@ class Choices {
|
|||
const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState);
|
||||
const activeItems = this.store.getItemsFilteredByActive();
|
||||
|
||||
|
||||
// If target is something that concerns us
|
||||
if (this.containerOuter.contains(target)) {
|
||||
// Handle button delete
|
||||
|
@ -1859,12 +1968,14 @@ class Choices {
|
|||
triggerEvent(this.passedElement, 'addItem', {
|
||||
id,
|
||||
value: passedValue,
|
||||
label: passedLabel,
|
||||
groupValue: group.value,
|
||||
});
|
||||
} else {
|
||||
triggerEvent(this.passedElement, 'addItem', {
|
||||
id,
|
||||
value: passedValue,
|
||||
label: passedLabel,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1886,6 +1997,7 @@ class Choices {
|
|||
|
||||
const id = item.id;
|
||||
const value = item.value;
|
||||
const label = item.label;
|
||||
const choiceId = item.choiceId;
|
||||
const groupId = item.groupId;
|
||||
const group = groupId >= 0 ? this.store.getGroupById(groupId) : null;
|
||||
|
@ -1896,12 +2008,14 @@ class Choices {
|
|||
triggerEvent(this.passedElement, 'removeItem', {
|
||||
id,
|
||||
value,
|
||||
label,
|
||||
groupValue: group.value,
|
||||
});
|
||||
} else {
|
||||
triggerEvent(this.passedElement, 'removeItem', {
|
||||
id,
|
||||
value,
|
||||
label,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -1953,7 +2067,7 @@ class Choices {
|
|||
*/
|
||||
_addGroup(group, id, valueKey = 'value', labelKey = 'label') {
|
||||
const groupChoices = isType('Object', group) ? group.choices : Array.from(group.getElementsByTagName('OPTION'));
|
||||
const groupId = id;
|
||||
const groupId = id ? id : Math.floor(new Date().valueOf() * Math.random());
|
||||
const isDisabled = group.disabled ? group.disabled : false;
|
||||
|
||||
if (groupChoices) {
|
||||
|
@ -2241,8 +2355,8 @@ class Choices {
|
|||
this.isSearching = false;
|
||||
|
||||
if (passedGroups && passedGroups.length) {
|
||||
passedGroups.forEach((group, index) => {
|
||||
this._addGroup(group, index);
|
||||
passedGroups.forEach((group) => {
|
||||
this._addGroup(group, (group.id || null));
|
||||
});
|
||||
} else {
|
||||
const passedOptions = Array.from(this.passedElement.options);
|
||||
|
@ -2260,18 +2374,32 @@ class Choices {
|
|||
});
|
||||
});
|
||||
|
||||
// Split array into placeholedrs and "normal" choices
|
||||
const { placeholderChoices, normalChoices } = allChoices.reduce((acc, choice) => {
|
||||
if (choice.placeholder) {
|
||||
acc.placeholderChoices.push(choice);
|
||||
} else {
|
||||
acc.normalChoices.push(choice);
|
||||
}
|
||||
return acc;
|
||||
}, { placeholderChoices: [], normalChoices: [] });
|
||||
|
||||
// If sorting is enabled or the user is searching, filter choices
|
||||
// Do not sort placeholder
|
||||
if (this.config.shouldSort) {
|
||||
allChoices.sort(filter);
|
||||
normalChoices.sort(filter);
|
||||
}
|
||||
|
||||
// Prepend placeholder
|
||||
const sortedChoices = [...placeholderChoices, ...normalChoices];
|
||||
|
||||
// Determine whether there is a selected choice
|
||||
const hasSelectedChoice = allChoices.some((choice) => {
|
||||
const hasSelectedChoice = sortedChoices.some((choice) => {
|
||||
return choice.selected === true;
|
||||
});
|
||||
|
||||
// Add each choice
|
||||
allChoices.forEach((choice, index) => {
|
||||
sortedChoices.forEach((choice, index) => {
|
||||
const isDisabled = choice.disabled ? choice.disabled : false;
|
||||
const isSelected = choice.selected ? choice.selected : false;
|
||||
// Pre-select first choice if it's a single select
|
||||
|
@ -2292,10 +2420,11 @@ class Choices {
|
|||
} else if (this.isTextElement) {
|
||||
// Add any preset values seperated by delimiter
|
||||
this.presetItems.forEach((item) => {
|
||||
if (isType('Object', item)) {
|
||||
const itemType = getType(item);
|
||||
if (itemType === 'Object') {
|
||||
if (!item.value) return;
|
||||
this._addItem(item.value, item.label, item.id);
|
||||
} else if (isType('String', item)) {
|
||||
} else if (itemType === 'String') {
|
||||
this._addItem(item);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -10,6 +10,16 @@ export const capitalise = function(str) {
|
|||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests the type of an object
|
||||
* @param {String} type Type to test object against
|
||||
* @param {Object} obj Object to be tested
|
||||
* @return {Boolean}
|
||||
*/
|
||||
export const getType = function(obj) {
|
||||
return Object.prototype.toString.call(obj).slice(8, -1);
|
||||
};
|
||||
|
||||
/**
|
||||
* Tests the type of an object
|
||||
* @param {String} type Type to test object against
|
||||
|
@ -17,7 +27,7 @@ export const capitalise = function(str) {
|
|||
* @return {Boolean}
|
||||
*/
|
||||
export const isType = function(type, obj) {
|
||||
var clas = Object.prototype.toString.call(obj).slice(8, -1);
|
||||
var clas = getType(obj);
|
||||
return obj !== undefined && obj !== null && clas === type;
|
||||
};
|
||||
|
||||
|
@ -30,7 +40,7 @@ export const isNode = (o) => {
|
|||
return (
|
||||
typeof Node === "object" ? o instanceof Node :
|
||||
o && typeof o === "object" && typeof o.nodeType === "number" && typeof o.nodeName === "string"
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -42,7 +52,7 @@ export const isElement = (o) => {
|
|||
return (
|
||||
typeof HTMLElement === "object" ? o instanceof HTMLElement : //DOM2
|
||||
o && typeof o === "object" && o !== null && o.nodeType === 1 && typeof o.nodeName === "string"
|
||||
);
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -248,6 +258,26 @@ export const findAncestor = function(el, cls) {
|
|||
return el;
|
||||
};
|
||||
|
||||
/**
|
||||
* Find ancestor in DOM tree by attribute name
|
||||
* @param {NodeElement} el Element to start search from
|
||||
* @param {string} attr Attribute name of parent
|
||||
* @return {?NodeElement} Found parent element or null
|
||||
*/
|
||||
export const findAncestorByAttrName = function(el, attr) {
|
||||
let target = el;
|
||||
|
||||
while (target) {
|
||||
if (target.hasAttribute(attr)) {
|
||||
return target;
|
||||
}
|
||||
|
||||
target = target.parentElement;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Debounce an event handler.
|
||||
* @param {Function} func Function to run after wait
|
||||
|
|
|
@ -159,6 +159,19 @@ export default class Store {
|
|||
|
||||
return foundGroup;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get placeholder choice from store
|
||||
* @return {Object} Found placeholder
|
||||
*/
|
||||
getPlaceholderChoice() {
|
||||
const choices = this.getChoices();
|
||||
const values = choices.filter((choice) => {
|
||||
return choice.placeholder === true;
|
||||
}, []);
|
||||
|
||||
return values[0];
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = Store;
|
||||
|
|
|
@ -138,4 +138,12 @@ h6, .h6 {
|
|||
display: none;
|
||||
}
|
||||
|
||||
.zero-bottom {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.zero-top {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
/*===== End of Section comment block ======*/
|
||||
|
|
2
assets/styles/css/base.min.css
vendored
2
assets/styles/css/base.min.css
vendored
|
@ -1 +1 @@
|
|||
*{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}*,:after,:before{box-sizing:border-box}body,html{position:relative;margin:0;width:100%;height:100%}body{font-family:"Helvetica Neue",Helvetica,Arial,"Lucida Grande",sans-serif;font-size:16px;line-height:1.4;color:#fff;background-color:#333;overflow-x:hidden}hr,label{display:block}label{margin-bottom:8px;font-size:14px;font-weight:500;cursor:pointer}p{margin-top:0}hr{margin:36px 0;border:0;border-bottom:1px solid #eaeaea;height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:12px;font-weight:400;line-height:1.2}a,a:focus,a:visited{color:#fff;text-decoration:none;font-weight:600}.form-control{display:block;width:100%;background-color:#f9f9f9;padding:12px;border:1px solid #ddd;border-radius:2.5px;font-size:14px;-webkit-appearance:none;-moz-appearance:none;appearance:none;margin-bottom:24px}.h1,h1{font-size:32px}.h2,h2{font-size:24px}.h3,h3{font-size:20px}.h4,h4{font-size:18px}.h5,h5{font-size:16px}.h6,h6{font-size:14px}.container{display:block;margin:auto;max-width:40em;padding:48px}@media (max-width:620px){.container{padding:0}}.section{background-color:#fff;padding:24px;color:#333}.section a,.section a:focus,.section a:visited{color:#00bcd4}.logo{display:block;margin-bottom:12px}.logo__img{width:100%;height:auto;display:inline-block;max-width:100%;vertical-align:top;padding:6px 0}.visible-ie{display:none}
|
||||
*{-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}*,:after,:before{box-sizing:border-box}body,html{position:relative;margin:0;width:100%;height:100%}body{font-family:"Helvetica Neue",Helvetica,Arial,"Lucida Grande",sans-serif;font-size:16px;line-height:1.4;color:#fff;background-color:#333;overflow-x:hidden}hr,label{display:block}label{margin-bottom:8px;font-size:14px;font-weight:500;cursor:pointer}p{margin-top:0}hr{margin:36px 0;border:0;border-bottom:1px solid #eaeaea;height:1px}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:12px;font-weight:400;line-height:1.2}a,a:focus,a:visited{color:#fff;text-decoration:none;font-weight:600}.form-control{display:block;width:100%;background-color:#f9f9f9;padding:12px;border:1px solid #ddd;border-radius:2.5px;font-size:14px;-webkit-appearance:none;-moz-appearance:none;appearance:none;margin-bottom:24px}.h1,h1{font-size:32px}.h2,h2{font-size:24px}.h3,h3{font-size:20px}.h4,h4{font-size:18px}.h5,h5{font-size:16px}.h6,h6{font-size:14px}.container{display:block;margin:auto;max-width:40em;padding:48px}@media (max-width:620px){.container{padding:0}}.section{background-color:#fff;padding:24px;color:#333}.section a,.section a:focus,.section a:visited{color:#00bcd4}.logo{display:block;margin-bottom:12px}.logo__img{width:100%;height:auto;display:inline-block;max-width:100%;vertical-align:top;padding:6px 0}.visible-ie{display:none}.zero-bottom{margin-bottom:0}.zero-top{margin-top:0}
|
|
@ -46,7 +46,7 @@
|
|||
}
|
||||
|
||||
.choices[data-type*="select-one"] .choices__button {
|
||||
background-image: url("../../icons//cross-inverse.svg");
|
||||
background-image: url("../../icons/cross-inverse.svg");
|
||||
padding: 0;
|
||||
background-size: 8px;
|
||||
height: 100%;
|
||||
|
@ -113,7 +113,7 @@
|
|||
margin-left: 8px;
|
||||
padding-left: 16px;
|
||||
border-left: 1px solid #008fa1;
|
||||
background-image: url("../../icons//cross.svg");
|
||||
background-image: url("../../icons/cross.svg");
|
||||
background-size: 8px;
|
||||
width: 8px;
|
||||
line-height: 1;
|
||||
|
|
2
assets/styles/css/choices.min.css
vendored
2
assets/styles/css/choices.min.css
vendored
File diff suppressed because one or more lines are too long
|
@ -118,5 +118,7 @@ h6, .h6 { font-size: $global-font-size-h6; }
|
|||
}
|
||||
|
||||
.visible-ie { display: none; }
|
||||
.zero-bottom { margin-bottom: 0; }
|
||||
.zero-top { margin-top: 0; }
|
||||
|
||||
/*===== End of Section comment block ======*/
|
||||
|
|
|
@ -17,7 +17,7 @@ $choices-keyline-color: #DDDDDD !default;
|
|||
$choices-primary-color: #00BCD4 !default;
|
||||
$choices-disabled-color: #eaeaea !default;
|
||||
$choices-highlight-color: $choices-primary-color !default;
|
||||
$choices-button-icon-path: '../../icons/' !default;
|
||||
$choices-button-icon-path: '../../icons' !default;
|
||||
$choices-button-dimension: 8px !default;
|
||||
$choices-button-offset: 8px !default;
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "choices.js",
|
||||
"version": "2.6.1",
|
||||
"version": "2.7.8",
|
||||
"description": "A vanilla JS customisable text input/select box plugin",
|
||||
"main": [
|
||||
"./assets/scripts/dist/choices.js",
|
||||
|
@ -16,7 +16,7 @@
|
|||
"node_modules",
|
||||
"bower_components",
|
||||
"test",
|
||||
"tests",
|
||||
"tests"
|
||||
],
|
||||
"keywords": [
|
||||
"customisable",
|
||||
|
|
121
index.html
121
index.html
|
@ -15,7 +15,7 @@
|
|||
<meta name="theme-color" content="#ffffff">
|
||||
|
||||
<!-- Ignore these -->
|
||||
<link rel="stylesheet" href="assets/styles/css/base.min.css?version=2.6.1">
|
||||
<link rel="stylesheet" href="assets/styles/css/base.min.css?version=2.7.8">
|
||||
<!-- End ignore these -->
|
||||
|
||||
<!-- Optional includes -->
|
||||
|
@ -23,8 +23,8 @@
|
|||
<!-- End optional includes -->
|
||||
|
||||
<!-- Choices includes -->
|
||||
<link rel="stylesheet" href="assets/styles/css/choices.min.css?version=2.6.1">
|
||||
<script src="assets/scripts/dist/choices.min.js?version=2.6.1"></script>
|
||||
<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.8"></script>
|
||||
<!-- End Choices includes -->
|
||||
|
||||
<!--[if lt IE 9]>
|
||||
|
@ -137,12 +137,15 @@
|
|||
<option value="Dropdown item 4" disabled>Dropdown item 4</option>
|
||||
</select>
|
||||
|
||||
<label for="label-event">Use label in event (add/remove)</label>
|
||||
<p id="message"></p>
|
||||
<select id="choices-multiple-labels" multiple></select>
|
||||
|
||||
<hr>
|
||||
|
||||
<h2>Single select input</h2>
|
||||
<label for="choices-single-default">Default</label>
|
||||
<select class="form-control" data-trigger name="choices-single-default" id="choices-single-default">
|
||||
<option selected disabled>This is a placeholder</option>
|
||||
<option value="Dropdown item 1">Dropdown item 1</option>
|
||||
<option value="Dropdown item 2">Dropdown item 2</option>
|
||||
<option value="Dropdown item 3">Dropdown item 3</option>
|
||||
|
@ -199,6 +202,7 @@
|
|||
|
||||
<label for="choices-single-no-search">Options added via config with no search</label>
|
||||
<select class="form-control" name="choices-single-no-search" id="choices-single-no-search">
|
||||
<option selected placeholder disabled>Press here</option>
|
||||
<option value="0">Zero</option>
|
||||
</select>
|
||||
|
||||
|
@ -232,18 +236,18 @@
|
|||
|
||||
<label for="choices-placeholder-option">Placeholder option</label>
|
||||
<select class="form-control" name="choices-placeholder-option" id="choices-placeholder-option">
|
||||
<option selected placeholder>Press here</option>
|
||||
<option selected placeholder disabled>Press here</option>
|
||||
<option value="one">One</option>
|
||||
<option value="two">Two</option>
|
||||
<option value="three">Three</option>
|
||||
</select>
|
||||
|
||||
<label for="choices-custom-templates">Custom templates</label>
|
||||
<select class="form-control" name="choices-custom-templates" id="choices-custom-templates">
|
||||
<label for="choices-single-custom-templates">Custom templates</label>
|
||||
<select class="form-control" name="choices-single-custom-templates" id="choices-single-custom-templates" placeholder="This is a placeholder">
|
||||
<option value="React">React</option>
|
||||
<option value="React">Angular</option>
|
||||
<option value="React">Ember</option>
|
||||
<option value="React">Vue</option>
|
||||
<option value="Angular">Angular</option>
|
||||
<option value="Ember">Ember</option>
|
||||
<option value="Vue">Vue</option>
|
||||
</select>
|
||||
|
||||
<p>Below is an example of how you could have two select inputs depend on eachother. 'Boroughs' will only be enabled if the value of 'States' is 'New York'</p>
|
||||
|
@ -285,11 +289,12 @@
|
|||
paste: false,
|
||||
duplicateItems: false,
|
||||
editItems: true,
|
||||
addItemText: (value) => {
|
||||
return `Appuyez sur Entrée pour ajouter <b>"${value}"</b>`;
|
||||
maxItemCount: 5,
|
||||
addItemText: function(value) {
|
||||
return 'Appuyez sur Entrée pour ajouter <b>"' + String(value) + '"</b>';
|
||||
},
|
||||
maxItemText: (maxItemCount) => {
|
||||
return `${maxItemCount} valeurs peuvent être ajoutées`;
|
||||
maxItemText: function(maxItemCount) {
|
||||
return String(maxItemCount) + 'valeurs peuvent être ajoutées';
|
||||
},
|
||||
uniqueItemText: 'Cette valeur est déjà présente',
|
||||
});
|
||||
|
@ -297,7 +302,7 @@
|
|||
var textEmailFilter = new Choices('#choices-text-email-filter', {
|
||||
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,}))$/,
|
||||
});
|
||||
}).setValue(['joe@bloggs.com']);
|
||||
|
||||
var textDisabled = new Choices('#choices-text-disabled', {
|
||||
addItems: false,
|
||||
|
@ -332,7 +337,30 @@
|
|||
|
||||
var multipleCancelButton = new Choices('#choices-multiple-remove-button', {
|
||||
removeItemButton: true,
|
||||
})
|
||||
});
|
||||
|
||||
/* Use label on event */
|
||||
var choicesSelect = new Choices('#choices-multiple-labels', {
|
||||
search: false,
|
||||
removeItemButton: true,
|
||||
choices: [
|
||||
{value: 'One', label: 'Label One'},
|
||||
{value: 'Two', label: 'Label Two', disabled: true},
|
||||
{value: 'Three', label: 'Label Three'},
|
||||
],
|
||||
}).setChoices([
|
||||
{value: 'Four', label: 'Label Four', disabled: true},
|
||||
{value: 'Five', label: 'Label Five'},
|
||||
{value: 'Six', label: 'Label Six', selected: true},
|
||||
], 'value', 'label', false);
|
||||
|
||||
choicesSelect.passedElement.addEventListener('addItem', function(event) {
|
||||
document.getElementById('message').innerHTML = 'You just added "' + event.detail.label + '"';
|
||||
});
|
||||
|
||||
choicesSelect.passedElement.addEventListener('removeItem', function(event) {
|
||||
document.getElementById('message').innerHTML = 'You just removed "' + event.detail.label + '"';
|
||||
});
|
||||
|
||||
var singleFetch = new Choices('#choices-single-remote-fetch', {
|
||||
searchPlaceholderValue: 'Pick an Arctic Monkeys record',
|
||||
|
@ -426,6 +454,7 @@
|
|||
var singlePlaceholderOption = new Choices('#choices-placeholder-option', {
|
||||
removeItemButton: false,
|
||||
preselectItem: false,
|
||||
shouldSort: false,
|
||||
});
|
||||
|
||||
var singleStates = new Choices(document.getElementById('choices-states'));
|
||||
|
@ -439,30 +468,44 @@
|
|||
}
|
||||
});
|
||||
|
||||
var singleCustomTemplates = new Choices(
|
||||
document.getElementById('choices-custom-templates'), {
|
||||
searchPlaceholderValue: 'Choose your favourite framework...',
|
||||
callbackOnCreateTemplates: function (strToEl) {
|
||||
var classNames = this.config.classNames;
|
||||
return {
|
||||
item: (data) => {
|
||||
return strToEl(`
|
||||
<div class="${classNames.item} ${data.highlighted ? classNames.highlightedState : classNames.itemSelectable}" data-item data-id="${data.id}" data-value="${data.value}" ${data.active ? 'aria-selected="true"' : ''} ${data.disabled ? 'aria-disabled="true"' : ''}>
|
||||
<span style="margin-right:10px;">🎉</span> ${data.label}
|
||||
</div>
|
||||
`);
|
||||
},
|
||||
choice: (data) => {
|
||||
return strToEl(`
|
||||
<div class="${classNames.item} ${classNames.itemChoice} ${data.disabled ? classNames.itemDisabled : classNames.itemSelectable}" data-select-text="${this.config.itemSelectText}" data-choice ${data.disabled ? 'data-choice-disabled aria-disabled="true"' : 'data-choice-selectable'} data-id="${data.id}" data-value="${data.value}" ${data.groupId > 0 ? 'role="treeitem"' : 'role="option"'}>
|
||||
<span style="margin-right:10px;">👉🏽</span> ${data.label}
|
||||
</div>
|
||||
`);
|
||||
},
|
||||
};
|
||||
}
|
||||
var customTemplates = new Choices(document.getElementById('choices-single-custom-templates'), {
|
||||
searchPlaceholderValue: 'Choose your favourite framework...',
|
||||
callbackOnCreateTemplates: function (strToEl) {
|
||||
var classNames = this.config.classNames;
|
||||
var itemSelectText = this.config.itemSelectText;
|
||||
return {
|
||||
item: function(data) {
|
||||
return strToEl('\
|
||||
<div\
|
||||
class="'+ String(classNames.item) + ' ' + String(data.highlighted ? classNames.highlightedState : classNames.itemSelectable) + '"\
|
||||
data-item\
|
||||
data-id="'+ String(data.id) + '"\
|
||||
data-value="'+ String(data.value) +'"\
|
||||
'+ String(data.active ? 'aria-selected="true"' : '') + '\
|
||||
'+ String(data.disabled ? 'aria-disabled="true"' : '') + '\
|
||||
>\
|
||||
<span style="margin-right:10px;">🎉</span> ' + String(data.label) + '\
|
||||
</div>\
|
||||
');
|
||||
},
|
||||
choice: function(data) {
|
||||
return strToEl('\
|
||||
<div\
|
||||
class="'+ String(classNames.item) + ' ' + String(classNames.itemChoice) + ' ' + String(data.disabled ? classNames.itemDisabled : classNames.itemSelectable) + '"\
|
||||
data-select-text="'+ String(itemSelectText) +'"\
|
||||
data-choice \
|
||||
'+ String(data.disabled ? 'data-choice-disabled aria-disabled="true"' : 'data-choice-selectable') + '\
|
||||
data-id="'+ String(data.id) +'"\
|
||||
data-value="'+ String(data.value) +'"\
|
||||
'+ String(data.groupId > 0 ? 'role="treeitem"' : 'role="option"') + '\
|
||||
>\
|
||||
<span style="margin-right:10px;">👉🏽</span> ' + String(data.label) + '\
|
||||
</div>\
|
||||
');
|
||||
},
|
||||
};
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "choices.js",
|
||||
"version": "2.6.1",
|
||||
"version": "2.7.8",
|
||||
"description": "A vanilla JS customisable text input/select box plugin",
|
||||
"main": "./assets/scripts/dist/choices.min.js",
|
||||
"scripts": {
|
||||
|
@ -13,7 +13,8 @@
|
|||
"js:build": "concurrently --prefix-colors yellow,green \"webpack --minimize --config webpack.config.prod.js\" \"webpack --config webpack.config.prod.js\"",
|
||||
"js:test": "./node_modules/karma/bin/karma start --single-run --no-auto-watch tests/karma.config.js",
|
||||
"js:test:watch": "./node_modules/karma/bin/karma start --auto-watch --no-single-run tests/karma.config.js",
|
||||
"preversion": "npm run js:build"
|
||||
"version": "node version.js --current $npm_package_version --new $npm_config_newVersion",
|
||||
"postversion": "npm run js:build"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
|
|
@ -77,10 +77,10 @@ describe('Choices', () => {
|
|||
expect(this.choices.config.search).toEqual(jasmine.any(Boolean));
|
||||
expect(this.choices.config.searchFloor).toEqual(jasmine.any(Number));
|
||||
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.regexFilter).toEqual(null);
|
||||
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.placeholderValue).toEqual(null);
|
||||
expect(this.choices.config.prependValue).toEqual(null);
|
||||
|
@ -255,7 +255,6 @@ describe('Choices', () => {
|
|||
beforeEach(function() {
|
||||
this.input = document.createElement('select');
|
||||
this.input.className = 'js-choices';
|
||||
this.input.placeholder = 'Placeholder text';
|
||||
|
||||
for (let i = 1; i < 4; i++) {
|
||||
const option = document.createElement('option');
|
||||
|
@ -367,9 +366,56 @@ describe('Choices', () => {
|
|||
});
|
||||
|
||||
it('should close the dropdown on double click', function() {
|
||||
this.choices = new Choices(this.input);
|
||||
const container = this.choices.containerOuter,
|
||||
openState = this.choices.config.classNames.openState;
|
||||
|
||||
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(openState)).toBe(false);
|
||||
});
|
||||
|
||||
it('should trigger showDropdown on dropdown opening', function() {
|
||||
this.choices = new Choices(this.input);
|
||||
const container = this.choices.containerOuter;
|
||||
|
||||
const showDropdownSpy = jasmine.createSpy('showDropdownSpy');
|
||||
const passedElement = this.choices.passedElement;
|
||||
|
||||
passedElement.addEventListener('showDropdown', showDropdownSpy);
|
||||
|
||||
this.choices.input.focus();
|
||||
|
||||
this.choices._onClick({
|
||||
target: container,
|
||||
ctrlKey: false,
|
||||
preventDefault: () => {}
|
||||
});
|
||||
|
||||
expect(showDropdownSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should trigger hideDropdown on dropdown closing', function() {
|
||||
this.choices = new Choices(this.input);
|
||||
const container = this.choices.containerOuter;
|
||||
|
||||
const hideDropdownSpy = jasmine.createSpy('hideDropdownSpy');
|
||||
const passedElement = this.choices.passedElement;
|
||||
|
||||
passedElement.addEventListener('hideDropdown', hideDropdownSpy);
|
||||
|
||||
this.choices.input.focus();
|
||||
|
||||
this.choices._onClick({
|
||||
target: container,
|
||||
ctrlKey: false,
|
||||
|
@ -382,7 +428,7 @@ describe('Choices', () => {
|
|||
preventDefault: () => {}
|
||||
});
|
||||
|
||||
expect(document.activeElement === this.choices.input && container.classList.contains('is-open')).toBe(false);
|
||||
expect(hideDropdownSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should filter choices when searching', function() {
|
||||
|
@ -394,7 +440,7 @@ describe('Choices', () => {
|
|||
passedElement.addEventListener('search', searchSpy);
|
||||
|
||||
this.choices.input.focus();
|
||||
this.choices.input.value = 'Value 3';
|
||||
this.choices.input.value = '3 ';
|
||||
|
||||
// Key down to search
|
||||
this.choices._onKeyUp({
|
||||
|
@ -403,9 +449,41 @@ describe('Choices', () => {
|
|||
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();
|
||||
});
|
||||
|
||||
|
@ -444,6 +522,39 @@ describe('Choices', () => {
|
|||
|
||||
expect(this.choices.currentState.choices[0].value).toEqual('Value 1');
|
||||
});
|
||||
|
||||
it('should set placeholder as first option if shouldSort true', function() {
|
||||
const option = document.createElement('option');
|
||||
option.setAttribute('selected', '');
|
||||
option.setAttribute('placeholder', '');
|
||||
option.setAttribute('disabled', '');
|
||||
option.innerHTML = 'Placeholder';
|
||||
this.input.appendChild(option);
|
||||
|
||||
this.choices = new Choices(this.input, { shouldSort: true });
|
||||
expect(this.choices.currentState.choices[0].value).toEqual('Placeholder');
|
||||
});
|
||||
|
||||
it('should set placeholder after click on remove item button', function() {
|
||||
const option = document.createElement('option');
|
||||
option.setAttribute('placeholder', '');
|
||||
option.setAttribute('disabled', '');
|
||||
option.innerHTML = 'Placeholder';
|
||||
this.input.appendChild(option);
|
||||
|
||||
this.choices = new Choices(this.input, { removeItemButton: true });
|
||||
|
||||
const removeItemButton = this.choices.containerOuter.querySelector('[data-button]');
|
||||
|
||||
this.choices._onClick({
|
||||
target: removeItemButton,
|
||||
ctrlKey: false,
|
||||
preventDefault: () => {}
|
||||
});
|
||||
|
||||
expect(this.choices.currentState.items[1].value).toBe('Placeholder');
|
||||
expect(this.choices.currentState.items[1].active).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should accept multiple select inputs', function() {
|
||||
|
@ -735,6 +846,42 @@ describe('Choices', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('should handle public methods on select-one input types', function() {
|
||||
beforeEach(function() {
|
||||
this.input = document.createElement('select');
|
||||
this.input.className = 'js-choices';
|
||||
this.input.placeholder = 'Placeholder text';
|
||||
|
||||
for (let i = 1; i < 10; i++) {
|
||||
const option = document.createElement('option');
|
||||
|
||||
option.value = `Value ${i}`;
|
||||
option.innerHTML = `Value ${i}`;
|
||||
|
||||
if (i % 2) {
|
||||
option.selected = true;
|
||||
}
|
||||
|
||||
this.input.appendChild(option);
|
||||
}
|
||||
|
||||
document.body.appendChild(this.input);
|
||||
this.choices = new Choices(this.input);
|
||||
});
|
||||
|
||||
it('should handle disable()', function() {
|
||||
this.choices.disable();
|
||||
|
||||
expect(this.choices.containerOuter.getAttribute('tabindex')).toBe('-1');
|
||||
});
|
||||
|
||||
it('should handle enable()', function() {
|
||||
this.choices.enable();
|
||||
|
||||
expect(this.choices.containerOuter.getAttribute('tabindex')).toBe('0');
|
||||
});
|
||||
});
|
||||
|
||||
describe('should handle public methods on text input types', function() {
|
||||
beforeEach(function() {
|
||||
this.input = document.createElement('input');
|
||||
|
@ -759,4 +906,47 @@ describe('Choices', () => {
|
|||
expect(randomItem.active).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('should react to config options', function() {
|
||||
beforeEach(function() {
|
||||
this.input = document.createElement('select');
|
||||
this.input.className = 'js-choices';
|
||||
this.input.setAttribute('multiple', '');
|
||||
|
||||
for (let i = 1; i < 4; i++) {
|
||||
const option = document.createElement('option');
|
||||
|
||||
option.value = `Value ${i}`;
|
||||
option.innerHTML = `Value ${i}`;
|
||||
|
||||
if (i % 2) {
|
||||
option.selected = true;
|
||||
}
|
||||
|
||||
this.input.appendChild(option);
|
||||
}
|
||||
|
||||
document.body.appendChild(this.input);
|
||||
});
|
||||
|
||||
it('should flip the dropdown', function() {
|
||||
this.choices = new Choices(this.input, {
|
||||
position: 'top'
|
||||
});
|
||||
|
||||
const container = this.choices.containerOuter;
|
||||
this.choices.input.focus();
|
||||
expect(container.classList.contains(this.choices.config.classNames.flippedState)).toBe(true);
|
||||
});
|
||||
|
||||
it('shouldn\'t flip the dropdown', function() {
|
||||
this.choices = new Choices(this.input, {
|
||||
position: 'bottom'
|
||||
});
|
||||
|
||||
const container = this.choices.containerOuter;
|
||||
this.choices.input.focus();
|
||||
expect(container.classList.contains(this.choices.config.classNames.flippedState)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
50
version.js
Normal file
50
version.js
Normal file
|
@ -0,0 +1,50 @@
|
|||
// Example usage: npm --newVersion=2.7.2 run version
|
||||
|
||||
const fs = require('fs'),
|
||||
path = require('path'),
|
||||
config = {
|
||||
files: ['bower.json', 'package.json', 'index.html']
|
||||
};
|
||||
|
||||
/**
|
||||
* Convert node arguments into an object
|
||||
* @return {Object} Arguments
|
||||
*/
|
||||
const argvToObject = () => {
|
||||
const args = {};
|
||||
let arg = null;
|
||||
process.argv.forEach((val, index) => {
|
||||
if(/^--/.test(val)) {
|
||||
arg = {
|
||||
index: index,
|
||||
name: val.replace(/^--/, '')
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if(arg && ((arg.index+1 === index ))) {
|
||||
args[arg.name] = val;
|
||||
}
|
||||
});
|
||||
|
||||
return args;
|
||||
};
|
||||
|
||||
const updateVersion = (config) => {
|
||||
const args = argvToObject();
|
||||
const currentVersion = args.current;
|
||||
const newVersion = args.new;
|
||||
console.log(`Updating version from ${currentVersion} to ${newVersion}`);
|
||||
config.files.forEach((file) => {
|
||||
const filePath = path.join(__dirname, file);
|
||||
const regex = new RegExp(currentVersion, 'g');
|
||||
|
||||
let contents = fs.readFileSync(filePath, 'utf-8');
|
||||
contents = contents.replace(regex, newVersion);
|
||||
fs.writeFileSync(filePath, contents);
|
||||
});
|
||||
|
||||
console.log(`Updated version to ${newVersion}`);
|
||||
};
|
||||
|
||||
updateVersion(config);
|
Loading…
Reference in a new issue