Better sort handling + sort tests

This commit is contained in:
Josh Johnson 2016-09-04 13:44:31 +01:00
parent 34bce568a2
commit ca86265731
3 changed files with 119 additions and 79 deletions

View file

@ -42,12 +42,6 @@ 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: [],
@ -117,14 +111,12 @@ export default class Choices {
this.currentState = {};
this.prevState = {};
this.currentValue = '';
this.highlightPosition = 0;
// Track searching
// Retrieve triggering element (i.e. element with 'data-choice' trigger)
this.passedElement = isType('String', element) ? document.querySelector(element) : element;
this.highlightPosition = 0;
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;
@ -156,15 +148,23 @@ 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 isValidElement = this.passedElement && isElement(this.passedElement);
const isValidType = ['select-one', 'select-multiple', 'text'].some(type => type === this.passedElement.type);
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;
if (isValidElement && isValidType) {
// Let's go
this.init();
} else {
@ -174,11 +174,11 @@ export default class Choices {
/**
* Initialise Choices
* @return {Object} Class instance
* @return
* @public
*/
init(callback = this.config.callbackOnInit) {
if (this.initialised === false) {
if (this.initialised !== true) {
// Set initialise flag
this.initialised = true;
@ -205,32 +205,27 @@ export default class Choices {
}
}
}
return this;
}
/**
* Destroy Choices and nullify values
* @return {Object} Class instance
* @return
* @public
*/
destroy() {
if (this.initialised === true) {
this._removeEventListeners();
this._removeEventListeners();
this.passedElement.classList.remove(this.config.classNames.input, this.config.classNames.hiddenState);
this.passedElement.tabIndex = '';
this.passedElement.removeAttribute('style', 'display:none;');
this.passedElement.removeAttribute('aria-hidden');
this.passedElement.classList.remove(this.config.classNames.input, this.config.classNames.hiddenState);
this.passedElement.tabIndex = '';
this.passedElement.removeAttribute('style', 'display:none;');
this.passedElement.removeAttribute('aria-hidden');
this.containerOuter.outerHTML = this.passedElement.outerHTML;
this.containerOuter.outerHTML = this.passedElement.outerHTML;
this.passedElement = null;
this.userConfig = null;
this.config = null;
this.store = null;
this.initialised = false;
}
return this;
this.passedElement = null;
this.userConfig = null;
this.config = null;
this.store = null;
}
/**
@ -500,6 +495,7 @@ export default class Choices {
}
});
}
return this;
}
@ -510,29 +506,29 @@ export default class Choices {
* @public
*/
setValueByChoice(value) {
if (this.passedElement.type === 'text') return;
if (this.passedElement.type !== 'text') {
const choices = this.store.getChoices();
// If only one value has been passed, convert to array
const choiceValue = isType('Array', value) ? value : [value];
const choices = this.store.getChoices();
// If only one value has been passed, convert to array
const choiceValue = isType('Array', value) ? value : [value];
// Loop through each value and
choiceValue.forEach((val) => {
const foundChoice = choices.find((choice) => {
// Check 'value' property exists and the choice isn't already selected
return choice.value === val;
});
// Loop through each value and
choiceValue.forEach((val) => {
const foundChoice = choices.find((choice) => {
// Check 'value' property exists and the choice isn't already selected
return choice.value === val;
});
if (foundChoice) {
if (!foundChoice.selected) {
this._addItem(foundChoice.value, foundChoice.label, foundChoice.id);
if (foundChoice) {
if (!foundChoice.selected) {
this._addItem(foundChoice.value, foundChoice.label, foundChoice.id);
} else {
console.warn('Attempting to select choice already selected');
}
} else {
console.warn('Attempting to select choice already selected');
console.warn('Attempting to select choice that does not exist');
}
} else {
console.warn('Attempting to select choice that does not exist');
}
});
});
}
return this;
}
@ -595,7 +591,7 @@ export default class Choices {
*/
disable() {
this.passedElement.disabled = true;
if (this.initialised === true) {
if (this.initialised) {
if (!this.containerOuter.classList.contains(this.config.classNames.disabledState)) {
this._removeEventListeners();
this.passedElement.setAttribute('disabled', '');
@ -613,7 +609,7 @@ export default class Choices {
*/
enable() {
this.passedElement.disabled = false;
if (this.initialised === true) {
if (this.initialised) {
if (this.containerOuter.classList.contains(this.config.classNames.disabledState)) {
this._addEventListeners();
this.passedElement.removeAttribute('disabled');
@ -648,7 +644,6 @@ 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) {
@ -867,7 +862,6 @@ 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);
@ -888,6 +882,7 @@ export default class Choices {
include: 'score',
});
const results = fuse.search(needle);
this.currentValue = newValue;
this.highlightPosition = 0;
this.isSearching = true;
@ -1879,7 +1874,8 @@ export default class Choices {
});
} else {
const passedOptions = Array.from(this.passedElement.options);
const allChoices = [];
const filter = this.config.sortFilter;
const allChoices = this.presetChoices;
// Create array of options from option elements
passedOptions.forEach((o) => {
@ -1891,17 +1887,34 @@ export default class Choices {
});
});
// Join choices with preset choices and add them
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);
// If sorting is enabled or the user is searching, filter choices
if (this.config.shouldSort) {
allChoices.sort(filter);
}
// Determine whether there is a selected choice
const hasSelectedChoice = allChoices.some((choice) => {
return choice.selected === true;
});
// 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);
} else {
this._addChoice(o.selected ? o.selected : false, o.disabled ? o.disabled : false, o.value, o.label);
// Otherwise pre-select the first choice in the array
this._addChoice(true, false, choice.value, choice.label);
}
});
} else {
this._addChoice(isSelected, isDisabled, choice.value, choice.label);
}
});
}
} else if (this.passedElement.type === 'text') {
// Add any preset values seperated by delimiter
@ -1946,7 +1959,6 @@ export default class Choices {
if (groupChoices.length >= 1) {
const dropdownGroup = this._getTemplate('choiceGroup', group);
groupFragment.appendChild(dropdownGroup);
this.renderChoices(groupChoices, groupFragment);
}
});
@ -2005,7 +2017,6 @@ export default class Choices {
items.forEach((item) => {
// Create a standard select option
const option = this._getTemplate('option', item);
// Append it to fragment
selectedOptionsFragment.appendChild(option);
});

View file

@ -195,13 +195,13 @@
</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">
<label for="states">States</label>
<select class="form-control" name="states" id="states" placeholder="Choose a state">
<option value="Michigan">Michigan</option>
<option value="Texas">Texas</option>
<option value="Chicago">Chicago</option>
<option value="New York">New York</option>
<option value="Washington">Washington</option>
<option value="Michigan">Michigan</option>
</select>
<label for="boroughs">Boroughs</label>
@ -353,7 +353,7 @@
var example15 = new Choices('#choices-17', {
choices: [
{value: 'One', label: 'Label One'},
{value: 'One', label: 'Label One', selected: true},
{value: 'Two', label: 'Label Two', disabled: true},
{value: 'Three', label: 'Label Three'},
],
@ -363,7 +363,7 @@
shouldSort: false,
});
var cities = new Choices(document.getElementById('cities'), {
var states = new Choices(document.getElementById('states'), {
callbackOnChange: function(value) {
if(value === 'New York') {
boroughs.enable();

View file

@ -133,7 +133,6 @@ describe('Choices', function() {
this.input.placeholder = 'Placeholder text';
document.body.appendChild(this.input);
});
it('should accept a user inputted value', function() {
@ -236,28 +235,27 @@ describe('Choices', function() {
const option = document.createElement('option');
option.value = `Value ${i}`;
option.innerHTML = `Value ${i}`;
option.innerHTML = `Label ${i}`;
this.input.appendChild(option);
}
document.body.appendChild(this.input);
this.choices = new Choices(this.input, {
removeItemButton: true
});
});
it('should open the choice list on focussing', function() {
this.choices = new Choices(this.input);
this.choices.input.focus();
expect(this.choices.dropdown.classList).toContain(this.choices.config.classNames.activeState);
});
it('should select the first choice', function() {
this.choices = new Choices(this.input);
expect(this.choices.currentState.items[0].value).toContain('Value 1');
});
it('should highlight the choices on keydown', function() {
this.choices = new Choices(this.input);
this.choices.input.focus();
for (var i = 0; i < 2; i++) {
@ -274,6 +272,7 @@ describe('Choices', function() {
});
it('should select choice on enter key press', function() {
this.choices = new Choices(this.input);
this.choices.input.focus();
// Key down to second choice
@ -295,6 +294,7 @@ describe('Choices', function() {
});
it('should trigger a change callback on selection', function() {
this.choices = new Choices(this.input);
spyOn(this.choices.config, 'callbackOnChange');
this.choices.input.focus();
@ -317,6 +317,7 @@ describe('Choices', function() {
});
it('should open the dropdown on click', function() {
this.choices = new Choices(this.input);
const container = this.choices.containerOuter;
this.choices._onClick({
target: container,
@ -328,6 +329,7 @@ describe('Choices', function() {
});
it('should close the dropdown on double click', function() {
this.choices = new Choices(this.input);
const container = this.choices.containerOuter;
this.choices._onClick({
@ -346,6 +348,7 @@ describe('Choices', function() {
});
it('should filter choices when searching', function() {
this.choices = new Choices(this.input);
this.choices.input.focus();
this.choices.input.value = 'Value 3';
@ -360,6 +363,32 @@ describe('Choices', function() {
expect(this.choices.isSearching && mostAccurateResult.value === 'Value 3').toBeTruthy;
});
it('shouldn\'t sort choices if shouldSort is false', function() {
this.choices = new Choices(this.input, {
shouldSort: false,
choices: [
{value: 'Value 5', label: 'Label Five'},
{value: 'Value 6', label: 'Label Six'},
{value: 'Value 7', label: 'Label Seven'},
],
});
expect(this.choices.currentState.choices[0].value).toEqual('Value 5');
});
it('should sort choices if shouldSort is false', function() {
this.choices = new Choices(this.input, {
shouldSort: true,
choices: [
{value: 'Value 5', label: 'Label Five'},
{value: 'Value 6', label: 'Label Six'},
{value: 'Value 7', label: 'Label Seven'},
],
});
expect(this.choices.currentState.choices[0].value).toEqual('Value 1');
});
});
describe('should accept multiple select inputs', function() {