Merge remote-tracking branch 'refs/remotes/jshjohnson/master'

This commit is contained in:
Maxim Mig 2017-07-06 13:00:05 +03:00
commit fdced6276a
18 changed files with 1355 additions and 719 deletions

3
.gitignore vendored
View file

@ -2,7 +2,8 @@ node_modules
npm-debug.log
.DS_Store
.vscode
package-lock.json
# Test
tests/reports
tests/results
tests/results

View file

@ -6,4 +6,4 @@ before_install:
- npm install -g npm@latest
install:
- npm install
script: npm run js:test
script: npm run test

46
CODE_OF_CONDUCT.md Normal file
View file

@ -0,0 +1,46 @@
# Contributor Covenant Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at josh@joshuajohnson.co.uk. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
[homepage]: http://contributor-covenant.org
[version]: http://contributor-covenant.org/version/1/4/

23
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,23 @@
# Contributions
In lieu of a formal styleguide, take care to maintain the existing coding style ensuring there are no linting errors. Add unit tests for any new or changed functionality. Lint and test your code using the npm scripts below:
### NPM tasks
| Task | Usage |
| -------------------- | ------------------------------------------------------------ |
| `npm run start` | Fire up local server for development |
| `npm run test` | Run sequence of tests once |
| `npm run test:watch` | Fire up test server and re-test on file change |
| `npm run js:build` | Compile Choices to an uglified JavaScript file |
| `npm run css:watch` | Watch SCSS files for changes. On a change, run build process |
| `npm run css:build` | Compile, minify and prefix SCSS files to CSS |
## Pull requests
When submitting a pull request that resolves a bug, feel free to use the following template:
```md
## This is the problem:
## Steps to reproduce:
## This is my solution:
```

232
README.md
View file

