Merge branch 'adammockor-feature/select-placeholders' into feature/select-placeholders

This commit is contained in:
Josh Johnson 2017-04-07 09:08:44 +01:00
commit 4070b6842f
19 changed files with 1309 additions and 539 deletions

1
.gitignore vendored
View file

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

120
README.md
View file

@ -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!

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -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);
}
});

View file

@ -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

View file

@ -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;

View file

@ -138,4 +138,12 @@ h6, .h6 {
display: none;
}
.zero-bottom {
margin-bottom: 0;
}
.zero-top {
margin-top: 0;
}
/*===== End of Section comment block ======*/

View file

@ -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}

View file

@ -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;

File diff suppressed because one or more lines are too long

View file

@ -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 ======*/

View file

@ -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;

View file

@ -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",

View file

@ -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>

View file

@ -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",

View file

@ -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
View 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);