From ca86265731660e4005b6fc97d546ae6fd577fb61 Mon Sep 17 00:00:00 2001 From: Josh Johnson Date: Sun, 4 Sep 2016 13:44:31 +0100 Subject: [PATCH] Better sort handling + sort tests --- assets/scripts/src/choices.js | 147 ++++++++++++++++++---------------- index.html | 10 +-- tests/spec/choices_spec.js | 41 ++++++++-- 3 files changed, 119 insertions(+), 79 deletions(-) diff --git a/assets/scripts/src/choices.js b/assets/scripts/src/choices.js index 23655ea..39dab3d 100644 --- a/assets/scripts/src/choices.js +++ b/assets/scripts/src/choices.js @@ -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); }); diff --git a/index.html b/index.html index d2b3066..447942d 100644 --- a/index.html +++ b/index.html @@ -195,13 +195,13 @@

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'

- - + - @@ -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(); diff --git a/tests/spec/choices_spec.js b/tests/spec/choices_spec.js index 0a4b247..953a6b4 100644 --- a/tests/spec/choices_spec.js +++ b/tests/spec/choices_spec.js @@ -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() {