Merge branch 'master' into feature/select-placeholders

* master: (48 commits)
  select-one input is now not focusable in disabled state (fixes #137)
  Don't use template literals on demo
  Use nornmal functions on demo
  Update README.md
  Version 2.7.7
  Resolve broken cancel buttons
  Resolve ARIA role bug
  Update README.md
  naming change
  lint fixes
  Fix item custom template select bug
  Version 2.7.6
  Update documentation Added showDropdown/hideDropdown to list of events
  Added showDropdown/hideDropdown events
  Version 2.7.5
  Add label to highlighting events too
  Add example in index.html + refactoring
  Adding generated sources
  Add feature of passing the label through the event of add/remove item
  Fix choices crash with wicked_pdf
  ...

# Conflicts:
#	assets/scripts/src/choices.js
#	index.html
#	tests/spec/choices_spec.js
This commit is contained in:
Adam Mockor 2017-04-04 16:59:12 +02:00
commit 4a8cdecbfd
17 changed files with 1111 additions and 493 deletions

View file

@ -1,4 +1,4 @@
# 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/)
@ -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
@ -64,7 +68,8 @@ Or include Choices directly:
paste: true,
search: true,
searchFloor: 1,
flip: true,
position: 'auto',
resetScrollPosition: true,
regexFilter: null,
shouldSort: true,
sortFilter: () => {...},
@ -246,12 +251,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`
@ -332,18 +344,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`
@ -457,6 +469,7 @@ 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);
@ -467,39 +480,47 @@ 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 +533,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:

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';
@ -61,6 +63,8 @@ class Choices {
searchFloor: 1,
searchPlaceholderValue: null,
flip: true,
position: 'auto',
resetScrollPosition: true,
regexFilter: null,
shouldSort: true,
sortFilter: sortByAlpha,
@ -389,8 +393,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 +413,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 +476,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 +509,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 +624,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 +659,8 @@ class Choices {
this.input.focus();
}
triggerEvent(this.passedElement, 'showDropdown', {});
return this;
}
@ -643,6 +676,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 +687,8 @@ class Choices {
this.input.blur();
}
triggerEvent(this.passedElement, 'hideDropdown', {});
return this;
}
@ -700,34 +736,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 +835,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 +887,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 +908,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;
}
@ -972,6 +1022,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 +1155,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 +1167,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');
};
}
@ -1268,6 +1327,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 +1403,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 +1453,9 @@ class Choices {
[enterKey]: onEnterKey,
[escapeKey]: onEscapeKey,
[upKey]: onDirectionKey,
[pageUpKey]: onDirectionKey,
[downKey]: onDirectionKey,
[pageDownKey]: onDirectionKey,
[deleteKey]: onDeleteKey,
[backKey]: onDeleteKey,
};
@ -1511,13 +1583,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 +1610,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 +1935,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 +1964,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 +1975,14 @@ class Choices {
triggerEvent(this.passedElement, 'removeItem', {
id,
value,
label,
groupValue: group.value,
});
} else {
triggerEvent(this.passedElement, 'removeItem', {
id,
value,
label,
});
}
@ -1953,7 +2034,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 +2322,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);
@ -2292,10 +2373,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

@ -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.7",
"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.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.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.7">
<script src="assets/scripts/dist/choices.min.js?version=2.7.7"></script>
<!-- End Choices includes -->
<!--[if lt IE 9]>
@ -137,6 +137,10 @@
<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>
@ -238,12 +242,12 @@
<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',
@ -439,28 +467,42 @@
}
});
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>\
');
},
};
}
);
});

View file

@ -1,6 +1,6 @@
{
"name": "choices.js",
"version": "2.6.1",
"version": "2.7.7",
"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

@ -78,6 +78,7 @@ describe('Choices', () => {
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.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));
@ -367,9 +368,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 +430,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() {
@ -735,6 +783,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 +843,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);