Ability to disable sorting of choices/groups

This commit is contained in:
Josh Johnson 2016-08-31 19:18:46 +01:00
parent 59977af5d3
commit aff165e4a1
6 changed files with 332 additions and 285 deletions

View file

@ -41,6 +41,7 @@ A vanilla, lightweight (~15kb gzipped 🎉), configurable select box/text input
search: true,
flip: true,
regexFilter: null,
shouldSort: true,
sortFilter: sortByAlpha,
sortFields: ['label', 'value'],
placeholder: true,
@ -225,6 +226,13 @@ Pass an array of objects:
**Usage:** A filter that will need to pass for a user to successfully add an item.
### shouldSort
**Type:** `Boolean` **Default:** `true`
**Input types affected:** `select-one`, `select-multiple`
**Usage:** Whether choices should be sorted. If false, choices will appear in the order they were given.
### sortFilter
**Type:** `Function` **Default:** sortByAlpha

View file

@ -104,6 +104,12 @@
}
}
// Retrieve triggering element (i.e. element with 'data-choice' trigger)
this.passedElement = (0, _utils.isType)('String', element) ? document.querySelector(element) : element;
// If element has already been initalised with Choices, return it silently
if (this.passedElement.getAttribute('data-choice') === 'active') return;
var defaultConfig = {
items: [],
choices: [],
@ -118,6 +124,7 @@
search: true,
flip: true,
regexFilter: null,
shouldSort: true,
sortFilter: _utils.sortByAlpha,
sortFields: ['label', 'value'],
placeholder: true,
@ -172,12 +179,14 @@
this.currentState = {};
this.prevState = {};
this.currentValue = '';
// Retrieve triggering element (i.e. element with 'data-choice' trigger)
this.passedElement = (0, _utils.isType)('String', element) ? document.querySelector(element) : element;
this.highlightPosition = 0;
// Track searching
this.canSearch = this.config.search;
// Track tapping
this.wasTap = true;
// Focus containerOuter but not show dropdown if true
this.focusAndHideDropdown = false;
// Assing preset choices from passed object
this.presetChoices = this.config.choices;
@ -209,25 +218,17 @@
this._onPaste = this._onPaste.bind(this);
this._onInput = this._onInput.bind(this);
// Focus containerOuter but not show dropdown if true
this.focusAndHideDropdown = false;
// Monitor touch taps/scrolls
this.wasTap = true;
// Cutting the mustard
var cuttingTheMustard = 'querySelector' in document && 'addEventListener' in document && 'classList' in document.createElement('div');
if (!cuttingTheMustard) console.error('Choices: Your browser doesn\'t support Choices');
// Input type check
var canInit = this.passedElement && (0, _utils.isElement)(this.passedElement) && ['select-one', 'select-multiple', 'text'].some(function (type) {
var isValidElement = this.passedElement && (0, _utils.isElement)(this.passedElement);
var isValidType = ['select-one', 'select-multiple', 'text'].some(function (type) {
return type === _this.passedElement.type;
});
if (canInit) {
// If element has already been initalised with Choices
if (this.passedElement.getAttribute('data-choice') === 'active') return;
if (isValidElement && isValidType) {
// Let's go
this.init();
} else {
@ -247,7 +248,8 @@
value: function init() {
var callback = arguments.length <= 0 || arguments[0] === undefined ? this.config.callbackOnInit : arguments[0];
if (this.initialised !== true) {
if (this.initialised === true) return;
// Set initialise flag
this.initialised = true;
@ -274,7 +276,6 @@
}
}
}
}
/**
* Destroy Choices and nullify values
@ -285,6 +286,8 @@
}, {
key: 'destroy',
value: function destroy() {
if (this.initialised !== true) return;
this._removeEventListeners();
this.passedElement.classList.remove(this.config.classNames.input, this.config.classNames.hiddenState);
@ -600,7 +603,8 @@
value: function setValue(args) {
var _this8 = this;
if (this.initialised === true) {
if (this.initialised !== true) return;
// Convert args to an itterable array
var values = [].concat(_toConsumableArray(args));
@ -622,7 +626,6 @@
}
}
});
}
return this;
}
@ -639,9 +642,9 @@
value: function setValueByChoice(value) {
var _this9 = this;
if (this.passedElement.type !== 'text') {
(function () {
var choices = _this9.store.getChoices();
if (this.passedElement.type === 'text') return;
var choices = this.store.getChoices();
// If only one value has been passed, convert to array
var choiceValue = (0, _utils.isType)('Array', value) ? value : [value];
@ -662,8 +665,6 @@
console.warn('Attempting to select choice that does not exist');
}
});
})();
}
return this;
}
@ -681,7 +682,8 @@
value: function setChoices(choices, value, label) {
var _this10 = this;
if (this.initialised === true) {
if (this.initialised !== true) return;
if (this.passedElement.type === 'select-one' || this.passedElement.type === 'select-multiple') {
if (!(0, _utils.isType)('Array', choices) || !value) return;
@ -696,7 +698,6 @@
});
}
}
}
return this;
}
@ -785,7 +786,8 @@
value: function ajax(fn) {
var _this11 = this;
if (this.initialised === true) {
if (this.initialised !== true) return;
if (this.passedElement.type === 'select-one' || this.passedElement.type === 'select-multiple') {
this.containerOuter.classList.add(this.config.classNames.loadingState);
this.containerOuter.setAttribute('aria-busy', 'true');
@ -801,6 +803,7 @@
if (results && results.length) {
// Remove loading states/text
_this11.containerOuter.classList.remove(_this11.config.classNames.loadingState);
if (_this11.passedElement.type === 'select-multiple') {
var placeholder = _this11.config.placeholder ? _this11.config.placeholderValue || _this11.passedElement.getAttribute('placeholder') : false;
if (placeholder) {
@ -810,19 +813,14 @@
// Add each result as a choice
results.forEach(function (result, index) {
// Select first choice in list if single select input
if (index === 0 && _this11.passedElement.type === 'select-one') {
_this11._addChoice(true, false, result[value], result[label]);
} else {
_this11._addChoice(false, false, result[value], result[label]);
}
});
}
_this11.containerOuter.removeAttribute('aria-busy');
};
fn(callback);
}
}
return this;
}
@ -1064,6 +1062,7 @@
var _this14 = this;
if (!value) return;
if (this.input === document.activeElement) {
var choices = this.store.getChoices();
var hasUnactiveChoices = choices.some(function (option) {
@ -1086,7 +1085,6 @@
include: 'score'
});
var results = fuse.search(needle);
_this14.currentValue = newValue;
_this14.highlightPosition = 0;
_this14.isSearching = true;
@ -2157,6 +2155,7 @@
// Join choices with preset choices and add them
allChoices.concat(_this19.presetChoices).forEach(function (o, index) {
// Pre-select first choice if it's a single select
if (index === 0 && _this19.passedElement.type === 'select-one') {
_this19._addChoice(true, o.disabled ? o.disabled : false, o.value, o.label);
} else {
@ -2195,7 +2194,12 @@
var groupFragment = fragment || document.createDocumentFragment();
var filter = this.config.sortFilter;
groups.sort(filter).forEach(function (group) {
// If sorting is enabled, filter groups
if (this.config.shouldSort) {
groups.sort(filter);
}
groups.forEach(function (group) {
// Grab options that are children of this group
var groupChoices = choices.filter(function (choice) {
if (_this20.passedElement.type === 'select-one') {
@ -2233,11 +2237,15 @@
var choicesFragment = fragment || document.createDocumentFragment();
var filter = this.isSearching ? _utils.sortByScore : this.config.sortFilter;
choices.sort(filter).forEach(function (choice) {
// If sorting is enabled or the user is searching, filter choices
if (this.config.shouldSort || this.isSearching) {
choices.sort(filter);
}
choices.forEach(function (choice) {
var dropdownItem = _this21._getTemplate('choice', choice);
if (_this21.passedElement.type === 'select-one') {
choicesFragment.appendChild(dropdownItem);
} else if (!choice.selected) {
var shouldRender = _this21.passedElement.type === 'select-one' || !choice.selected;
if (shouldRender) {
choicesFragment.appendChild(dropdownItem);
}
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -42,6 +42,12 @@ export default class Choices {
}
}
// Retrieve triggering element (i.e. element with 'data-choice' trigger)
this.passedElement = isType('String', element) ? document.querySelector(element) : element;
// If element has already been initalised with Choices, return it silently
if (this.passedElement.getAttribute('data-choice') === 'active') return;
const defaultConfig = {
items: [],
choices: [],
@ -111,12 +117,14 @@ export default class Choices {
this.currentState = {};
this.prevState = {};
this.currentValue = '';
// Retrieve triggering element (i.e. element with 'data-choice' trigger)
this.passedElement = isType('String', element) ? document.querySelector(element) : element;
this.highlightPosition = 0;
// Track searching
this.canSearch = this.config.search;
// Track tapping
this.wasTap = true;
// Focus containerOuter but not show dropdown if true
this.focusAndHideDropdown = false;
// Assing preset choices from passed object
this.presetChoices = this.config.choices;
@ -148,23 +156,15 @@ export default class Choices {
this._onPaste = this._onPaste.bind(this);
this._onInput = this._onInput.bind(this);
// Focus containerOuter but not show dropdown if true
this.focusAndHideDropdown = false;
// Monitor touch taps/scrolls
this.wasTap = true;
// Cutting the mustard
const cuttingTheMustard = 'querySelector' in document && 'addEventListener' in document && 'classList' in document.createElement('div');
if (!cuttingTheMustard) console.error('Choices: Your browser doesn\'t support Choices');
// Input type check
const canInit = this.passedElement && isElement(this.passedElement) && ['select-one', 'select-multiple', 'text'].some(type => type === this.passedElement.type);
if (canInit) {
// If element has already been initalised with Choices
if (this.passedElement.getAttribute('data-choice') === 'active') return;
const isValidElement = this.passedElement && isElement(this.passedElement);
const isValidType = ['select-one', 'select-multiple', 'text'].some(type => type === this.passedElement.type);
if (isValidElement && isValidType) {
// Let's go
this.init();
} else {
@ -178,7 +178,8 @@ export default class Choices {
* @public
*/
init(callback = this.config.callbackOnInit) {
if (this.initialised !== true) {
if (this.initialised === true) return;
// Set initialise flag
this.initialised = true;
@ -205,7 +206,6 @@ export default class Choices {
}
}
}
}
/**
* Destroy Choices and nullify values
@ -213,6 +213,8 @@ export default class Choices {
* @public
*/
destroy() {
if (this.initialised !== true) return;
this._removeEventListeners();
this.passedElement.classList.remove(this.config.classNames.input, this.config.classNames.hiddenState);
@ -472,7 +474,8 @@ export default class Choices {
* @public
*/
setValue(args) {
if (this.initialised === true) {
if (this.initialised !== true) return;
// Convert args to an itterable array
const values = [...args];
@ -494,7 +497,6 @@ export default class Choices {
}
}
});
}
return this;
}
@ -506,7 +508,8 @@ export default class Choices {
* @public
*/
setValueByChoice(value) {
if (this.passedElement.type !== 'text') {
if (this.passedElement.type === 'text') return;
const choices = this.store.getChoices();
// If only one value has been passed, convert to array
const choiceValue = isType('Array', value) ? value : [value];
@ -528,7 +531,6 @@ export default class Choices {
console.warn('Attempting to select choice that does not exist');
}
});
}
return this;
}
@ -541,7 +543,8 @@ export default class Choices {
* @public
*/
setChoices(choices, value, label) {
if (this.initialised === true) {
if (this.initialised !== true) return;
if (this.passedElement.type === 'select-one' || this.passedElement.type === 'select-multiple') {
if (!isType('Array', choices) || !value) return;
@ -556,7 +559,6 @@ export default class Choices {
});
}
}
}
return this;
}
@ -628,7 +630,8 @@ export default class Choices {
* @public
*/
ajax(fn) {
if (this.initialised === true) {
if (this.initialised !== true) return;
if (this.passedElement.type === 'select-one' || this.passedElement.type === 'select-multiple') {
this.containerOuter.classList.add(this.config.classNames.loadingState);
this.containerOuter.setAttribute('aria-busy', 'true');
@ -644,6 +647,7 @@ export default class Choices {
if (results && results.length) {
// Remove loading states/text
this.containerOuter.classList.remove(this.config.classNames.loadingState);
if (this.passedElement.type === 'select-multiple') {
const placeholder = this.config.placeholder ? this.config.placeholderValue || this.passedElement.getAttribute('placeholder') : false;
if (placeholder) {
@ -653,19 +657,14 @@ export default class Choices {
// Add each result as a choice
results.forEach((result, index) => {
// Select first choice in list if single select input
if (index === 0 && this.passedElement.type === 'select-one') {
this._addChoice(true, false, result[value], result[label]);
} else {
this._addChoice(false, false, result[value], result[label]);
}
});
}
this.containerOuter.removeAttribute('aria-busy');
};
fn(callback);
}
}
return this;
}
@ -867,6 +866,7 @@ export default class Choices {
*/
_searchChoices(value) {
if (!value) return;
if (this.input === document.activeElement) {
const choices = this.store.getChoices();
const hasUnactiveChoices = choices.some((option) => option.active !== true);
@ -1880,7 +1880,6 @@ export default class Choices {
const passedOptions = Array.from(this.passedElement.options);
const allChoices = [];
// Create array of options from option elements
passedOptions.forEach((o) => {
allChoices.push({
@ -1895,6 +1894,7 @@ export default class Choices {
allChoices
.concat(this.presetChoices)
.forEach((o, index) => {
// Pre-select first choice if it's a single select
if (index === 0 && this.passedElement.type === 'select-one') {
this._addChoice(true, o.disabled ? o.disabled : false, o.value, o.label);
} else {
@ -1927,9 +1927,12 @@ export default class Choices {
const groupFragment = fragment || document.createDocumentFragment();
const filter = this.config.sortFilter;
groups
.sort(filter)
.forEach((group) => {
// If sorting is enabled, filter groups
if (this.config.shouldSort) {
groups.sort(filter);
}
groups.forEach((group) => {
// Grab options that are children of this group
const groupChoices = choices.filter((choice) => {
if (this.passedElement.type === 'select-one') {
@ -1960,17 +1963,17 @@ export default class Choices {
renderChoices(choices, fragment) {
// 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();
if (this.config.shouldSort || this.isSearching) {
const filter = this.isSearching ? sortByScore : this.config.sortFilter;
// If sorting is enabled or the user is searching, filter choices
if (this.config.shouldSort || this.isSearching) {
choices.sort(filter);
}
choices
.forEach((choice) => {
choices.forEach((choice) => {
const dropdownItem = this._getTemplate('choice', choice);
if (this.passedElement.type === 'select-one') {
choicesFragment.appendChild(dropdownItem);
} else if (!choice.selected) {
const shouldRender = this.passedElement.type === 'select-one' || !choice.selected;
if (shouldRender) {
choicesFragment.appendChild(dropdownItem);
}
});

View file

@ -41,7 +41,9 @@
<h1 class="visible-ie">Choices.js</h1>
<p>Choices.js is a lightweight, configurable select box/text input plugin. Similar to Select2 and Selectize but without the jQuery dependency.</p>
<p>For all config options, visit the <a href="https://github.com/jshjohnson/Choices">GitHub repo</a>.</p>
<hr>
<h2>Text inputs</h2>
<label for="choices-1">Limited to 5 values with remove button</label>
<input class="form-control" id="choices-1" type="text" value="preset-1,preset-2" placeholder="Enter something">
@ -170,6 +172,28 @@
<label for="choices-17">Option selected via config</label>
<select class="form-control" name="choices-17" id="choices-17" placeholder="This is a placeholder"></select>
<label for="choices-18">Options without sorting</label>
<select class="form-control" name="choices-18" id="choices-18" placeholder="This is a placeholder">
<option value="Madrid">Madrid</option>
<option value="Toronto">Toronto</option>
<option value="Vancouver">Vancouver</option>
<option value="London">London</option>
<option value="Manchester">Manchester</option>
<option value="Liverpool">Liverpool</option>
<option value="Paris">Paris</option>
<option value="Malaga">Malaga</option>
<option value="Washington" disabled>Washington</option>
<option value="Lyon">Lyon</option>
<option value="Marseille">Marseille</option>
<option value="Hamburg">Hamburg</option>
<option value="Munich">Munich</option>
<option value="Barcelona">Barcelona</option>
<option value="Berlin">Berlin</option>
<option value="Montreal">Montreal</option>
<option value="New York">New York</option>
<option value="Michigan">Michigan</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="cities">States</label>
<select class="form-control" name="cities" id="cities" placeholder="Choose a state">
@ -335,6 +359,10 @@
],
}).setValueByChoice('Two');
var example16 = new Choices('#choices-18', {
shouldSort: false,
});
var cities = new Choices(document.getElementById('cities'), {
callbackOnChange: function(value) {
if(value === 'New York') {