Begin to move passedElement logic into wrapped component

This commit is contained in:
Josh Johnson 2017-10-13 13:43:58 +01:00
parent 82cb17ec2b
commit bd77f458b3
11 changed files with 222 additions and 93 deletions

View file

@ -4,6 +4,8 @@ import Dropdown from './components/dropdown';
import Container from './components/container'; import Container from './components/container';
import Input from './components/input'; import Input from './components/input';
import List from './components/list'; 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 { DEFAULT_CONFIG, DEFAULT_CLASSNAMES, EVENTS, KEY_CODES } from './constants';
import { TEMPLATES } from './templates'; import { TEMPLATES } from './templates';
import { addChoice, filterChoices, activateChoices, clearChoices } from './actions/choices'; import { addChoice, filterChoices, activateChoices, clearChoices } from './actions/choices';
@ -71,8 +73,19 @@ class Choices {
this.currentValue = ''; this.currentValue = '';
// Retrieve triggering element (i.e. element with 'data-choice' trigger) // Retrieve triggering element (i.e. element with 'data-choice' trigger)
this.element = element; const passedElement = isType('String', element) ? document.querySelector(element) : element;
this.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.passedElement) {
if (!this.config.silent) { if (!this.config.silent) {
@ -81,11 +94,6 @@ class Choices {
return false; 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.isIe11 = !!(navigator.userAgent.match(/Trident/) && navigator.userAgent.match(/rv[ :]11/));
this.isScrollingOnIe = false; this.isScrollingOnIe = false;
@ -103,7 +111,7 @@ class Choices {
this.placeholder = false; this.placeholder = false;
if (!this.isSelectOneElement) { if (!this.isSelectOneElement) {
this.placeholder = this.config.placeholder ? this.placeholder = this.config.placeholder ?
(this.config.placeholderValue || this.passedElement.getAttribute('placeholder')) : (this.config.placeholderValue || this.passedElement.element.getAttribute('placeholder')) :
false; false;
} }
@ -113,14 +121,14 @@ class Choices {
this.presetItems = this.config.items; this.presetItems = this.config.items;
// Then add any values passed from attribute // Then add any values passed from attribute
if (this.passedElement.value) { if (this.passedElement.element.value) {
this.presetItems = this.presetItems.concat( this.presetItems = this.presetItems.concat(
this.passedElement.value.split(this.config.delimiter), this.passedElement.element.value.split(this.config.delimiter),
); );
} }
// Set unique base Id // Set unique base Id
this.baseId = generateId(this.passedElement, 'choices-'); this.baseId = generateId(this.passedElement.element, 'choices-');
this.idNames = { this.idNames = {
itemChoice: 'item-choice', itemChoice: 'item-choice',
@ -149,11 +157,11 @@ class Choices {
console.error('Choices: Your browser doesn\'t support 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 (canInit) {
// If element has already been initialised with Choices // 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; return false;
} }
@ -210,31 +218,11 @@ class Choices {
// Remove all event listeners // Remove all event listeners
this._removeEventListeners(); this._removeEventListeners();
this.passedElement.reveal();
// 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;
// Move passed element back to original position // Move passed element back to original position
this.containerOuter.element.parentNode.insertBefore( this.containerOuter.element.parentNode.insertBefore(
this.passedElement, this.passedElement.element,
this.containerOuter.element, this.containerOuter.element,
); );
@ -372,8 +360,8 @@ class Choices {
const itemsFiltered = this.store.getItemsReducedToValues(items); const itemsFiltered = this.store.getItemsReducedToValues(items);
const itemsFilteredString = itemsFiltered.join(this.config.delimiter); const itemsFilteredString = itemsFiltered.join(this.config.delimiter);
// Update the value of the hidden input // Update the value of the hidden input
this.passedElement.setAttribute('value', itemsFilteredString); this.passedElement.element.setAttribute('value', itemsFilteredString);
this.passedElement.value = itemsFilteredString; this.passedElement.element.value = itemsFilteredString;
} else { } else {
const selectedOptionsFragment = document.createDocumentFragment(); const selectedOptionsFragment = document.createDocumentFragment();
const addOptionToFragment = (item) => { const addOptionToFragment = (item) => {
@ -387,8 +375,8 @@ class Choices {
items.forEach(item => addOptionToFragment(item)); items.forEach(item => addOptionToFragment(item));
// Update selected choices // Update selected choices
this.passedElement.innerHTML = ''; this.passedElement.element.innerHTML = '';
this.passedElement.appendChild(selectedOptionsFragment); this.passedElement.element.appendChild(selectedOptionsFragment);
} }
const addItemToFragment = (item) => { const addItemToFragment = (item) => {
@ -542,7 +530,7 @@ class Choices {
eventResponse.groupValue = group.value; eventResponse.groupValue = group.value;
} }
triggerEvent(this.passedElement, EVENTS.highlightItem, eventResponse); triggerEvent(this.passedElement.element, EVENTS.highlightItem, eventResponse);
} }
return this; return this;
@ -576,7 +564,7 @@ class Choices {
highlightItem(id, false), highlightItem(id, false),
); );
triggerEvent(this.passedElement, EVENTS.highlightItem, eventResponse); triggerEvent(this.passedElement.element, EVENTS.highlightItem, eventResponse);
return this; return this;
} }
@ -681,7 +669,7 @@ class Choices {
this.dropdown.show(); this.dropdown.show();
this.input.activate(focusInput); this.input.activate(focusInput);
triggerEvent(this.passedElement, EVENTS.showDropdown, {}); triggerEvent(this.passedElement.element, EVENTS.showDropdown, {});
return this; return this;
} }
@ -699,7 +687,7 @@ class Choices {
this.dropdown.hide(); this.dropdown.hide();
this.input.deactivate(blurInput); this.input.deactivate(blurInput);
triggerEvent(this.passedElement, EVENTS.hideDropdown, {}); triggerEvent(this.passedElement.element, EVENTS.hideDropdown, {});
return this; return this;
} }
@ -934,11 +922,11 @@ class Choices {
return this; return this;
} }
this.passedElement.disabled = false; this.passedElement.element.disabled = false;
if (this.containerOuter.isDisabled) { if (this.containerOuter.isDisabled) {
this._addEventListeners(); this._addEventListeners();
this.passedElement.removeAttribute('disabled'); this.passedElement.element.removeAttribute('disabled');
this.input.enable(); this.input.enable();
this.containerOuter.enable(); this.containerOuter.enable();
} }
@ -956,11 +944,11 @@ class Choices {
return this; return this;
} }
this.passedElement.disabled = true; this.passedElement.element.disabled = true;
if (!this.containerOuter.isDisabled) { if (!this.containerOuter.isDisabled) {
this._removeEventListeners(); this._removeEventListeners();
this.passedElement.setAttribute('disabled', ''); this.passedElement.element.setAttribute('disabled', '');
this.input.disable(); this.input.disable();
this.containerOuter.disable(); this.containerOuter.disable();
} }
@ -1004,7 +992,7 @@ class Choices {
return; return;
} }
triggerEvent(this.passedElement, EVENTS.change, { triggerEvent(this.passedElement.element, EVENTS.change, {
value, value,
}); });
} }
@ -1113,7 +1101,7 @@ class Choices {
// Update choice keyCode // Update choice keyCode
choice.keyCode = passedKeyCode; choice.keyCode = passedKeyCode;
triggerEvent(this.passedElement, EVENTS.choice, { triggerEvent(this.passedElement.element, EVENTS.choice, {
choice, choice,
}); });
@ -1362,7 +1350,7 @@ class Choices {
if (value && value.length >= this.config.searchFloor) { if (value && value.length >= this.config.searchFloor) {
const resultCount = this.config.searchChoices ? this._searchChoices(value) : 0; const resultCount = this.config.searchChoices ? this._searchChoices(value) : 0;
// Trigger search event // Trigger search event
triggerEvent(this.passedElement, EVENTS.search, { triggerEvent(this.passedElement.element, EVENTS.search, {
value, value,
resultCount, 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 { } else {
// On IE11, clicking the scollbar blurs our input and thus // On IE11, clicking the scollbar blurs our input and thus
// closes the dropdown. To stop this, we refocus our input // closes the dropdown. To stop this, we refocus our input
@ -2056,7 +2044,7 @@ class Choices {
// Trigger change event // Trigger change event
if (group && group.value) { if (group && group.value) {
triggerEvent(this.passedElement, EVENTS.addItem, { triggerEvent(this.passedElement.element, EVENTS.addItem, {
id, id,
value: passedValue, value: passedValue,
label: passedLabel, label: passedLabel,
@ -2064,7 +2052,7 @@ class Choices {
keyCode: passedKeyCode, keyCode: passedKeyCode,
}); });
} else { } else {
triggerEvent(this.passedElement, EVENTS.addItem, { triggerEvent(this.passedElement.element, EVENTS.addItem, {
id, id,
value: passedValue, value: passedValue,
label: passedLabel, label: passedLabel,
@ -2098,14 +2086,14 @@ class Choices {
); );
if (group && group.value) { if (group && group.value) {
triggerEvent(this.passedElement, EVENTS.removeItem, { triggerEvent(this.passedElement.element, EVENTS.removeItem, {
id, id,
value, value,
label, label,
groupValue: group.value, groupValue: group.value,
}); });
} else { } else {
triggerEvent(this.passedElement, EVENTS.removeItem, { triggerEvent(this.passedElement.element, EVENTS.removeItem, {
id, id,
value, value,
label, label,
@ -2272,7 +2260,7 @@ class Choices {
* @private * @private
*/ */
_createInput() { _createInput() {
const direction = this.passedElement.getAttribute('dir') || 'ltr'; const direction = this.passedElement.element.getAttribute('dir') || 'ltr';
const containerOuter = this._getTemplate('containerOuter', direction); const containerOuter = this._getTemplate('containerOuter', direction);
const containerInner = this._getTemplate('containerInner'); const containerInner = this._getTemplate('containerInner');
const itemList = this._getTemplate('itemList'); const itemList = this._getTemplate('itemList');
@ -2287,28 +2275,10 @@ class Choices {
this.itemList = new List(this, itemList, this.config.classNames); this.itemList = new List(this, itemList, this.config.classNames);
this.dropdown = new Dropdown(this, dropdown, this.config.classNames); this.dropdown = new Dropdown(this, dropdown, this.config.classNames);
// Hide passed input this.passedElement.conceal();
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');
// Wrap input in container preserving DOM ordering // 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 // Wrapper inner container with outer container
wrap(this.containerInner.element, this.containerOuter.element); wrap(this.containerInner.element, this.containerOuter.element);
@ -2339,14 +2309,14 @@ class Choices {
} }
if (this.isSelectElement) { if (this.isSelectElement) {
const passedGroups = Array.from(this.passedElement.getElementsByTagName('OPTGROUP')); const passedGroups = Array.from(this.passedElement.element.getElementsByTagName('OPTGROUP'));
this.highlightPosition = 0; this.highlightPosition = 0;
this.isSearching = false; this.isSearching = false;
if (passedGroups && passedGroups.length) { if (passedGroups && passedGroups.length) {
// If we have a placeholder option // 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') { if (placeholderChoice && placeholderChoice.parentNode.tagName === 'SELECT') {
this._addChoice( this._addChoice(
placeholderChoice.value, placeholderChoice.value,
@ -2363,7 +2333,7 @@ class Choices {
this._addGroup(group, (group.id || null)); this._addGroup(group, (group.id || null));
}); });
} else { } else {
const passedOptions = Array.from(this.passedElement.options); const passedOptions = Array.from(this.passedElement.element.options);
const filter = this.config.sortFilter; const filter = this.config.sortFilter;
const allChoices = this.presetChoices; const allChoices = this.presetChoices;

View file

@ -3,7 +3,7 @@ import 'whatwg-fetch';
import 'es6-promise'; import 'es6-promise';
import 'core-js/fn/object/assign'; import 'core-js/fn/object/assign';
import 'custom-event-autopolyfill'; import 'custom-event-autopolyfill';
import { expect, assert } from 'chai'; import { expect } from 'chai';
import sinon from 'sinon'; import sinon from 'sinon';
import Choices from './choices'; import Choices from './choices';
@ -11,9 +11,11 @@ import Dropdown from './components/dropdown';
import Container from './components/container'; import Container from './components/container';
import Input from './components/input'; import Input from './components/input';
import List from './components/list'; import List from './components/list';
import WrappedInput from './components/wrapped-input';
import WrappedSelect from './components/wrapped-select';
describe('Choices', () => { describe('Choices', () => {
describe('should initialize Choices', () => { describe('initialize Choices', () => {
let input; let input;
let instance; let instance;
@ -39,7 +41,7 @@ describe('Choices', () => {
}); });
it('should not re-initialise if passed element again', () => { 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'); sinon.spy(reinitialise, '_createTemplates');
expect(reinitialise._createTemplates.callCount).to.equal(0); expect(reinitialise._createTemplates.callCount).to.equal(0);
}); });
@ -117,7 +119,7 @@ describe('Choices', () => {
}); });
it('should hide passed input', () => { 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', () => { it('should create an outer container', () => {
@ -180,6 +182,11 @@ describe('Choices', () => {
instance.destroy(); 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', () => { it('should accept a user inputted value', () => {
instance = new Choices(input); instance = new Choices(input);
@ -295,6 +302,11 @@ describe('Choices', () => {
instance.destroy(); 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', () => { it('should open the choice list on focusing', () => {
instance = new Choices(input); instance = new Choices(input);
instance.input.element.focus(); instance.input.element.focus();
@ -355,8 +367,8 @@ describe('Choices', () => {
const addSpyStub = sinon.stub(); const addSpyStub = sinon.stub();
const passedElement = instance.passedElement; const passedElement = instance.passedElement;
passedElement.addEventListener('change', onChangeStub); passedElement.element.addEventListener('change', onChangeStub);
passedElement.addEventListener('addItem', addSpyStub); passedElement.element.addEventListener('addItem', addSpyStub);
instance.input.element.focus(); instance.input.element.focus();
@ -421,7 +433,7 @@ describe('Choices', () => {
const showDropdownStub = sinon.spy(); const showDropdownStub = sinon.spy();
const passedElement = instance.passedElement; const passedElement = instance.passedElement;
passedElement.addEventListener('showDropdown', showDropdownStub); passedElement.element.addEventListener('showDropdown', showDropdownStub);
instance.input.focus(); instance.input.focus();
@ -441,7 +453,7 @@ describe('Choices', () => {
const hideDropdownStub = sinon.stub(); const hideDropdownStub = sinon.stub();
const passedElement = instance.passedElement; const passedElement = instance.passedElement;
passedElement.addEventListener('hideDropdown', hideDropdownStub); passedElement.element.addEventListener('hideDropdown', hideDropdownStub);
instance.input.element.focus(); instance.input.element.focus();
@ -466,7 +478,7 @@ describe('Choices', () => {
const onSearchStub = sinon.spy(); const onSearchStub = sinon.spy();
const passedElement = instance.passedElement; const passedElement = instance.passedElement;
passedElement.addEventListener('search', onSearchStub); passedElement.element.addEventListener('search', onSearchStub);
instance.input.element.focus(); instance.input.element.focus();
instance.input.element.value = '3 '; instance.input.element.value = '3 ';
@ -494,7 +506,7 @@ describe('Choices', () => {
const onSearchStub = sinon.spy(); const onSearchStub = sinon.spy();
const passedElement = instance.passedElement; const passedElement = instance.passedElement;
passedElement.addEventListener('search', onSearchStub); passedElement.element.addEventListener('search', onSearchStub);
instance.input.element.focus(); instance.input.element.focus();
instance.input.element.value = 'Javascript'; instance.input.element.value = 'Javascript';
@ -601,6 +613,10 @@ describe('Choices', () => {
instance.destroy(); instance.destroy();
}); });
it('should wrap passed input', () => {
expect(instance.passedElement).to.be.an.instanceof(WrappedSelect);
});
it('should add any pre-defined values', () => { it('should add any pre-defined values', () => {
expect(instance.currentState.items.length).to.be.above(1); expect(instance.currentState.items.length).to.be.above(1);
}); });

View file

@ -8,7 +8,6 @@ describe('Container', () => {
let choicesInstance; let choicesInstance;
let choicesElement; let choicesElement;
beforeEach(() => { beforeEach(() => {
choicesInstance = { choicesInstance = {
config: { config: {

View file

@ -8,7 +8,6 @@ describe('Input', () => {
let choicesInstance; let choicesInstance;
let choicesElement; let choicesElement;
beforeEach(() => { beforeEach(() => {
choicesInstance = { choicesInstance = {
config: { config: {

View file

@ -7,7 +7,6 @@ describe('List', () => {
let choicesInstance; let choicesInstance;
let choicesElement; let choicesElement;
beforeEach(() => { beforeEach(() => {
choicesInstance = { choicesInstance = {
config: { config: {

View file

@ -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;
}
}

View file

@ -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);
});
});

View file

@ -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();
}
}

View file

@ -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);
});
});

View file

@ -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();
}
}

View file

@ -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);
});
});