Merge branch 'feature/select-add-items'

This commit is contained in:
Josh Johnson 2017-05-09 09:08:48 +01:00
commit 4f03f7310c
7 changed files with 386 additions and 105 deletions

View file

@ -191,11 +191,11 @@ Pass an array of objects:
**Usage:** The amount of items a user can input/select ("-1" indicates no limit).
### addItems
**Type:** `Boolean` **Default:** `true`
**Type:** `Boolean` **Default:** `true` (for text inputs) `false` (for select elements)
**Input types affected:** `text`
**Input types affected:** `text`, `select-one`, `select-multiple`
**Usage:** Whether a user can add items.
**Usage:** Whether a user can add items.
### removeItems
**Type:** `Boolean` **Default:** `true`

View file

@ -1,4 +1,8 @@
<<<<<<< HEAD
/*! choices.js v2.7.8 | (c) 2017 Josh Johnson | https://github.com/jshjohnson/Choices#readme */
=======
/*! choices.js v2.5.0 | (c) 2016 Josh Johnson | https://github.com/jshjohnson/Choices#readme */
>>>>>>> 291143b... Add dist files
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory();
@ -180,18 +184,6 @@ return /******/ (function(modules) { // webpackBootstrap
callbackOnCreateTemplates: null
};
// Merge options with user options
this.config = (0, _utils.extend)(defaultConfig, userConfig);
// Create data store
this.store = new _index2.default(this.render);
// State tracking
this.initialised = false;
this.currentState = {};
this.prevState = {};
this.currentValue = '';
// Retrieve triggering element (i.e. element with 'data-choice' trigger)
this.element = element;
this.passedElement = (0, _utils.isType)('String', element) ? document.querySelector(element) : element;
@ -203,8 +195,14 @@ return /******/ (function(modules) { // webpackBootstrap
return;
}
this.highlightPosition = 0;
this.canSearch = this.config.search;
// It only makes sense for addItems to be true for
// text inputs by default
if (this.isSelectElement) {
defaultConfig.addItems = false;
}
// Merge options with user options
this.config = (0, _utils.extend)(defaultConfig, userConfig);
// Assing preset choices from passed object
this.presetChoices = this.config.choices;
@ -236,7 +234,16 @@ return /******/ (function(modules) { // webpackBootstrap
this._onPaste = this._onPaste.bind(this);
this._onInput = this._onInput.bind(this);
// Monitor touch taps/scrolls
// Create data store
this.store = new _index2.default(this.render);
// State tracking
this.initialised = false;
this.currentState = {};
this.prevState = {};
this.currentValue = '';
this.highlightPosition = 0;
this.canSearch = this.config.search;
this.wasTap = true;
// Cutting the mustard
@ -475,7 +482,7 @@ return /******/ (function(modules) { // webpackBootstrap
if (this.currentState !== this.prevState) {
// Choices
if (this.currentState.choices !== this.prevState.choices || this.currentState.groups !== this.prevState.groups) {
if (this.passedElement.type === 'select-multiple' || this.passedElement.type === 'select-one') {
if (!this.isTextElement) {
// Get active groups/choices
var activeGroups = this.store.getGroupsFilteredByActive();
var activeChoices = this.store.getChoicesFilteredByActive();
@ -501,8 +508,8 @@ return /******/ (function(modules) { // webpackBootstrap
// If we actually have anything to add to our dropdown
// append it and highlight the first choice
this.choiceList.appendChild(choiceListFragment);
this._highlightChoice();
} else {
<<<<<<< HEAD
// Otherwise show a notice
var dropdownItem = void 0;
var notice = void 0;
@ -513,6 +520,16 @@ return /******/ (function(modules) { // webpackBootstrap
} else {
notice = (0, _utils.isType)('Function', this.config.noChoicesText) ? this.config.noChoicesText() : this.config.noChoicesText;
dropdownItem = this._getTemplate('notice', notice);
=======
var activeItems = this.store.getItemsFilteredByActive();
var canAddItem = this._canAddItem(activeItems, this.input.value);
var dropdownItem = this._getTemplate('notice', this.config.noChoicesText);
if (this.config.addItems && canAddItem.notice) {
dropdownItem = this._getTemplate('notice', canAddItem.notice);
} else if (this.isSearching) {
dropdownItem = this._getTemplate('notice', this.config.noResultsText);
>>>>>>> 291143b... Add dist files
}
this.choiceList.appendChild(dropdownItem);
@ -522,11 +539,11 @@ return /******/ (function(modules) { // webpackBootstrap
// Items
if (this.currentState.items !== this.prevState.items) {
var activeItems = this.store.getItemsFilteredByActive();
if (activeItems) {
var _activeItems = this.store.getItemsFilteredByActive();
if (_activeItems) {
// Create a fragment to store our list items
// (so we don't have to update the DOM for each item)
var itemListFragment = this.renderItems(activeItems);
var itemListFragment = this.renderItems(_activeItems);
// Clear list
this.itemList.innerHTML = '';
@ -1222,10 +1239,10 @@ return /******/ (function(modules) { // webpackBootstrap
if (canAddItem.response) {
this._addItem(choice.value, choice.label, choice.id, choice.groupId);
this._triggerChange(choice.value);
}
}
this._triggerChange(choice.value);
this.clearInput(this.passedElement);
// We wont to close the dropdown if we are dealing with a single select box
@ -1281,20 +1298,20 @@ return /******/ (function(modules) { // webpackBootstrap
var canAddItem = true;
var notice = (0, _utils.isType)('Function', this.config.addItemText) ? this.config.addItemText(value) : this.config.addItemText;
if (this.passedElement.type === 'select-multiple' || this.passedElement.type === 'text') {
if (this.config.maxItemCount > 0 && this.config.maxItemCount <= this.itemList.children.length) {
// If there is a max entry limit and we have reached that limit
// don't update
canAddItem = false;
notice = (0, _utils.isType)('Function', this.config.maxItemText) ? this.config.maxItemText(this.config.maxItemCount) : this.config.maxItemText;
}
}
if (this.passedElement.type === 'text' && this.config.addItems) {
if (this.config.addItems) {
var isUnique = !activeItems.some(function (item) {
return item.value === value.trim();
return item.value === value.trim() || item.label === value.trim();
});
if (this.passedElement.type === 'select-multiple' || this.passedElement.type === 'text') {
if (this.config.maxItemCount > 0 && this.config.maxItemCount <= this.itemList.children.length) {
// If there is a max entry limit and we have reached that limit
// don't update
canAddItem = false;
notice = (0, _utils.isType)('Function', this.config.maxItemText) ? this.config.maxItemText(this.config.maxItemCount) : this.config.maxItemText;
}
}
// If a user has supplied a regular expression filter
if (this.config.regexFilter) {
// Determine whether we can update based on whether
@ -1417,7 +1434,8 @@ return /******/ (function(modules) { // webpackBootstrap
this.currentValue = newValue;
this.highlightPosition = 0;
this.isSearching = true;
this.store.dispatch((0, _index3.filterChoices)(results));
return results;
}
}
@ -1441,10 +1459,26 @@ return /******/ (function(modules) { // webpackBootstrap
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) {
<<<<<<< HEAD
// Check flag to filter search input
if (this.config.searchChoices) {
// Filter available choices
this._searchChoices(value);
=======
// Filter available choices
var results = this._searchChoices(value);
if (results) {
this.store.dispatch((0, _index3.filterChoices)(results));
}
// Run callback if it is a function
if (callback) {
if ((0, _utils.isType)('Function', callback)) {
callback.call(this, value);
} else {
console.error('callbackOnSearch: Callback is not a function');
}
>>>>>>> 291143b... Add dist files
}
// Trigger search event
(0, _utils.triggerEvent)(this.passedElement, 'search', {
@ -1588,26 +1622,112 @@ return /******/ (function(modules) { // webpackBootstrap
};
var onEnterKey = function onEnterKey() {
var highlighted = _this17.dropdown.querySelector('.' + _this17.config.classNames.highlightedState);
if (hasActiveDropdown && highlighted) {
// If we have a highlighted choice, select it
_this17._handleChoiceAction(activeItems, highlighted);
} else if (passedElementType === 'select-one') {
// Open single select dropdown if it's not active
if (!hasActiveDropdown) {
_this17.showDropdown(true);
e.preventDefault();
}
}
// If enter key is pressed and the input has a value
<<<<<<< HEAD
if (passedElementType === 'text' && target.value) {
var value = _this16.input.value;
var canAddItem = _this16._canAddItem(activeItems, value);
=======
if (target.value) {
<<<<<<< HEAD
var value = _this17.input.value;
var canAddItem = _this17._canAddItem(activeItems, value);
>>>>>>> 291143b... Add dist files
// All is good, add
if (canAddItem.response) {
<<<<<<< HEAD
if (hasActiveDropdown) {
_this16.hideDropdown();
}
<<<<<<< HEAD
_this16._addItem(value);
_this16._triggerChange(value);
_this16.clearInput(_this16.passedElement);
=======
=======
// Track whether we will end up adding an item
var willAddItem = _this17.isTextElement || _this17.isSelectElement && _this17.config.addItems;
>>>>>>> acb2451... Update dist files, after FF from main repo
=======
(function () {
var value = _this17.input.value;
var canAddItem = _this17._canAddItem(activeItems, value);
// All is good, add
if (canAddItem.response) {
// Track whether we will end up adding an item
var willAddItem = _this17.isTextElement || _this17.isSelectElement && _this17.config.addItems;
>>>>>>> c4dd94f... Update dist files
if (willAddItem) {
if (hasActiveDropdown) {
_this17.hideDropdown();
}
if (_this17.isTextElement) {
_this17._addItem(value);
} else {
var matchingChoices = [];
var isUnique = void 0;
var duplicateItems = _this17.config.duplicateItems;
if (!duplicateItems) {
matchingChoices = _this17.store.getChoices().filter(function (choice) {
return choice.label === value.trim();
});
isUnique = !_this17.store.getItemsFilteredByActive().some(function (item) {
return item.label === value.trim();
});
}
if (duplicateItems || matchingChoices.length === 0 && isUnique) {
_this17._addChoice(true, false, value, value);
}
if (duplicateItems || isUnique) {
if (matchingChoices[0]) {
_this17._addItem(matchingChoices[0].value, matchingChoices[0].label, matchingChoices[0].id);
}
}
_this17.containerOuter.focus();
}
<<<<<<< HEAD
<<<<<<< HEAD
_this17._triggerChange(value);
_this17.clearInput(_this17.passedElement);
>>>>>>> 291143b... Add dist files
=======
_this17._triggerChange(value);
_this17.clearInput(_this17.passedElement);
}
>>>>>>> acb2451... Update dist files, after FF from main repo
}
=======
_this17._triggerChange(value);
_this17.clearInput(_this17.passedElement);
}
}
})();
>>>>>>> c4dd94f... Update dist files
}
if (target.hasAttribute('data-button')) {
_this16._handleButtonAction(activeItems, target);
e.preventDefault();
}
<<<<<<< HEAD
if (hasActiveDropdown) {
e.preventDefault();
@ -1624,6 +1744,8 @@ return /******/ (function(modules) { // webpackBootstrap
e.preventDefault();
}
}
=======
>>>>>>> 291143b... Add dist files
};
var onEscapeKey = function onEscapeKey() {
@ -2493,7 +2615,10 @@ return /******/ (function(modules) { // webpackBootstrap
}
}
if (!this.config.addItems) this.disable();
// Disable text input if no entry allowed
if (!this.config.addItems && this.isTextElement) {
this.disable();
}
containerOuter.appendChild(containerInner);
containerOuter.appendChild(dropdown);
@ -4064,7 +4189,12 @@ return /******/ (function(modules) { // webpackBootstrap
if (value == null) {
return value === undefined ? undefinedTag : nullTag;
}
<<<<<<< HEAD
return (symToStringTag && symToStringTag in Object(value))
=======
value = Object(value);
return (symToStringTag && symToStringTag in value)
>>>>>>> 291143b... Add dist files
? getRawTag(value)
: objectToString(value);
}
@ -4304,16 +4434,16 @@ return /******/ (function(modules) { // webpackBootstrap
/* 18 */
/***/ function(module, exports) {
module.exports = function(module) {
if(!module.webpackPolyfill) {
module.deprecate = function() {};
module.paths = [];
// module.parent = undefined by default
module.children = [];
module.webpackPolyfill = 1;
}
return module;
}
module.exports = function(module) {
if(!module.webpackPolyfill) {
module.deprecate = function() {};
module.paths = [];
// module.parent = undefined by default
module.children = [];
module.webpackPolyfill = 1;
}
return module;
}
/***/ },

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -116,18 +116,6 @@ class Choices {
callbackOnCreateTemplates: null,
};
// Merge options with user options
this.config = extend(defaultConfig, userConfig);
// Create data store
this.store = new Store(this.render);
// State tracking
this.initialised = false;
this.currentState = {};
this.prevState = {};
this.currentValue = '';
// Retrieve triggering element (i.e. element with 'data-choice' trigger)
this.element = element;
this.passedElement = isType('String', element) ? document.querySelector(element) : element;
@ -139,8 +127,14 @@ class Choices {
return;
}
this.highlightPosition = 0;
this.canSearch = this.config.search;
// It only makes sense for addItems to be true for
// text inputs by default
if (this.isSelectElement) {
defaultConfig.addItems = false;
}
// Merge options with user options
this.config = extend(defaultConfig, userConfig);
// Assing preset choices from passed object
this.presetChoices = this.config.choices;
@ -172,7 +166,16 @@ class Choices {
this._onPaste = this._onPaste.bind(this);
this._onInput = this._onInput.bind(this);
// Monitor touch taps/scrolls
// Create data store
this.store = new Store(this.render);
// State tracking
this.initialised = false;
this.currentState = {};
this.prevState = {};
this.currentValue = '';
this.highlightPosition = 0;
this.canSearch = this.config.search;
this.wasTap = true;
// Cutting the mustard
@ -385,8 +388,7 @@ class Choices {
// Choices
if (this.currentState.choices !== this.prevState.choices ||
this.currentState.groups !== this.prevState.groups) {
if (this.passedElement.type === 'select-multiple' ||
this.passedElement.type === 'select-one') {
if (!this.isTextElement) {
// Get active groups/choices
const activeGroups = this.store.getGroupsFilteredByActive();
const activeChoices = this.store.getChoicesFilteredByActive();
@ -412,18 +414,15 @@ class Choices {
// If we actually have anything to add to our dropdown
// append it and highlight the first choice
this.choiceList.appendChild(choiceListFragment);
this._highlightChoice();
} else {
// Otherwise show a notice
let dropdownItem;
let notice;
const activeItems = this.store.getItemsFilteredByActive();
const canAddItem = this._canAddItem(activeItems, this.input.value);
let dropdownItem = this._getTemplate('notice', this.config.noChoicesText);
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);
if (this.config.addItems && canAddItem.notice) {
dropdownItem = this._getTemplate('notice', canAddItem.notice);
} else if (this.isSearching) {
dropdownItem = this._getTemplate('notice', this.config.noResultsText);
}
this.choiceList.appendChild(dropdownItem);
@ -1033,10 +1032,10 @@ class Choices {
if (canAddItem.response) {
this._addItem(choice.value, choice.label, choice.id, choice.groupId);
this._triggerChange(choice.value);
}
}
this._triggerChange(choice.value);
this.clearInput(this.passedElement);
// We wont to close the dropdown if we are dealing with a single select box
@ -1084,17 +1083,17 @@ class Choices {
let canAddItem = true;
let notice = isType('Function', this.config.addItemText) ? this.config.addItemText(value) : this.config.addItemText;
if (this.passedElement.type === 'select-multiple' || this.passedElement.type === 'text') {
if (this.config.maxItemCount > 0 && this.config.maxItemCount <= this.itemList.children.length) {
// If there is a max entry limit and we have reached that limit
// don't update
canAddItem = false;
notice = isType('Function', this.config.maxItemText) ? this.config.maxItemText(this.config.maxItemCount) : this.config.maxItemText;
}
}
if (this.config.addItems) {
const isUnique = !activeItems.some((item) => (item.value === value.trim()) || (item.label === value.trim()));
if (this.passedElement.type === 'text' && this.config.addItems) {
const isUnique = !activeItems.some((item) => item.value === value.trim());
if (this.passedElement.type === 'select-multiple' || this.passedElement.type === 'text') {
if (this.config.maxItemCount > 0 && this.config.maxItemCount <= this.itemList.children.length) {
// If there is a max entry limit and we have reached that limit
// don't update
canAddItem = false;
notice = isType('Function', this.config.maxItemText) ? this.config.maxItemText(this.config.maxItemCount) : this.config.maxItemText;
}
}
// If a user has supplied a regular expression filter
if (this.config.regexFilter) {
@ -1205,7 +1204,8 @@ class Choices {
this.currentValue = newValue;
this.highlightPosition = 0;
this.isSearching = true;
this.store.dispatch(filterChoices(results));
return results;
}
}
@ -1227,7 +1227,10 @@ class Choices {
// Check flag to filter search input
if (this.config.searchChoices) {
// Filter available choices
this._searchChoices(value);
const results = this._searchChoices(value);
if (results) {
this.store.dispatch(filterChoices(results));
}
}
// Trigger search event
triggerEvent(this.passedElement, 'search', {
@ -1357,19 +1360,66 @@ class Choices {
};
const onEnterKey = () => {
const highlighted = this.dropdown.querySelector(`.${this.config.classNames.highlightedState}`);
if (hasActiveDropdown && highlighted) {
// If we have a highlighted choice, select it
this._handleChoiceAction(activeItems, highlighted);
} else if (passedElementType === 'select-one') {
// Open single select dropdown if it's not active
if (!hasActiveDropdown) {
this.showDropdown(true);
e.preventDefault();
}
}
// If enter key is pressed and the input has a value
if (passedElementType === 'text' && target.value) {
if (target.value) {
const value = this.input.value;
const canAddItem = this._canAddItem(activeItems, value);
// All is good, add
if (canAddItem.response) {
if (hasActiveDropdown) {
this.hideDropdown();
// Track whether we will end up adding an item
const willAddItem = this.isTextElement || (this.isSelectElement && this.config.addItems);
if (willAddItem) {
if (hasActiveDropdown) {
this.hideDropdown();
}
if (this.isTextElement) {
this._addItem(value);
} else {
let matchingChoices = [];
let isUnique;
const duplicateItems = this.config.duplicateItems;
if (!duplicateItems) {
matchingChoices = this.store
.getChoices()
.filter((choice) => choice.label === value.trim());
isUnique = !this.store
.getItemsFilteredByActive()
.some((item) => item.label === value.trim());
}
if (duplicateItems || (matchingChoices.length === 0 && isUnique)) {
this._addChoice(true, false, value, value);
}
if (duplicateItems || isUnique) {
if (matchingChoices[0]) {
this._addItem(
matchingChoices[0].value,
matchingChoices[0].label,
matchingChoices[0].id
);
}
}
this.containerOuter.focus();
}
this._triggerChange(value);
this.clearInput(this.passedElement);
}
this._addItem(value);
this._triggerChange(value);
this.clearInput(this.passedElement);
}
}
@ -2214,7 +2264,10 @@ class Choices {
}
}
if (!this.config.addItems) this.disable();
// Disable text input if no entry allowed
if (!this.config.addItems && this.isTextElement) {
this.disable();
}
containerOuter.appendChild(containerInner);
containerOuter.appendChild(dropdown);

View file

@ -141,11 +141,19 @@
<p id="message"></p>
<select id="choices-multiple-labels" multiple></select>
<label for="choices-multiple-add-items">Add items if they don't exist</label>
<select class="form-control" name="choices-multiple-add-items" id="choices-multiple-add-items" placeholder="This is a placeholder" multiple>
<option value="React">England</option>
<option value="React">Wales</option>
<option value="React">Scotland</option>
<option value="React">Northern Ireland</option>
</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" placeholder="This is a search placeholder">
<label for="choices-single-placeholder">Default</label>
<select class="form-control" data-trigger name="choices-single-placeholder" id="choices-single-placeholder" placeholder="This is a search placeholder">
<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>
@ -242,6 +250,14 @@
<option value="Vue">Vue</option>
</select>
<label for="choices-single-add-items">Add items if they don't exist</label>
<select class="form-control" name="choices-single-add-items" id="choices-single-add-items" placeholder="This is a placeholder">
<option value="React">England</option>
<option value="React">Wales</option>
<option value="React">Scotland</option>
<option value="React">Northern Ireland</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>
<label for="states">States</label>
<select class="form-control" name="states" id="states" placeholder="Choose a state">
@ -355,6 +371,13 @@
document.getElementById('message').innerHTML = 'You just removed "' + event.detail.label + '"';
});
var multipleAddItems = new Choices('#choices-multiple-add-items', {
addItems: true,
duplicateItems: false,
});
var multipleAddItems = new Choices('#choices-single-placeholder');
var singleFetch = new Choices('#choices-single-remote-fetch', {
placeholder: true,
placeholderValue: 'Pick an Arctic Monkeys record'
@ -444,6 +467,10 @@
var singleNoSorting = new Choices('#choices-single-no-sorting', {
shouldSort: false,
addItems: true,
callbackOnChange: function(a, b) {
console.log(this.currentState);
}
});
var states = new Choices(document.getElementById('states'));
@ -456,6 +483,11 @@
}
});
var singleAddItems = new Choices('#choices-single-add-items', {
addItems: true,
duplicateItems: false,
});
var customTemplates = new Choices(document.getElementById('choices-single-custom-templates'), {
callbackOnCreateTemplates: function (strToEl) {
var classNames = this.config.classNames;
@ -494,6 +526,16 @@
}
});
var states = new Choices(document.getElementById('states'), {
callbackOnChange: function(value) {
if(value === 'New York') {
boroughs.enable();
} else {
boroughs.disable();
}
}
});
var boroughs = new Choices(document.getElementById('boroughs')).disable();
});
</script>

View file

@ -284,7 +284,7 @@ describe('Choices', () => {
this.choices = new Choices(this.input);
this.choices.input.focus();
for (var i = 0; i < 2; i++) {
for (var i = 0; i < 3; i++) {
// Key down to third choice
this.choices._onKeyDown({
target: this.choices.input,
@ -317,7 +317,7 @@ describe('Choices', () => {
preventDefault: () => {}
});
expect(this.choices.currentState.items.length).toBe(2);
expect(this.choices.currentState.items.length).toBe(1);
});
it('should trigger add/change event on selection', function() {
@ -333,12 +333,16 @@ describe('Choices', () => {
this.choices.input.focus();
// Key down to second choice
this.choices._onKeyDown({
target: this.choices.input,
keyCode: 40,
ctrlKey: false,
preventDefault: () => {}
});
let count = 0;
while (count < 2) {
this.choices._onKeyDown({
target: this.choices.input,
keyCode: 40,
ctrlKey: false,
preventDefault: () => {}
});
count++;
}
// Key down to select choice
this.choices._onKeyDown({