@ -70,6 +70,7 @@ Or include Choices directly:
searchEnabled: true,
searchChoices: true,
searchFloor: 1,
searchResultLimit: 4,
searchFields: ['label', 'value'],
position: 'auto',
resetScrollPosition: true,
@ -91,29 +92,29 @@ Or include Choices directly:
return `Only ${maxItemCount} values can be added.`;
},
classNames: {
containerOuter: 'choices',
containerInner: 'choices__inner',
input: 'choices__input',
inputCloned: 'choices__input--cloned',
list: 'choices__list',
listItems: 'choices__list--multiple',
listSingle: 'choices__list--single',
listDropdown: 'choices__list--dropdown',
item: 'choices__item',
itemSelectable: 'choices__item--selectable',
itemDisabled: 'choices__item--disabled',
itemChoice: 'choices__item--choice',
group: 'choices__group',
groupHeading : 'choices__heading',
button: 'choices__button',
activeState: 'is-active',
focusState: 'is-focused',
openState: 'is-open',
disabledState: 'is-disabled',
highlightedState: 'is-highlighted',
hiddenState: 'is-hidden',
flippedState: 'is-flipped',
loadingState: 'is-loading',
containerOuter: 'choices',
containerInner: 'choices__inner',
input: 'choices__input',
inputCloned: 'choices__input--cloned',
list: 'choices__list',
listItems: 'choices__list--multiple',
listSingle: 'choices__list--single',
listDropdown: 'choices__list--dropdown',
item: 'choices__item',
itemSelectable: 'choices__item--selectable',
itemDisabled: 'choices__item--disabled',
itemChoice: 'choices__item--choice',
group: 'choices__group',
groupHeading : 'choices__heading',
button: 'choices__button',
activeState: 'is-active',
focusState: 'is-focused',
openState: 'is-open',
disabledState: 'is-disabled',
highlightedState: 'is-highlighted',
hiddenState: 'is-hidden',
flippedState: 'is-flipped',
loadingState: 'is-loading',
},
// Choices uses the great Fuse library for searching. You
// can find more options here: https://github.com/krisk/Fuse#options
@ -164,7 +165,10 @@ Pass an array of objects:
{
value: 'Value 2',
label: 'Label 2',
id: 2
id: 2,
customProperties: {
random: 'I am a custom property'
}
}]
```
@ -189,6 +193,10 @@ Pass an array of objects:
label: 'Option 2',
selected: false,
disabled: true,
customProperties: {
description: 'Custom description about Option 2',
random: 'Another random custom property'
},
}]
```
@ -268,7 +276,7 @@ Pass an array of objects:
**Input types affected:**`select-one`, `select-multiple`
**Usage:** Specify which fields should be used when a user is searching.
**Usage:** Specify which fields should be used when a user is searching. If you have added custom properties to your choices, you can add these values thus: `['label', 'value', 'customProperties.example']`.
### searchFloor
**Type:** `Number` **Default:** `1`
@ -277,6 +285,13 @@ Pass an array of objects:
**Usage:** The minimum length a search value should be before choices are searched.
### searchResultLimit: 4,
**Type:** `Number` **Default:** `4`
**Input types affected:** `select-one`, `select-multiple`
**Usage:** The maximum amount of search results to show.
### position
**Type:** `String` **Default:** `auto`
@ -317,9 +332,9 @@ Pass an array of objects:
```js
// Sorting via length of label from largest to smallest
const example = new Choices(element, {
sortFilter: function(a, b) {
return b.label.length - a.label.length;
},
sortFilter: function(a, b) {
return b.label.length - a.label.length;
},
};
```
@ -398,29 +413,29 @@ const example = new Choices(element, {
```
classNames: {
containerOuter: 'choices',
containerInner: 'choices__inner',
input: 'choices__input',
inputCloned: 'choices__input--cloned',
list: 'choices__list',
listItems: 'choices__list--multiple',
listSingle: 'choices__list--single',
listDropdown: 'choices__list--dropdown',
item: 'choices__item',
itemSelectable: 'choices__item--selectable',
itemDisabled: 'choices__item--disabled',
itemOption: 'choices__item--choice',
group: 'choices__group',
groupHeading : 'choices__heading',
button: 'choices__button',
activeState: 'is-active',
focusState: 'is-focused',
openState: 'is-open',
disabledState: 'is-disabled',
highlightedState: 'is-highlighted',
hiddenState: 'is-hidden',
flippedState: 'is-flipped',
selectedState: 'is-highlighted',
containerOuter: 'choices',
containerInner: 'choices__inner',
input: 'choices__input',
inputCloned: 'choices__input--cloned',
list: 'choices__list',
listItems: 'choices__list--multiple',
listSingle: 'choices__list--single',
listDropdown: 'choices__list--dropdown',
item: 'choices__item',
itemSelectable: 'choices__item--selectable',
itemDisabled: 'choices__item--disabled',
itemOption: 'choices__item--choice',
group: 'choices__group',
groupHeading : 'choices__heading',
button: 'choices__button',
activeState: 'is-active',
focusState: 'is-focused',
openState: 'is-open',
disabledState: 'is-disabled',
highlightedState: 'is-highlighted',
hiddenState: 'is-hidden',
flippedState: 'is-flipped',
selectedState: 'is-highlighted',
}
```
@ -457,14 +472,14 @@ const example = new Choices(element, {
<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>&bigstar;</span> ${data.label}
</div>
`);
`);
},
choice: (data) => {
return template(`
<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>&bigstar;</span> ${data.label}
</div>
`);
<span>&bigstar;</span> ${data.label}
</div>
`);
},
};
}
@ -498,7 +513,6 @@ example.passedElement.addEventListener('addItem', function(event) {
console.log(event.detail.label);
console.log(event.detail.groupValue);
}, false);
```
### addItem
@ -566,12 +580,14 @@ Methods can be called either directly or by chaining:
const choices = new Choices(element, {
addItems: false,
removeItems: false,
}).setValue(['Set value 1', 'Set value 2']).disable();
})
.setValue(['Set value 1', 'Set value 2'])
.disable();
// Calling a method directly
const choices = new Choices(element, {
addItems: false,
removeItems: false,
addItems: false,
removeItems: false,
});
choices.setValue(['Set value 1', 'Set value 2'])
@ -640,7 +656,7 @@ choices.disable();
### setChoices(choices, value, label, replaceChoices);
**Input types affected:** `select-one`, `select-multiple`
**Usage:** Set choices of select input via an array of objects, a value name and a label name. This behaves the same as passing items via the `choices` option but can be called after initialising Choices. This can also be used to add groups of choices (see example 2); Optionally pass a true `replaceChoices` value to remove any existing choices.
**Usage:** Set choices of select input via an array of objects, a value name and a label name. This behaves the same as passing items via the `choices` option but can be called after initialising Choices. This can also be used to add groups of choices (see example 2); Optionally pass a true `replaceChoices` value to remove any existing choices. Optionally pass a `customProperties` object to add additional data to your choices (useful when searching/filtering etc).
**Example 1:**
@ -648,9 +664,9 @@ choices.disable();
const example = new Choices(element);
example.setChoices([
{value: 'One', label: 'Label One', disabled: true},
{value: 'Two', label: 'Label Two', selected: true},
{value: 'Three', label: 'Label Three'},
{value: 'One', label: 'Label One', disabled: true},
{value: 'Two', label: 'Label Two', selected: true},
{value: 'Three', label: 'Label Three'},
], 'value', 'label', false);
```
@ -660,24 +676,27 @@ example.setChoices([
const example = new Choices(element);
example.setChoices([{
label: 'Group one',
id: 1,
disabled: false,
choices: [
{value: 'Child One', label: 'Child One', selected: true},
{value: 'Child Two', label: 'Child Two', disabled: true},
{value: 'Child Three', label: 'Child Three'},
]
label: 'Group one',
id: 1,
disabled: false,
choices: [
{value: 'Child One', label: 'Child One', selected: true},
{value: 'Child Two', label: 'Child Two', disabled: true},
{value: 'Child Three', label: 'Child Three'},
]
},
{
label: 'Group two',
id: 2,
disabled: false,
choices: [
{value: 'Child Four', label: 'Child Four', disabled: true},
{value: 'Child Five', label: 'Child Five'},
{value: 'Child Six', label: 'Child Six'},
]
label: 'Group two',
id: 2,
disabled: false,
choices: [
{value: 'Child Four', label: 'Child Four', disabled: true},
{value: 'Child Five', label: 'Child Five'},
{value: 'Child Six', label: 'Child Six', customProperties: {
description: 'Custom description about child six',
random: 'Another random custom property'
}},
]
}], 'value', 'label', false);
```
@ -706,9 +725,9 @@ const example = new Choices(element);
// via an array of objects
example.setValue([
{value: 'One', label: 'Label One'},
{value: 'Two', label: 'Label Two'},
{value: 'Three', label: 'Label Three'},
{value: 'One', label: 'Label One'},
{value: 'Two', label: 'Label Two'},
{value: 'Three', label: 'Label Three'},
]);
// or via an array of strings
@ -718,17 +737,17 @@ example.setValue(['Four','Five','Six']);
### setValueByChoice(value);
**Input types affected:** `select-one`, `select-multiple`
**Usage:** Set value of input based on existing Choice.
**Usage:** Set value of input based on existing Choice. `value` can be either a single string or an array of strings
**Example:**
```js
const example = new Choices(element, {
choices: [
{value: 'One', label: 'Label One'},
{value: 'Two', label: 'Label Two', disabled: true},
{value: 'Three', label: 'Label Three'},
],
choices: [
{value: 'One', label: 'Label One'},
{value: 'Two', label: 'Label Two', disabled: true},
{value: 'Three', label: 'Label Three'},
],
});
example.setValueByChoice('Two'); // Choice with value of 'Two' has now been selected.
@ -749,7 +768,7 @@ example.setValueByChoice('Two'); // Choice with value of 'Two' has now been sele
### disable();
**Input types affected:** `text`, `select-one`, `select-multiple`
**Usage:** Disables input from accepting new value/sselecting further choices.
**Usage:** Disables input from accepting new value/selecting further choices.
### enable();
**Input types affected:** `text`, `select-one`, `select-multiple`
@ -768,15 +787,15 @@ example.setValueByChoice('Two'); // Choice with value of 'Two' has now been sele
var example = new Choices(element);
example.ajax(function(callback) {
fetch(url)
.then(function(response) {
response.json().then(function(data) {
callback(data, 'value', 'label');
});
})
.catch(function(error) {
console.log(error);
});
fetch(url)
.then(function(response) {
response.json().then(function(data) {
callback(data, 'value', 'label');
});
})
.catch(function(error) {
console.log(error);
});
});
```
@ -807,15 +826,14 @@ To setup a local environment: clone this repo, navigate into it's directory in a
```npm install```
### NPM tasks
| Task | Usage |
| ------------------- | ------------------------------------------------------------ |
| `npm start` | Fire up local server for development |
| `npm run js:build` | Compile Choices to an uglified JavaScript file |
| `npm run css:watch` | Watch SCSS files for changes. On a change, run build process |
| `npm run css:build` | Compile, minify and prefix SCSS files to CSS |
## Contributions
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
| Task | Usage |
| -------------------- | ------------------------------------------------------------ |
| `npm run start` | Fire up local server for development |
| `npm run test` | Run sequence of tests once |
| `npm run test:watch` | Fire up test server and re-test on file change |
| `npm run js:build` | Compile Choices to an uglified JavaScript file |
| `npm run css:watch` | Watch SCSS files for changes. On a change, run build process |
| `npm run css:build` | Compile, minify and prefix SCSS files to CSS |
## License
MIT License

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

@ -1,4 +1,4 @@
export const addItem = (value, label, id, choiceId, groupId) => {
export const addItem = (value, label, id, choiceId, groupId, customProperties) => {
return {
type: 'ADD_ITEM',
value,
@ -6,6 +6,7 @@ export const addItem = (value, label, id, choiceId, groupId) => {
id,
choiceId,
groupId,
customProperties
};
};
@ -25,7 +26,7 @@ export const highlightItem = (id, highlighted) => {
};
};
export const addChoice = (value, label, id, groupId, disabled, elementId) => {
export const addChoice = (value, label, id, groupId, disabled, elementId, customProperties) => {
return {
type: 'ADD_CHOICE',
value,
@ -34,6 +35,7 @@ export const addChoice = (value, label, id, groupId, disabled, elementId) => {
groupId,
disabled,
elementId: elementId,
customProperties
};
};

View file

@ -1,4 +1,5 @@
import Fuse from 'fuse.js';
import classNames from 'classnames';
import Store from './store/index.js';
import {
addItem,
@ -64,6 +65,7 @@ class Choices {
searchEnabled: true,
searchChoices: true,
searchFloor: 1,
searchResultLimit: 4,
searchFields: ['label', 'value'],
position: 'auto',
resetScrollPosition: true,
@ -334,19 +336,29 @@ class Choices {
// Create a fragment to store our list items (so we don't have to update the DOM for each item)
const choicesFragment = fragment || document.createDocumentFragment();
const filter = this.isSearching ? sortByScore : this.config.sortFilter;
const appendChoice = (choice) => {
const dropdownItem = this._getTemplate('choice', choice);
const shouldRender = this.passedElement.type === 'select-one' || !choice.selected;
if (shouldRender) {
choicesFragment.appendChild(dropdownItem);
}
};
// If sorting is enabled or the user is searching, filter choices
if (this.config.shouldSort || this.isSearching) {
choices.sort(filter);
}
choices.forEach((choice) => {
const dropdownItem = this._getTemplate('choice', choice);
const shouldRender = this.passedElement.type === 'select-one' || !choice.selected;
if (shouldRender) {
choicesFragment.appendChild(dropdownItem);
if (this.isSearching) {
for (let i = 0; i < this.config.searchResultLimit; i++) {
const choice = choices[i];
if (choice) {
appendChoice(choice);
}
}
});
} else {
choices.forEach(choice => appendChoice(choice));
}
return choicesFragment;
}
@ -361,10 +373,10 @@ class Choices {
renderItems(items, fragment) {
// Create fragment to add elements to
const itemListFragment = fragment || document.createDocumentFragment();
// Simplify store data to just values
const itemsFiltered = this.store.getItemsReducedToValues(items);
if (this.isTextElement) {
// Simplify store data to just values
const itemsFiltered = this.store.getItemsReducedToValues(items);
// Assign hidden input array of values
this.passedElement.setAttribute('value', itemsFiltered.join(this.config.delimiter));
} else {
@ -419,7 +431,7 @@ class Choices {
this.choiceList.innerHTML = '';
// Scroll back to top of choices list
if(this.config.resetScrollPosition){
if (this.config.resetScrollPosition) {
this.choiceList.scrollTop = 0;
}
@ -509,7 +521,7 @@ class Choices {
this.store.dispatch(highlightItem(id, true));
if (runEvent) {
if(group && group.value) {
if (group && group.value) {
triggerEvent(this.passedElement, 'highlightItem', {
id,
value: item.value,
@ -545,7 +557,7 @@ class Choices {
this.store.dispatch(highlightItem(id, false));
if(group && group.value) {
if (group && group.value) {
triggerEvent(this.passedElement, 'unhighlightItem', {
id,
value: item.value,
@ -795,13 +807,32 @@ class Choices {
// 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);
this._addChoice(
item.value,
item.label,
true,
false,
-1,
item.customProperties
);
} else {
this._addItem(item.value, item.label, item.id);
this._addItem(
item.value,
item.label,
item.id,
undefined,
item.customProperties
);
}
} else if (itemType === 'String') {
if (passedElementType !== 'text') {
this._addChoice(true, false, item, item, -1);
this._addChoice(
item,
item,
true,
false,
-1
);
} else {
this._addItem(item);
}
@ -840,7 +871,13 @@ class Choices {
if (foundChoice) {
if (!foundChoice.selected) {
this._addItem(foundChoice.value, foundChoice.label, foundChoice.id, foundChoice.groupId);
this._addItem(
foundChoice.value,
foundChoice.label,
foundChoice.id,
foundChoice.groupId,
foundChoice.customProperties
);
} else if (!this.config.silent) {
console.warn('Attempting to select choice already selected');
}
@ -868,19 +905,29 @@ class Choices {
return;
}
// Clear choices if needed
if(replaceChoices) {
if (replaceChoices) {
this._clearChoices();
}
// Add choices if passed
if (choices && choices.length) {
this.containerOuter.classList.remove(this.config.classNames.loadingState);
choices.forEach((result, index) => {
const isSelected = result.selected ? result.selected : false;
const isDisabled = result.disabled ? result.disabled : false;
if (result.choices) {
this._addGroup(result, (result.id || null), value, label);
this._addGroup(
result,
(result.id || null),
value,
label
);
} else {
this._addChoice(isSelected, isDisabled, result[value], result[label]);
this._addChoice(
result[value],
result[label],
result.selected,
result.disabled,
undefined,
result['customProperties']
);
}
});
}
@ -968,7 +1015,9 @@ class Choices {
if (this.initialised === true) {
if (this.isSelectElement) {
// Show loading text
this._handleLoadingState(true);
requestAnimationFrame(() => {
this._handleLoadingState(true)
});
// Run callback
fn(this._ajaxCallback());
}
@ -1092,7 +1141,13 @@ class Choices {
const canAddItem = this._canAddItem(activeItems, choice.value);
if (canAddItem.response) {
this._addItem(choice.value, choice.label, choice.id, choice.groupId);
this._addItem(
choice.value,
choice.label,
choice.id,
choice.groupId,
choice.customProperties
);
this._triggerChange(choice.value);
}
}
@ -1115,7 +1170,7 @@ class Choices {
_handleBackspace(activeItems) {
if (this.config.removeItems && activeItems) {
const lastItem = activeItems[activeItems.length - 1];
const hasHighlightedItems = activeItems.some((item) => item.highlighted === true);
const hasHighlightedItems = activeItems.some(item => item.highlighted);
// If editing the last item is allowed and there are not other selected items,
// we can edit the item value. Otherwise if we can remove items, remove all selected items
@ -1166,10 +1221,15 @@ class Choices {
}
}
// If no duplicates are allowed, and the value already exists
// in the array
const isUnique = !activeItems.some((item) => item.value === isType('String', value) ? value.trim() : value);
const isUnique = !activeItems.some((item) => {
if (isType('String', value)) {
return item.value === value.trim();
}
return item.value === value;
});
if (
!isUnique &&
@ -1178,7 +1238,9 @@ class Choices {
canAddItem
) {
canAddItem = false;
notice = isType('Function', this.config.uniqueItemText) ? this.config.uniqueItemText(value) : this.config.uniqueItemText;
notice = isType('Function', this.config.uniqueItemText) ?
this.config.uniqueItemText(value) :
this.config.uniqueItemText;
}
return {
@ -1195,7 +1257,7 @@ class Choices {
*/
_handleLoadingState(isLoading = true) {
let placeholderItem = this.itemList.querySelector(`.${this.config.classNames.placeholder}`);
if(isLoading) {
if (isLoading) {
this.containerOuter.classList.add(this.config.classNames.loadingState);
this.containerOuter.setAttribute('aria-busy', 'true');
if (this.passedElement.type === 'select-one') {
@ -1211,7 +1273,11 @@ class Choices {
} else {
// Remove loading states/text
this.containerOuter.classList.remove(this.config.classNames.loadingState);
const placeholder = this.config.placeholder ? this.config.placeholderValue || this.passedElement.getAttribute('placeholder') : false;
const placeholder = this.config.placeholder ?
this.config.placeholderValue ||
this.passedElement.getAttribute('placeholder') :
false;
if (this.passedElement.type === 'select-one') {
placeholderItem.innerHTML = placeholder || '';
} else {
@ -1238,12 +1304,23 @@ class Choices {
this._handleLoadingState(false);
// Add each result as a choice
parsedResults.forEach((result, index) => {
const isSelected = result.selected ? result.selected : false;
const isDisabled = result.disabled ? result.disabled : false;
if (result.choices) {
this._addGroup(result, (result.id || null), value, label);
const groupId = (result.id || null);
this._addGroup(
result,
groupId,
value,
label
);
} else {
this._addChoice(isSelected, isDisabled, result[value], result[label]);
this._addChoice(
result[value],
result[label],
result.selected,
result.disabled,
undefined,
result['customProperties']
);
}
});
} else {
@ -1293,7 +1370,7 @@ class Choices {
}
const choices = this.store.getChoices();
const hasUnactiveChoices = choices.some((option) => option.active !== true);
const hasUnactiveChoices = choices.some(option => !option.active);
// Run callback if it is a function
if (this.input === document.activeElement) {
@ -1677,7 +1754,7 @@ class Choices {
const activeItems = this.store.getItemsFilteredByActive();
const hasShiftKey = e.shiftKey;
if(foundTarget = findAncestorByAttrName(target, 'data-button')) {
if (foundTarget = findAncestorByAttrName(target, 'data-button')) {
this._handleButtonAction(activeItems, foundTarget);
} else if (foundTarget = findAncestorByAttrName(target, 'data-item')) {
this._handleItemAction(activeItems, foundTarget, hasShiftKey);
@ -1725,7 +1802,7 @@ class Choices {
this.hideDropdown(true);
}
} else {
const hasHighlightedItems = activeItems.some((item) => item.highlighted === true);
const hasHighlightedItems = activeItems.some(item => item.highlighted);
// De-select any highlighted items
if (hasHighlightedItems) {
@ -1823,7 +1900,7 @@ class Choices {
if (this.containerOuter.contains(target)) {
const activeItems = this.store.getItemsFilteredByActive();
const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState);
const hasHighlightedItems = activeItems.some((item) => item.highlighted === true);
const hasHighlightedItems = activeItems.some(item => item.highlighted);
const blurActions = {
text: () => {
if (target === this.input) {
@ -1995,10 +2072,13 @@ class Choices {
* Add item to store with correct value
* @param {String} value Value to add to store
* @param {String} label Label to add to store
* @param {Number} choiceId ID of the associated choice that was selected
* @param {Number} groupId ID of group choice is within. Negative number indicates no group
* @param {Object} customProperties Object containing user defined properties
* @return {Object} Class instance
* @public
*/
_addItem(value, label, choiceId = -1, groupId = -1) {
_addItem(value, label, choiceId = -1, groupId = -1, customProperties = null) {
let passedValue = isType('String', value) ? value.trim() : value;
const items = this.store.getItems();
const passedLabel = label || passedValue;
@ -2020,14 +2100,14 @@ class Choices {
passedValue += this.config.appendValue.toString();
}
this.store.dispatch(addItem(passedValue, passedLabel, id, passedOptionId, groupId));
this.store.dispatch(addItem(passedValue, passedLabel, id, passedOptionId, groupId, customProperties));
if (this.passedElement.type === 'select-one') {
this.removeActiveItems(id);
}
// Trigger change event
if(group && group.value) {
if (group && group.value) {
triggerEvent(this.passedElement, 'addItem', {
id,
value: passedValue,
@ -2069,7 +2149,7 @@ class Choices {
this.store.dispatch(removeItem(id, choiceId));
if(group && group.value) {
if (group && group.value) {
triggerEvent(this.passedElement, 'removeItem', {
id,
value,
@ -2089,15 +2169,16 @@ class Choices {
/**
* Add choice to dropdown
* @param {Boolean} isSelected Whether choice is selected
* @param {Boolean} isDisabled Whether choice is disabled
* @param {String} value Value of choice
* @param {String} Label Label of choice
* @param {Boolean} isSelected Whether choice is selected
* @param {Boolean} isDisabled Whether choice is disabled
* @param {Number} groupId ID of group choice is within. Negative number indicates no group
* @param {Object} customProperties Object containing user defined properties
* @return
* @private
*/
_addChoice(isSelected, isDisabled, value, label, groupId = -1) {
_addChoice(value, label, isSelected = false, isDisabled = false, groupId = -1, customProperties = null) {
if (typeof value === 'undefined' || value === null) {
return;
}
@ -2108,10 +2189,24 @@ class Choices {
const choiceId = choices ? choices.length + 1 : 1;
const choiceElementId = `${this.baseId}-${this.idNames.itemChoice}-${choiceId}`;
this.store.dispatch(addChoice(value, choiceLabel, choiceId, groupId, isDisabled, choiceElementId));
this.store.dispatch(addChoice(
value,
choiceLabel,
choiceId,
groupId,
isDisabled,
choiceElementId,
customProperties
));
if (isSelected) {
this._addItem(value, choiceLabel, choiceId);
this._addItem(
value,
choiceLabel,
choiceId,
undefined,
customProperties
);
}
}
@ -2139,23 +2234,36 @@ class Choices {
const isDisabled = group.disabled ? group.disabled : false;
if (groupChoices) {
this.store.dispatch(addGroup(group.label, groupId, true, isDisabled));
this.store.dispatch(addGroup(
group.label,
groupId,
true,
isDisabled
));
groupChoices.forEach((option) => {
const isOptDisabled = (option.disabled || (option.parentNode && option.parentNode.disabled)) || false;
const isOptSelected = option.selected ? option.selected : false;
let label;
const isOptDisabled = option.disabled ||
(option.parentNode && option.parentNode.disabled);
let label = isType('Object', option) ?
option[labelKey] :
option.innerHTML;
if (isType('Object', option)) {
label = option[labelKey] || option[valueKey];
} else {
label = option.innerHTML;
}
this._addChoice(isOptSelected, isOptDisabled, option[valueKey], label, groupId);
this._addChoice(
option[valueKey],
label,
option.selected,
isOptDisabled,
groupId,
option.customProperties
);
});
} else {
this.store.dispatch(addGroup(group.label, group.id, false, group.disabled));
this.store.dispatch(addGroup(
group.label,
group.id,
false,
group.disabled
));
}
}
@ -2180,74 +2288,231 @@ class Choices {
* @private
*/
_createTemplates() {
const classNames = this.config.classNames;
const globalClasses = this.config.classNames;
const templates = {
containerOuter: (direction) => {
return strToEl(`
<div class="${classNames.containerOuter}" ${this.isSelectElement ? ( this.config.searchEnabled ? 'role="combobox" aria-autocomplete="list"' : 'role="listbox"') : ''} data-type="${this.passedElement.type}" ${this.passedElement.type === 'select-one' ? 'tabindex="0"' : ''} aria-haspopup="true" aria-expanded="false" dir="${direction}"></div>
<div
class="${globalClasses.containerOuter}"
${this.isSelectElement ? (this.config.searchEnabled ?
'role="combobox" aria-autocomplete="list"' :
'role="listbox"') :
''
}
data-type="${this.passedElement.type}"
${this.passedElement.type === 'select-one' ?
'tabindex="0"' :
''
}
aria-haspopup="true"
aria-expanded="false"
dir="${direction}"
>
</div>
`);
},
containerInner: () => {
return strToEl(`
<div class="${classNames.containerInner}"></div>
<div class="${globalClasses.containerInner}"></div>
`);
},
itemList: () => {
const localClasses = classNames(
globalClasses.list,
{
[globalClasses.listSingle]: (this.passedElement.type === 'select-one'),
[globalClasses.listItems]: (this.passedElement.type !== 'select-one')
}
);
return strToEl(`
<div class="${classNames.list} ${this.passedElement.type === 'select-one' ? classNames.listSingle : classNames.listItems}"></div>
<div class="${localClasses}"></div>
`);
},
placeholder: (value) => {
return strToEl(`
<div class="${classNames.placeholder}">${value}</div>
<div class="${globalClasses.placeholder}">
${value}
</div>
`);
},
item: (data) => {
let localClasses = classNames(
globalClasses.item,
{
[globalClasses.highlightedState]: data.highlighted,
[globalClasses.itemSelectable]: !data.highlighted
}
);
if (this.config.removeItemButton) {
localClasses = classNames(
globalClasses.item,
{
[globalClasses.highlightedState]: data.highlighted,
[globalClasses.itemSelectable]: !data.disabled
}
);
return strToEl(`
<div class="${classNames.item} ${data.highlighted ? classNames.highlightedState : ''} ${!data.disabled ? classNames.itemSelectable : ''}" data-item data-id="${data.id}" data-value="${data.value}" ${data.active ? 'aria-selected="true"' : ''} ${data.disabled ? 'aria-disabled="true"' : ''} data-deletable>
${data.label}<button type="button" class="${classNames.button}" data-button>Remove item</button>
<div
class="${localClasses}"
data-item
data-id="${data.id}"
data-value="${data.value}"
data-deletable
${data.active ?
'aria-selected="true"' :
''
}
${data.disabled ?
'aria-disabled="true"' :
''
}
>
${data.label}<!--
--><button
type="button"
class="${globalClasses.button}"
data-button
aria-label="Remove item: '${data.value}'"
>
Remove item
</button>
</div>
`);
}
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"' : ''}>
<div
class="${localClasses}"
data-item
data-id="${data.id}"
data-value="${data.value}"
${data.active ?
'aria-selected="true"' :
''
}
${data.disabled ?
'aria-disabled="true"' :
''
}
>
${data.label}
</div>
`);
},
choiceList: () => {
return strToEl(`
<div class="${classNames.list}" dir="ltr" role="listbox" ${this.passedElement.type !== 'select-one' ? 'aria-multiselectable="true"' : ''}></div>
<div
class="${globalClasses.list}"
dir="ltr"
role="listbox"
${this.passedElement.type !== 'select-one' ?
'aria-multiselectable="true"' :
''
}
>
</div>
`);
},
choiceGroup: (data) => {
let localClasses = classNames(
globalClasses.group,
{
[globalClasses.itemDisabled]: data.disabled
}
);
return strToEl(`
<div class="${classNames.group} ${data.disabled ? classNames.itemDisabled : ''}" data-group data-id="${data.id}" data-value="${data.value}" role="group" ${data.disabled ? 'aria-disabled="true"' : ''}>
<div class="${classNames.groupHeading}">${data.value}</div>
<div
class="${localClasses}"
data-group
data-id="${data.id}"
data-value="${data.value}"
role="group"
${data.disabled ?
'aria-disabled="true"' :
''
}
>
<div class="${globalClasses.groupHeading}">${data.value}</div>
</div>
`);
},
choice: (data) => {
let localClasses = classNames(
globalClasses.item,
globalClasses.itemChoice,
{
[globalClasses.itemDisabled]: data.disabled,
[globalClasses.itemSelectable]: !data.disabled
}
);
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'} id="${data.elementId}" data-id="${data.id}" data-value="${data.value}" ${data.groupId > 0 ? 'role="treeitem"' : 'role="option"'}>
<div
class="${localClasses}"
data-select-text="${this.config.itemSelectText}"
data-choice
data-id="${data.id}"
data-value="${data.value}"
${data.disabled ?
'data-choice-disabled aria-disabled="true"' :
'data-choice-selectable'
}
id="${data.elementId}"
${data.groupId > 0 ?
'role="treeitem"' :
'role="option"'
}
>
${data.label}
</div>
`);
},
input: () => {
let localClasses = classNames(
globalClasses.input,
globalClasses.inputCloned
);
return strToEl(`
<input type="text" class="${classNames.input} ${classNames.inputCloned}" autocomplete="off" autocapitalize="off" spellcheck="false" role="textbox" aria-autocomplete="list">
<input
type="text"
class="${localClasses}"
autocomplete="off"
autocapitalize="off"
spellcheck="false"
role="textbox"
aria-autocomplete="list"
>
`);
},
dropdown: () => {
let localClasses = classNames(
globalClasses.list,
globalClasses.listDropdown
);
return strToEl(`
<div class="${classNames.list} ${classNames.listDropdown}" aria-expanded="false"></div>
<div
class="${localClasses}"
aria-expanded="false"
>
</div>
`);
},
notice: (label) => {
let localClasses = classNames(
globalClasses.item,
globalClasses.itemChoice
);
return strToEl(`
<div class="${classNames.item} ${classNames.itemChoice}">${label}</div>
<div class="${localClasses}">
${label}
</div>
`);
},
option: (data) => {
@ -2366,26 +2631,43 @@ class Choices {
}
// Determine whether there is a selected choice
const hasSelectedChoice = allChoices.some((choice) => {
return choice.selected === true;
});
const hasSelectedChoice = allChoices.some(choice => choice.selected);
// Add each choice
allChoices.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
if (this.passedElement.type === 'select-one') {
if (hasSelectedChoice || (!hasSelectedChoice && index > 0)) {
// If there is a selected choice already or the choice is not
// the first in the array, add each choice normally
this._addChoice(isSelected, isDisabled, choice.value, choice.label);
this._addChoice(
choice.value,
choice.label,
choice.selected,
choice.disabled,
undefined,
choice.customProperties
);
} else {
// Otherwise pre-select the first choice in the array
this._addChoice(true, false, choice.value, choice.label);
this._addChoice(
choice.value,
choice.label,
true,
false,
undefined,
choice.customProperties
);
}
} else {
this._addChoice(isSelected, isDisabled, choice.value, choice.label);
this._addChoice(
choice.value,
choice.label,
choice.selected,
choice.disabled,
undefined,
choice.customProperties
);
}
});
}
@ -2397,7 +2679,13 @@ class Choices {
if (!item.value) {
return;
}
this._addItem(item.value, item.label, item.id);
this._addItem(
item.value,
item.label,
item.id,
undefined,
item.customProperties
);
} else if (itemType === 'String') {
this._addItem(item);
}

View file

@ -477,10 +477,11 @@ export const getRandomNumber = function(min, max) {
* @return {HTMLElement} Converted node element
*/
export const strToEl = (function() {
var tmpEl = document.createElement('div');
let tmpEl = document.createElement('div');
return function(str) {
var r;
tmpEl.innerHTML = str;
let cleanedInput = str.trim();
let r;
tmpEl.innerHTML = cleanedInput;
r = tmpEl.children[0];
while (tmpEl.firstChild) {

View file

@ -11,11 +11,12 @@ const choices = (state = [], action) => {
elementId: action.elementId,
groupId: action.groupId,
value: action.value,
label: action.label,
disabled: action.disabled,
label: (action.label || action.value),
disabled: (action.disabled || false),
selected: false,
active: true,
score: 9999,
customProperties: action.customProperties
}];
}

View file

@ -10,6 +10,7 @@ const items = (state = [], action) => {
label: action.label,
active: true,
highlighted: false,
customProperties: action.customProperties
}];
return newState.map((item) => {

View file

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

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.8.4">
<link rel="stylesheet" href="assets/styles/css/base.min.css?version=2.8.7">
<!-- 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.8.4">
<script src="assets/scripts/dist/choices.min.js?version=2.8.4"></script>
<link rel="stylesheet" href="assets/styles/css/choices.min.css?version=2.8.7">
<script src="assets/scripts/dist/choices.min.js?version=2.8.7"></script>
<!-- End Choices includes -->
<!--[if lt IE 9]>
@ -63,7 +63,7 @@
<input class="form-control" id="choices-text-prepend-append-value" type="text" value="preset-1, preset-2" placeholder="This is a placeholder">
<label for="choices-text-preset-values">Preset values passed through options</label>
<input class="form-control" id="choices-text-preset-values" type="text" value="olivia@benson.com" placeholder="This is a placeholder">
<input class="form-control" id="choices-text-preset-values" type="text" value="Michael Smith" placeholder="This is a placeholder">
<label for="choices-text-i18n">I18N labels</label>
<input class="form-control" data-trigger id="choices-text-i18n" type="text">
@ -208,7 +208,8 @@
<label for="choices-single-preset-options">Option and option groups added via config</label>
<select class="form-control" name="choices-single-preset-options" id="choices-single-preset-options" placeholder="This is a placeholder"></select>
<label for="choices-single-selected-option">Option selected via config</label>
<label for="choices-single-selected-option">Option selected via config with custom properties</label>
<p><small>Try searching for 'fantastic'</small></p>
<select class="form-control" name="choices-single-selected-option" id="choices-single-selected-option" placeholder="This is a placeholder"></select>
<label for="choices-single-no-sorting">Options without sorting</label>
@ -306,7 +307,13 @@
}).removeActiveItems();
var textPresetVal = new Choices('#choices-text-preset-values', {
items: ['josh@joshuajohnson.co.uk', { value: 'joe@bloggs.co.uk', label: 'Joe Bloggs' } ],
items: ['Josh Johnson', {
value: 'joe@bloggs.co.uk',
label: 'Joe Bloggs',
customProperties: {
description: 'Joe Blogg is such a generic name'
}
}],
});
var multipleDefault = new Choices(document.getElementById('choices-multiple-groups'));
@ -317,14 +324,14 @@
maxItemCount: 5,
}).ajax(function(callback) {
fetch('https://api.discogs.com/artists/55980/releases?token=QBRmstCkwXEvCjTclCpumbtNwvVkEzGAdELXyRyW')
.then(function(response) {
response.json().then(function(data) {
callback(data.releases, 'title', 'title');
.then(function(response) {
response.json().then(function(data) {
callback(data.releases, 'title', 'title');
});
})
.catch(function(error) {
console.error(error);
});
})
.catch(function(error) {
console.error(error);
});
});
var multipleCancelButton = new Choices('#choices-multiple-remove-button', {
@ -358,15 +365,15 @@
placeholderValue: 'Pick an Arctic Monkeys record'
}).ajax(function(callback) {
fetch('https://api.discogs.com/artists/391170/releases?token=QBRmstCkwXEvCjTclCpumbtNwvVkEzGAdELXyRyW')
.then(function(response) {
response.json().then(function(data) {
callback(data.releases, 'title', 'title');
singleFetch.setValueByChoice('Fake Tales Of San Francisco');
.then(function(response) {
response.json().then(function(data) {
callback(data.releases, 'title', 'title');
singleFetch.setValueByChoice('Fake Tales Of San Francisco');
});
})
.catch(function(error) {
console.error(error);
});
})
.catch(function(error) {
console.error(error);
});
});
var singleXhrRemove = new Choices('#choices-single-remove-xhr', {
@ -433,10 +440,13 @@
}], 'value', 'label');
var singleSelectedOpt = new Choices('#choices-single-selected-option', {
searchFields: ['label', 'value', 'customProperties.description'],
choices: [
{value: 'One', label: 'Label One', selected: true},
{value: 'Two', label: 'Label Two', disabled: true},
{value: 'Three', label: 'Label Three'},
{value: 'Three', label: 'Label Three', customProperties: {
description: 'This option is fantastic'
}},
],
}).setValueByChoice('Two');

View file

@ -1,18 +1,18 @@
{
"name": "choices.js",
"version": "2.8.4",
"version": "2.8.7",
"description": "A vanilla JS customisable text input/select box plugin",
"main": "./assets/scripts/dist/choices.min.js",
"scripts": {
"start": "node server.js",
"test": "./node_modules/karma/bin/karma start --single-run --no-auto-watch tests/karma.config.js",
"test:watch": "./node_modules/karma/bin/karma start --auto-watch --no-single-run tests/karma.config.js",
"css:watch": "nodemon -e scss -x \"npm run css:build\"",
"css:build": "npm run css:sass -s && npm run css:prefix -s && npm run css:min -s",
"css:sass": "node-sass --output-style expanded --include-path scss assets/styles/scss/base.scss assets/styles/css/base.css && node-sass --output-style expanded --include-path scss assets/styles/scss/choices.scss assets/styles/css/choices.css",
"css:prefix": "postcss --use autoprefixer -b 'last 2 versions' assets/styles/css/*.css -d assets/styles/css/",
"css:min": "csso assets/styles/css/base.css assets/styles/css/base.min.css && csso assets/styles/css/choices.css assets/styles/css/choices.min.css",
"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",
"version": "node version.js --current $npm_package_version --new $npm_config_newVersion",
"postversion": "npm run js:build"
},
@ -33,6 +33,7 @@
"babel-loader": "^6.2.4",
"babel-preset-es2015": "^6.6.0",
"concurrently": "^3.1.0",
"core-js": "^2.4.1",
"csso": "^1.8.2",
"es6-promise": "^3.2.1",
"eslint": "^3.3.0",
@ -61,8 +62,9 @@
"wrapper-webpack-plugin": "^0.1.7"
},
"dependencies": {
"redux": "^3.3.1",
"fuse.js": "^2.2.2"
"classnames": "^2.2.5",
"fuse.js": "^2.2.2",
"redux": "^3.3.1"
},
"npmName": "choices.js",
"npmFileMap": [

View file

@ -1,39 +1,16 @@
import 'whatwg-fetch';
import 'es6-promise';
import 'core-js/fn/object/assign';
import Choices from '../../assets/scripts/src/choices.js';
if (typeof Object.assign != 'function') {
Object.assign = function (target, varArgs) { // .length of function is 2
if (target == null) { // TypeError if undefined or null
throw new TypeError('Cannot convert undefined or null to object');
}
var to = Object(target);
for (var index = 1; index < arguments.length; index++) {
var nextSource = arguments[index];
if (nextSource != null) { // Skip over if undefined or null
for (var nextKey in nextSource) {
// Avoid bugs when hasOwnProperty is shadowed
if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) {
to[nextKey] = nextSource[nextKey];
}
}
}
}
return to;
};
}
import itemReducer from '../../assets/scripts/src/reducers/items.js';
import choiceReducer from '../../assets/scripts/src/reducers/choices.js';
import {
addItem as addItemAction,
addChoice as addChoiceAction
} from '../../assets/scripts/src/actions/index.js';
describe('Choices', () => {
afterEach(function() {
this.choices.destroy();
});
describe('should initialize Choices', () => {
beforeEach(function() {
this.input = document.createElement('input');
this.input.type = 'text';
@ -43,6 +20,10 @@ describe('Choices', () => {
this.choices = new Choices(this.input);
});
afterEach(function() {
this.choices.destroy();
});
it('should be defined', function() {
expect(this.choices).toBeDefined();
});
@ -78,6 +59,7 @@ describe('Choices', () => {
expect(this.choices.config.searchEnabled).toEqual(jasmine.any(Boolean));
expect(this.choices.config.searchChoices).toEqual(jasmine.any(Boolean));
expect(this.choices.config.searchFloor).toEqual(jasmine.any(Number));
expect(this.choices.config.searchResultLimit).toEqual(jasmine.any(Number));
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);
@ -163,6 +145,10 @@ describe('Choices', () => {
document.body.appendChild(this.input);
});
afterEach(function() {
this.choices.destroy();
});
it('should accept a user inputted value', function() {
this.choices = new Choices(this.input);
@ -271,6 +257,10 @@ describe('Choices', () => {
document.body.appendChild(this.input);
});
afterEach(function() {
this.choices.destroy();
});
it('should open the choice list on focussing', function() {
this.choices = new Choices(this.input);
this.choices.input.focus();
@ -566,6 +556,10 @@ describe('Choices', () => {
});
});
afterEach(function() {
this.choices.destroy();
});
it('should add any pre-defined values', function() {
expect(this.choices.currentState.items.length).toBeGreaterThan(1);
});
@ -603,6 +597,10 @@ describe('Choices', () => {
this.choices = new Choices(this.input);
});
afterEach(function() {
this.choices.destroy();
});
it('should handle highlightItem()', function() {
const items = this.choices.currentState.items;
const randomItem = items[Math.floor(Math.random() * items.length)];
@ -838,6 +836,10 @@ describe('Choices', () => {
this.choices = new Choices(this.input);
});
afterEach(function() {
this.choices.destroy();
});
it('should handle disable()', function() {
this.choices.disable();
@ -862,6 +864,10 @@ describe('Choices', () => {
this.choices = new Choices(this.input);
});
afterEach(function() {
this.choices.destroy();
});
it('should handle clearInput()', function() {
this.choices.clearInput();
expect(this.choices.input.value).toBe('');
@ -898,6 +904,10 @@ describe('Choices', () => {
document.body.appendChild(this.input);
});
afterEach(function() {
this.choices.destroy();
});
it('should flip the dropdown', function() {
this.choices = new Choices(this.input, {
position: 'top'
@ -918,4 +928,133 @@ describe('Choices', () => {
expect(container.classList.contains(this.choices.config.classNames.flippedState)).toBe(false);
});
});
describe('should allow custom properties provided by the user on items or choices', function() {
it('should allow the user to supply custom properties for an item', function() {
const randomItem = {
id: 8999,
choiceId: 9000,
groupId: 9001,
value: 'value',
label: 'label',
customProperties: {
foo: 'bar'
}
}
const expectedState = [{
id: randomItem.id,
choiceId: randomItem.choiceId,
groupId: randomItem.groupId,
value: randomItem.value,
label: randomItem.label,
active: true,
highlighted: false,
customProperties: randomItem.customProperties
}];
const action = addItemAction(
randomItem.value,
randomItem.label,
randomItem.id,
randomItem.choiceId,
randomItem.groupId,
randomItem.customProperties
);
expect(itemReducer([], action)).toEqual(expectedState);
});
it('should allow the user to supply custom properties for a choice', function() {
const randomChoice = {
id: 123,
elementId: 321,
groupId: 213,
value: 'value',
label: 'label',
disabled: false,
customProperties: {
foo: 'bar'
}
}
const expectedState = [{
id: randomChoice.id,
elementId: randomChoice.elementId,
groupId: randomChoice.groupId,
value: randomChoice.value,
label: randomChoice.label,
disabled: randomChoice.disabled,
selected: false,
active: true,
score: 9999,
customProperties: randomChoice.customProperties
}];
const action = addChoiceAction(
randomChoice.value,
randomChoice.label,
randomChoice.id,
randomChoice.groupId,
randomChoice.disabled,
randomChoice.elementId,
randomChoice.customProperties
);
expect(choiceReducer([], action)).toEqual(expectedState);
});
});
describe('should allow custom properties provided by the user on items or choices', function() {
beforeEach(function() {
this.input = document.createElement('select');
this.input.className = 'js-choices';
this.input.setAttribute('multiple', '');
document.body.appendChild(this.input);
});
afterEach(function() {
this.choices.destroy();
});
it('should allow the user to supply custom properties for a choice that will be inherited by the item when the user selects the choice', function() {
const expectedCustomProperties = {
isBestOptionEver: true
};
this.choices = new Choices(this.input);
this.choices.setChoices([{
value: '42',
label: 'My awesome choice',
selected: false,
disabled: false,
customProperties: expectedCustomProperties
}], 'value', 'label', true);
this.choices.setValueByChoice('42');
const selectedItems = this.choices.getValue();
expect(selectedItems.length).toBe(1);
expect(selectedItems[0].customProperties).toBe(expectedCustomProperties);
});
it('should allow the user to supply custom properties when directly creating a selected item', function() {
const expectedCustomProperties = {
isBestOptionEver: true
};
this.choices = new Choices(this.input);
this.choices.setValue([{
value: 'bar',
label: 'foo',
customProperties: expectedCustomProperties
}]);
const selectedItems = this.choices.getValue();
expect(selectedItems.length).toBe(1);
expect(selectedItems[0].customProperties).toBe(expectedCustomProperties);
});
});
});

View file

@ -1,10 +1,10 @@
// Example usage: npm --newVersion=2.7.2 run version
// Example usage: npm --newVersion=2.8.7 run version
const fs = require('fs'),
path = require('path'),
config = {
files: ['bower.json', 'package.json', 'index.html']
};
const fs = require('fs');
const path = require('path');
const config = {
files: ['bower.json', 'package.json', 'index.html', 'version.js']
};
/**
* Convert node arguments into an object
@ -30,11 +30,17 @@ const argvToObject = () => {
return args;
};
/**
* Loop through files updating the current version
* @param {Object} config
*/
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');