From bd77f458b323c956fafde7294b7d93af7b681d75 Mon Sep 17 00:00:00 2001 From: Josh Johnson Date: Fri, 13 Oct 2017 13:43:58 +0100 Subject: [PATCH] Begin to move passedElement logic into wrapped component --- src/scripts/src/choices.js | 130 +++++++----------- src/scripts/src/choices.test.js | 36 +++-- src/scripts/src/components/container.test.js | 1 - src/scripts/src/components/input.test.js | 1 - src/scripts/src/components/list.test.js | 1 - src/scripts/src/components/wrapped-element.js | 53 +++++++ .../src/components/wrapped-element.test.js | 19 +++ src/scripts/src/components/wrapped-input.js | 18 +++ .../src/components/wrapped-input.test.js | 19 +++ src/scripts/src/components/wrapped-select.js | 18 +++ .../src/components/wrapped-select.test.js | 19 +++ 11 files changed, 222 insertions(+), 93 deletions(-) create mode 100644 src/scripts/src/components/wrapped-element.js create mode 100644 src/scripts/src/components/wrapped-element.test.js create mode 100644 src/scripts/src/components/wrapped-input.js create mode 100644 src/scripts/src/components/wrapped-input.test.js create mode 100644 src/scripts/src/components/wrapped-select.js create mode 100644 src/scripts/src/components/wrapped-select.test.js diff --git a/src/scripts/src/choices.js b/src/scripts/src/choices.js index a980aa0..81f2c78 100644 --- a/src/scripts/src/choices.js +++ b/src/scripts/src/choices.js @@ -4,6 +4,8 @@ import Dropdown from './components/dropdown'; import Container from './components/container'; import Input from './components/input'; import List from './components/list'; +import WrappedInput from './components/wrapped-input'; +import WrappedSelect from './components/wrapped-select'; import { DEFAULT_CONFIG, DEFAULT_CLASSNAMES, EVENTS, KEY_CODES } from './constants'; import { TEMPLATES } from './templates'; import { addChoice, filterChoices, activateChoices, clearChoices } from './actions/choices'; @@ -71,8 +73,19 @@ class Choices { this.currentValue = ''; // Retrieve triggering element (i.e. element with 'data-choice' trigger) - this.element = element; - this.passedElement = isType('String', element) ? document.querySelector(element) : element; + const passedElement = isType('String', element) ? document.querySelector(element) : element; + + this.isTextElement = passedElement.type === 'text'; + this.isSelectOneElement = passedElement.type === 'select-one'; + this.isSelectMultipleElement = passedElement.type === 'select-multiple'; + this.isSelectElement = this.isSelectOneElement || this.isSelectMultipleElement; + this.isValidElementType = this.isTextElement || this.isSelectElement; + + if (this.isTextElement) { + this.passedElement = new WrappedInput(this, passedElement, this.config.classNames); + } else if (this.isSelectElement) { + this.passedElement = new WrappedSelect(this, passedElement, this.config.classNames); + } if (!this.passedElement) { if (!this.config.silent) { @@ -81,11 +94,6 @@ class Choices { return false; } - this.isTextElement = this.passedElement.type === 'text'; - this.isSelectOneElement = this.passedElement.type === 'select-one'; - this.isSelectMultipleElement = this.passedElement.type === 'select-multiple'; - this.isSelectElement = this.isSelectOneElement || this.isSelectMultipleElement; - this.isValidElementType = this.isTextElement || this.isSelectElement; this.isIe11 = !!(navigator.userAgent.match(/Trident/) && navigator.userAgent.match(/rv[ :]11/)); this.isScrollingOnIe = false; @@ -103,7 +111,7 @@ class Choices { this.placeholder = false; if (!this.isSelectOneElement) { this.placeholder = this.config.placeholder ? - (this.config.placeholderValue || this.passedElement.getAttribute('placeholder')) : + (this.config.placeholderValue || this.passedElement.element.getAttribute('placeholder')) : false; } @@ -113,14 +121,14 @@ class Choices { this.presetItems = this.config.items; // Then add any values passed from attribute - if (this.passedElement.value) { + if (this.passedElement.element.value) { this.presetItems = this.presetItems.concat( - this.passedElement.value.split(this.config.delimiter), + this.passedElement.element.value.split(this.config.delimiter), ); } // Set unique base Id - this.baseId = generateId(this.passedElement, 'choices-'); + this.baseId = generateId(this.passedElement.element, 'choices-'); this.idNames = { itemChoice: 'item-choice', @@ -149,11 +157,11 @@ class Choices { console.error('Choices: Your browser doesn\'t support Choices'); } - const canInit = isElement(this.passedElement) && this.isValidElementType; + const canInit = isElement(this.passedElement.element) && this.isValidElementType; if (canInit) { // If element has already been initialised with Choices - if (this.passedElement.getAttribute('data-choice') === 'active') { + if (this.passedElement.element.getAttribute('data-choice') === 'active') { return false; } @@ -210,31 +218,11 @@ class Choices { // Remove all event listeners this._removeEventListeners(); - - // Reinstate passed element - this.passedElement.classList.remove( - this.config.classNames.input, - this.config.classNames.hiddenState, - ); - this.passedElement.removeAttribute('tabindex'); - - // Recover original styles if any - const origStyle = this.passedElement.getAttribute('data-choice-orig-style'); - if (origStyle) { - this.passedElement.removeAttribute('data-choice-orig-style'); - this.passedElement.setAttribute('style', origStyle); - } else { - this.passedElement.removeAttribute('style'); - } - this.passedElement.removeAttribute('aria-hidden'); - this.passedElement.removeAttribute('data-choice'); - - // Re-assign values - this is weird, I know - this.passedElement.value = this.passedElement.value; + this.passedElement.reveal(); // Move passed element back to original position this.containerOuter.element.parentNode.insertBefore( - this.passedElement, + this.passedElement.element, this.containerOuter.element, ); @@ -372,8 +360,8 @@ class Choices { const itemsFiltered = this.store.getItemsReducedToValues(items); const itemsFilteredString = itemsFiltered.join(this.config.delimiter); // Update the value of the hidden input - this.passedElement.setAttribute('value', itemsFilteredString); - this.passedElement.value = itemsFilteredString; + this.passedElement.element.setAttribute('value', itemsFilteredString); + this.passedElement.element.value = itemsFilteredString; } else { const selectedOptionsFragment = document.createDocumentFragment(); const addOptionToFragment = (item) => { @@ -387,8 +375,8 @@ class Choices { items.forEach(item => addOptionToFragment(item)); // Update selected choices - this.passedElement.innerHTML = ''; - this.passedElement.appendChild(selectedOptionsFragment); + this.passedElement.element.innerHTML = ''; + this.passedElement.element.appendChild(selectedOptionsFragment); } const addItemToFragment = (item) => { @@ -542,7 +530,7 @@ class Choices { eventResponse.groupValue = group.value; } - triggerEvent(this.passedElement, EVENTS.highlightItem, eventResponse); + triggerEvent(this.passedElement.element, EVENTS.highlightItem, eventResponse); } return this; @@ -576,7 +564,7 @@ class Choices { highlightItem(id, false), ); - triggerEvent(this.passedElement, EVENTS.highlightItem, eventResponse); + triggerEvent(this.passedElement.element, EVENTS.highlightItem, eventResponse); return this; } @@ -681,7 +669,7 @@ class Choices { this.dropdown.show(); this.input.activate(focusInput); - triggerEvent(this.passedElement, EVENTS.showDropdown, {}); + triggerEvent(this.passedElement.element, EVENTS.showDropdown, {}); return this; } @@ -699,7 +687,7 @@ class Choices { this.dropdown.hide(); this.input.deactivate(blurInput); - triggerEvent(this.passedElement, EVENTS.hideDropdown, {}); + triggerEvent(this.passedElement.element, EVENTS.hideDropdown, {}); return this; } @@ -934,11 +922,11 @@ class Choices { return this; } - this.passedElement.disabled = false; + this.passedElement.element.disabled = false; if (this.containerOuter.isDisabled) { this._addEventListeners(); - this.passedElement.removeAttribute('disabled'); + this.passedElement.element.removeAttribute('disabled'); this.input.enable(); this.containerOuter.enable(); } @@ -956,11 +944,11 @@ class Choices { return this; } - this.passedElement.disabled = true; + this.passedElement.element.disabled = true; if (!this.containerOuter.isDisabled) { this._removeEventListeners(); - this.passedElement.setAttribute('disabled', ''); + this.passedElement.element.setAttribute('disabled', ''); this.input.disable(); this.containerOuter.disable(); } @@ -1004,7 +992,7 @@ class Choices { return; } - triggerEvent(this.passedElement, EVENTS.change, { + triggerEvent(this.passedElement.element, EVENTS.change, { value, }); } @@ -1113,7 +1101,7 @@ class Choices { // Update choice keyCode choice.keyCode = passedKeyCode; - triggerEvent(this.passedElement, EVENTS.choice, { + triggerEvent(this.passedElement.element, EVENTS.choice, { choice, }); @@ -1362,7 +1350,7 @@ class Choices { if (value && value.length >= this.config.searchFloor) { const resultCount = this.config.searchChoices ? this._searchChoices(value) : 0; // Trigger search event - triggerEvent(this.passedElement, EVENTS.search, { + triggerEvent(this.passedElement.element, EVENTS.search, { value, resultCount, }); @@ -1819,7 +1807,7 @@ class Choices { }, }; - focusActions[this.passedElement.type](); + focusActions[this.passedElement.element.type](); } } @@ -1873,7 +1861,7 @@ class Choices { }, }; - blurActions[this.passedElement.type](); + blurActions[this.passedElement.element.type](); } else { // On IE11, clicking the scollbar blurs our input and thus // closes the dropdown. To stop this, we refocus our input @@ -2056,7 +2044,7 @@ class Choices { // Trigger change event if (group && group.value) { - triggerEvent(this.passedElement, EVENTS.addItem, { + triggerEvent(this.passedElement.element, EVENTS.addItem, { id, value: passedValue, label: passedLabel, @@ -2064,7 +2052,7 @@ class Choices { keyCode: passedKeyCode, }); } else { - triggerEvent(this.passedElement, EVENTS.addItem, { + triggerEvent(this.passedElement.element, EVENTS.addItem, { id, value: passedValue, label: passedLabel, @@ -2098,14 +2086,14 @@ class Choices { ); if (group && group.value) { - triggerEvent(this.passedElement, EVENTS.removeItem, { + triggerEvent(this.passedElement.element, EVENTS.removeItem, { id, value, label, groupValue: group.value, }); } else { - triggerEvent(this.passedElement, EVENTS.removeItem, { + triggerEvent(this.passedElement.element, EVENTS.removeItem, { id, value, label, @@ -2272,7 +2260,7 @@ class Choices { * @private */ _createInput() { - const direction = this.passedElement.getAttribute('dir') || 'ltr'; + const direction = this.passedElement.element.getAttribute('dir') || 'ltr'; const containerOuter = this._getTemplate('containerOuter', direction); const containerInner = this._getTemplate('containerInner'); const itemList = this._getTemplate('itemList'); @@ -2287,28 +2275,10 @@ class Choices { this.itemList = new List(this, itemList, this.config.classNames); this.dropdown = new Dropdown(this, dropdown, this.config.classNames); - // Hide passed input - this.passedElement.classList.add( - this.config.classNames.input, - this.config.classNames.hiddenState, - ); - - // Remove element from tab index - this.passedElement.tabIndex = '-1'; - - // Backup original styles if any - const origStyle = this.passedElement.getAttribute('style'); - - if (origStyle) { - this.passedElement.setAttribute('data-choice-orig-style', origStyle); - } - - this.passedElement.setAttribute('style', 'display:none;'); - this.passedElement.setAttribute('aria-hidden', 'true'); - this.passedElement.setAttribute('data-choice', 'active'); + this.passedElement.conceal(); // Wrap input in container preserving DOM ordering - wrap(this.passedElement, this.containerInner.element); + wrap(this.passedElement.element, this.containerInner.element); // Wrapper inner container with outer container wrap(this.containerInner.element, this.containerOuter.element); @@ -2339,14 +2309,14 @@ class Choices { } if (this.isSelectElement) { - const passedGroups = Array.from(this.passedElement.getElementsByTagName('OPTGROUP')); + const passedGroups = Array.from(this.passedElement.element.getElementsByTagName('OPTGROUP')); this.highlightPosition = 0; this.isSearching = false; if (passedGroups && passedGroups.length) { // If we have a placeholder option - const placeholderChoice = this.passedElement.querySelector('option[placeholder]'); + const placeholderChoice = this.passedElement.element.querySelector('option[placeholder]'); if (placeholderChoice && placeholderChoice.parentNode.tagName === 'SELECT') { this._addChoice( placeholderChoice.value, @@ -2363,7 +2333,7 @@ class Choices { this._addGroup(group, (group.id || null)); }); } else { - const passedOptions = Array.from(this.passedElement.options); + const passedOptions = Array.from(this.passedElement.element.options); const filter = this.config.sortFilter; const allChoices = this.presetChoices; diff --git a/src/scripts/src/choices.test.js b/src/scripts/src/choices.test.js index 643df36..fdcd03d 100644 --- a/src/scripts/src/choices.test.js +++ b/src/scripts/src/choices.test.js @@ -3,7 +3,7 @@ import 'whatwg-fetch'; import 'es6-promise'; import 'core-js/fn/object/assign'; import 'custom-event-autopolyfill'; -import { expect, assert } from 'chai'; +import { expect } from 'chai'; import sinon from 'sinon'; import Choices from './choices'; @@ -11,9 +11,11 @@ import Dropdown from './components/dropdown'; import Container from './components/container'; import Input from './components/input'; import List from './components/list'; +import WrappedInput from './components/wrapped-input'; +import WrappedSelect from './components/wrapped-select'; describe('Choices', () => { - describe('should initialize Choices', () => { + describe('initialize Choices', () => { let input; let instance; @@ -39,7 +41,7 @@ describe('Choices', () => { }); it('should not re-initialise if passed element again', () => { - const reinitialise = new Choices(instance.passedElement); + const reinitialise = new Choices(instance.passedElement.element); sinon.spy(reinitialise, '_createTemplates'); expect(reinitialise._createTemplates.callCount).to.equal(0); }); @@ -117,7 +119,7 @@ describe('Choices', () => { }); it('should hide passed input', () => { - expect(instance.passedElement.style.display).to.equal('none'); + expect(instance.passedElement.element.style.display).to.equal('none'); }); it('should create an outer container', () => { @@ -180,6 +182,11 @@ describe('Choices', () => { instance.destroy(); }); + it('should wrap passed input', () => { + instance = new Choices(input); + expect(instance.passedElement).to.be.an.instanceof(WrappedInput); + }); + it('should accept a user inputted value', () => { instance = new Choices(input); @@ -295,6 +302,11 @@ describe('Choices', () => { instance.destroy(); }); + it('should wrap passed input', () => { + instance = new Choices(input); + expect(instance.passedElement).to.be.an.instanceof(WrappedSelect); + }); + it('should open the choice list on focusing', () => { instance = new Choices(input); instance.input.element.focus(); @@ -355,8 +367,8 @@ describe('Choices', () => { const addSpyStub = sinon.stub(); const passedElement = instance.passedElement; - passedElement.addEventListener('change', onChangeStub); - passedElement.addEventListener('addItem', addSpyStub); + passedElement.element.addEventListener('change', onChangeStub); + passedElement.element.addEventListener('addItem', addSpyStub); instance.input.element.focus(); @@ -421,7 +433,7 @@ describe('Choices', () => { const showDropdownStub = sinon.spy(); const passedElement = instance.passedElement; - passedElement.addEventListener('showDropdown', showDropdownStub); + passedElement.element.addEventListener('showDropdown', showDropdownStub); instance.input.focus(); @@ -441,7 +453,7 @@ describe('Choices', () => { const hideDropdownStub = sinon.stub(); const passedElement = instance.passedElement; - passedElement.addEventListener('hideDropdown', hideDropdownStub); + passedElement.element.addEventListener('hideDropdown', hideDropdownStub); instance.input.element.focus(); @@ -466,7 +478,7 @@ describe('Choices', () => { const onSearchStub = sinon.spy(); const passedElement = instance.passedElement; - passedElement.addEventListener('search', onSearchStub); + passedElement.element.addEventListener('search', onSearchStub); instance.input.element.focus(); instance.input.element.value = '3 '; @@ -494,7 +506,7 @@ describe('Choices', () => { const onSearchStub = sinon.spy(); const passedElement = instance.passedElement; - passedElement.addEventListener('search', onSearchStub); + passedElement.element.addEventListener('search', onSearchStub); instance.input.element.focus(); instance.input.element.value = 'Javascript'; @@ -601,6 +613,10 @@ describe('Choices', () => { instance.destroy(); }); + it('should wrap passed input', () => { + expect(instance.passedElement).to.be.an.instanceof(WrappedSelect); + }); + it('should add any pre-defined values', () => { expect(instance.currentState.items.length).to.be.above(1); }); diff --git a/src/scripts/src/components/container.test.js b/src/scripts/src/components/container.test.js index a57821c..921f28b 100644 --- a/src/scripts/src/components/container.test.js +++ b/src/scripts/src/components/container.test.js @@ -8,7 +8,6 @@ describe('Container', () => { let choicesInstance; let choicesElement; - beforeEach(() => { choicesInstance = { config: { diff --git a/src/scripts/src/components/input.test.js b/src/scripts/src/components/input.test.js index 9ebfc37..7187320 100644 --- a/src/scripts/src/components/input.test.js +++ b/src/scripts/src/components/input.test.js @@ -8,7 +8,6 @@ describe('Input', () => { let choicesInstance; let choicesElement; - beforeEach(() => { choicesInstance = { config: { diff --git a/src/scripts/src/components/list.test.js b/src/scripts/src/components/list.test.js index 7d582bb..cf481b3 100644 --- a/src/scripts/src/components/list.test.js +++ b/src/scripts/src/components/list.test.js @@ -7,7 +7,6 @@ describe('List', () => { let choicesInstance; let choicesElement; - beforeEach(() => { choicesInstance = { config: { diff --git a/src/scripts/src/components/wrapped-element.js b/src/scripts/src/components/wrapped-element.js new file mode 100644 index 0000000..f5bfa3d --- /dev/null +++ b/src/scripts/src/components/wrapped-element.js @@ -0,0 +1,53 @@ +export default class WrappedElement { + constructor(instance, element, classNames) { + this.parentInstance = instance; + this.element = element; + this.classNames = classNames; + } + + conceal() { + // Hide passed input + this.element.classList.add( + this.classNames.input, + this.classNames.hiddenState, + ); + + // Remove element from tab index + this.element.tabIndex = '-1'; + + // Backup original styles if any + const origStyle = this.element.getAttribute('style'); + + if (origStyle) { + this.element.setAttribute('data-choice-orig-style', origStyle); + } + + this.element.setAttribute('style', 'display:none;'); + this.element.setAttribute('aria-hidden', 'true'); + this.element.setAttribute('data-choice', 'active'); + } + + reveal() { + // Reinstate passed element + this.element.classList.remove( + this.classNames.input, + this.classNames.hiddenState, + ); + this.element.removeAttribute('tabindex'); + + // Recover original styles if any + const origStyle = this.element.getAttribute('data-choice-orig-style'); + + if (origStyle) { + this.element.removeAttribute('data-choice-orig-style'); + this.element.setAttribute('style', origStyle); + } else { + this.element.removeAttribute('style'); + } + this.element.removeAttribute('aria-hidden'); + this.element.removeAttribute('data-choice'); + + // Re-assign values - this is weird, I know + this.element.value = this.element.value; + } +} diff --git a/src/scripts/src/components/wrapped-element.test.js b/src/scripts/src/components/wrapped-element.test.js new file mode 100644 index 0000000..c941bba --- /dev/null +++ b/src/scripts/src/components/wrapped-element.test.js @@ -0,0 +1,19 @@ +import { expect } from 'chai'; +import WrappedElement from './wrapped-element'; +import { DEFAULT_CLASSNAMES, DEFAULT_CONFIG } from '../constants'; + +describe('WrappedElement', () => { + let instance; + let choicesInstance; + let choicesElement; + + beforeEach(() => { + choicesInstance = { + config: { + ...DEFAULT_CONFIG, + }, + }; + choicesElement = document.createElement('select'); + instance = new WrappedElement(choicesInstance, choicesElement, DEFAULT_CLASSNAMES); + }); +}); diff --git a/src/scripts/src/components/wrapped-input.js b/src/scripts/src/components/wrapped-input.js new file mode 100644 index 0000000..7254395 --- /dev/null +++ b/src/scripts/src/components/wrapped-input.js @@ -0,0 +1,18 @@ +import WrappedElement from './wrapped-element'; + +export default class WrappedInput extends WrappedElement { + constructor(instance, element, classNames) { + super(instance, element, classNames); + this.parentInstance = instance; + this.element = element; + this.classNames = classNames; + } + + conceal() { + super.conceal(); + } + + reveal() { + super.reveal(); + } +} diff --git a/src/scripts/src/components/wrapped-input.test.js b/src/scripts/src/components/wrapped-input.test.js new file mode 100644 index 0000000..ad5ba69 --- /dev/null +++ b/src/scripts/src/components/wrapped-input.test.js @@ -0,0 +1,19 @@ +import { expect } from 'chai'; +import WrappedInput from './wrapped-input'; +import { DEFAULT_CLASSNAMES, DEFAULT_CONFIG } from '../constants'; + +describe('WrappedInput', () => { + let instance; + let choicesInstance; + let choicesElement; + + beforeEach(() => { + choicesInstance = { + config: { + ...DEFAULT_CONFIG, + }, + }; + choicesElement = document.createElement('input'); + instance = new WrappedInput(choicesInstance, choicesElement, DEFAULT_CLASSNAMES); + }); +}); diff --git a/src/scripts/src/components/wrapped-select.js b/src/scripts/src/components/wrapped-select.js new file mode 100644 index 0000000..6fb4a71 --- /dev/null +++ b/src/scripts/src/components/wrapped-select.js @@ -0,0 +1,18 @@ +import WrappedElement from './wrapped-element'; + +export default class WrappedSelect extends WrappedElement { + constructor(instance, element, classNames) { + super(instance, element, classNames); + this.parentInstance = instance; + this.element = element; + this.classNames = classNames; + } + + conceal() { + super.conceal(); + } + + reveal() { + super.reveal(); + } +} diff --git a/src/scripts/src/components/wrapped-select.test.js b/src/scripts/src/components/wrapped-select.test.js new file mode 100644 index 0000000..df564e2 --- /dev/null +++ b/src/scripts/src/components/wrapped-select.test.js @@ -0,0 +1,19 @@ +import { expect } from 'chai'; +import WrappedSelect from './wrapped-select'; +import { DEFAULT_CLASSNAMES, DEFAULT_CONFIG } from '../constants'; + +describe('WrappedSelect', () => { + let instance; + let choicesInstance; + let choicesElement; + + beforeEach(() => { + choicesInstance = { + config: { + ...DEFAULT_CONFIG, + }, + }; + choicesElement = document.createElement('select'); + instance = new WrappedSelect(choicesInstance, choicesElement, DEFAULT_CLASSNAMES); + }); +});