Merge with latest + housekeeping

This commit is contained in:
Josh Johnson 2017-10-29 18:57:54 +00:00
commit affd67e542
12 changed files with 381 additions and 287 deletions

View file

@ -6,7 +6,7 @@ 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 { DEFAULT_CONFIG, DEFAULT_CLASSNAMES, EVENTS, KEY_CODES, SCROLLING_SPEED } from './constants';
import { TEMPLATES } from './templates';
import { addChoice, filterChoices, activateChoices, clearChoices } from './actions/choices';
import { addItem, removeItem, highlightItem } from './actions/items';
@ -24,7 +24,6 @@ import {
sortByAlpha,
sortByScore,
generateId,
triggerEvent,
findAncestorByAttrName,
regexFilter,
} from './lib/utils';
@ -121,9 +120,9 @@ class Choices {
this.presetItems = this.config.items;
// Then add any values passed from attribute
if (this.passedElement.element.value) {
if (this.passedElement.getValue()) {
this.presetItems = this.presetItems.concat(
this.passedElement.element.value.split(this.config.delimiter),
this.passedElement.getValue().split(this.config.delimiter),
);
}
@ -219,15 +218,7 @@ class Choices {
// Remove all event listeners
this._removeEventListeners();
this.passedElement.reveal();
// Move passed element back to original position
this.containerOuter.element.parentNode.insertBefore(
this.passedElement.element,
this.containerOuter.element,
);
// Remove added elements
this.containerOuter.element.parentNode.removeChild(this.containerOuter.element);
this.containerOuter.revert(this.passedElement.element);
// Clear data store
this.clearStore();
@ -292,7 +283,7 @@ class Choices {
(this.isSelectOneElement || !choice.selected) :
true;
if (shouldRender) {
const dropdownItem = this._getTemplate('choice', choice);
const dropdownItem = this._getTemplate('choice', choice, this.config.itemSelectText);
choicesFragment.appendChild(dropdownItem);
}
};
@ -359,9 +350,9 @@ class Choices {
// Simplify store data to just values
const itemsFiltered = this.store.getItemsReducedToValues(items);
const itemsFilteredString = itemsFiltered.join(this.config.delimiter);
// Update the value of the hidden input
this.passedElement.element.setAttribute('value', itemsFilteredString);
this.passedElement.element.value = itemsFilteredString;
this.passedElement.setValue(itemsFilteredString);
} else {
const selectedOptionsFragment = document.createDocumentFragment();
const addOptionToFragment = (item) => {
@ -373,15 +364,13 @@ class Choices {
// Add each list item to list
items.forEach(item => addOptionToFragment(item));
// Update selected choices
this.passedElement.element.innerHTML = '';
this.passedElement.element.appendChild(selectedOptionsFragment);
// Update the options of the hidden input
this.passedElement.setOptions(selectedOptionsFragment);
}
const addItemToFragment = (item) => {
// Create new list element
const listItem = this._getTemplate('item', item);
const listItem = this._getTemplate('item', item, this.config.removeItemButton);
// Append it to list
itemListFragment.appendChild(listItem);
};
@ -399,104 +388,104 @@ class Choices {
*/
render() {
this.currentState = this.store.getState();
const stateChanged = (
this.currentState.choices !== this.prevState.choices ||
this.currentState.groups !== this.prevState.groups ||
this.currentState.items !== this.prevState.items
);
// Only render if our state has actually changed
if (this.currentState !== this.prevState) {
// Choices
if (
(this.currentState.choices !== this.prevState.choices ||
this.currentState.groups !== this.prevState.groups ||
this.currentState.items !== this.prevState.items) &&
this.isSelectElement
) {
// Get active groups/choices
const activeGroups = this.store.getGroupsFilteredByActive();
const activeChoices = this.store.getChoicesFilteredByActive();
let choiceListFragment = document.createDocumentFragment();
// Clear choices
this.choiceList.clear();
// Scroll back to top of choices list
if (this.config.resetScrollPosition) {
this.choiceList.scrollTo(0);
}
// If we have grouped options
if (activeGroups.length >= 1 && this.isSearching !== true) {
// If we have a placeholder choice along with groups
const activePlaceholders = activeChoices.filter(
activeChoice => activeChoice.placeholder === true && activeChoice.groupId === -1,
);
if (activePlaceholders.length >= 1) {
choiceListFragment = this.renderChoices(activePlaceholders, choiceListFragment);
}
choiceListFragment = this.renderGroups(activeGroups, activeChoices, choiceListFragment);
} else if (activeChoices.length >= 1) {
choiceListFragment = this.renderChoices(activeChoices, choiceListFragment);
}
const activeItems = this.store.getItemsFilteredByActive();
const canAddItem = this._canAddItem(activeItems, this.input.getValue());
// If we have choices to show
if (choiceListFragment.childNodes && choiceListFragment.childNodes.length > 0) {
// ...and we can select them
if (canAddItem.response) {
// ...append them and highlight the first choice
this.choiceList.append(choiceListFragment);
this._highlightChoice();
} else {
// ...otherwise show a notice
this.choiceList.append(this._getTemplate('notice', canAddItem.notice));
}
} else {
// Otherwise show a notice
let dropdownItem;
let notice;
if (this.isSearching) {
notice = isType('Function', this.config.noResultsText) ?
this.config.noResultsText() :
this.config.noResultsText;
dropdownItem = this._getTemplate('notice', notice, 'no-results');
} else {
notice = isType('Function', this.config.noChoicesText) ?
this.config.noChoicesText() :
this.config.noChoicesText;
dropdownItem = this._getTemplate('notice', notice, 'no-choices');
}
this.choiceList.append(dropdownItem);
}
}
// Items
if (this.currentState.items !== this.prevState.items) {
// Get active items (items that can be selected)
const activeItems = this.store.getItemsFilteredByActive();
// Clear list
this.itemList.clear();
if (activeItems && activeItems.length) {
// Create a fragment to store our list items
// (so we don't have to update the DOM for each item)
const itemListFragment = this.renderItems(activeItems);
// If we have items to add
if (itemListFragment.childNodes) {
// Update list
this.itemList.append(itemListFragment);
}
}
}
this.prevState = this.currentState;
if (!stateChanged) {
return;
}
/* Choices */
if (this.isSelectElement) {
// Get active groups/choices
const activeGroups = this.store.getGroupsFilteredByActive();
const activeChoices = this.store.getChoicesFilteredByActive();
let choiceListFragment = document.createDocumentFragment();
// Clear choices
this.choiceList.clear();
// Scroll back to top of choices list
if (this.config.resetScrollPosition) {
this.choiceList.scrollTo(0);
}
// If we have grouped options
if (activeGroups.length >= 1 && !this.isSearching) {
// If we have a placeholder choice along with groups
const activePlaceholders = activeChoices.filter(
activeChoice => activeChoice.placeholder === true && activeChoice.groupId === -1,
);
if (activePlaceholders.length >= 1) {
choiceListFragment = this.renderChoices(activePlaceholders, choiceListFragment);
}
choiceListFragment = this.renderGroups(activeGroups, activeChoices, choiceListFragment);
} else if (activeChoices.length >= 1) {
choiceListFragment = this.renderChoices(activeChoices, choiceListFragment);
}
const activeItems = this.store.getItemsFilteredByActive();
const canAddItem = this._canAddItem(activeItems, this.input.getValue());
// If we have choices to show
if (choiceListFragment.childNodes && choiceListFragment.childNodes.length > 0) {
// ...and we can select them
if (canAddItem.response) {
// ...append them and highlight the first choice
this.choiceList.append(choiceListFragment);
this._highlightChoice();
} else {
// ...otherwise show a notice
this.choiceList.append(this._getTemplate('notice', canAddItem.notice));
}
} else {
// Otherwise show a notice
let dropdownItem;
let notice;
if (this.isSearching) {
notice = isType('Function', this.config.noResultsText) ?
this.config.noResultsText() :
this.config.noResultsText;
dropdownItem = this._getTemplate('notice', notice, 'no-results');
} else {
notice = isType('Function', this.config.noChoicesText) ?
this.config.noChoicesText() :
this.config.noChoicesText;
dropdownItem = this._getTemplate('notice', notice, 'no-choices');
}
this.choiceList.append(dropdownItem);
}
}
/* Items */
// Get active items (items that can be selected)
const activeItems = this.store.getItemsFilteredByActive() || [];
// Clear list
this.itemList.clear();
if (activeItems.length) {
// Create a fragment to store our list items
// (so we don't have to update the DOM for each item)
const itemListFragment = this.renderItems(activeItems);
// If we have items to add
if (itemListFragment.childNodes) {
// Update list
this.itemList.append(itemListFragment);
}
}
this.prevState = this.currentState;
}
/**
@ -530,7 +519,7 @@ class Choices {
eventResponse.groupValue = group.value;
}
triggerEvent(this.passedElement.element, EVENTS.highlightItem, eventResponse);
this.passedElement.triggerEvent(EVENTS.highlightItem, eventResponse);
}
return this;
@ -560,11 +549,8 @@ class Choices {
eventResponse.groupValue = group.value;
}
this.store.dispatch(
highlightItem(id, false),
);
triggerEvent(this.passedElement.element, EVENTS.highlightItem, eventResponse);
this.store.dispatch(highlightItem(id, false));
this.passedElement.triggerEvent(EVENTS.highlightItem, eventResponse);
return this;
}
@ -668,8 +654,8 @@ class Choices {
this.containerOuter.open(this.dropdown.getVerticalPos());
this.dropdown.show();
this.input.activate(focusInput);
this.passedElement.triggerEvent(EVENTS.showDropdown, {});
triggerEvent(this.passedElement.element, EVENTS.showDropdown, {});
return this;
}
@ -686,8 +672,7 @@ class Choices {
this.containerOuter.close();
this.dropdown.hide();
this.input.deactivate(blurInput);
triggerEvent(this.passedElement.element, EVENTS.hideDropdown, {});
this.passedElement.triggerEvent(EVENTS.hideDropdown, {});
return this;
}
@ -922,11 +907,10 @@ class Choices {
return this;
}
this.passedElement.element.disabled = false;
this.passedElement.enable();
if (this.containerOuter.isDisabled) {
this._addEventListeners();
this.passedElement.element.removeAttribute('disabled');
this.input.enable();
this.containerOuter.enable();
}
@ -944,11 +928,10 @@ class Choices {
return this;
}
this.passedElement.element.disabled = true;
this.passedElement.disable();
if (!this.containerOuter.isDisabled) {
this._removeEventListeners();
this.passedElement.element.setAttribute('disabled', '');
this.input.disable();
this.containerOuter.disable();
}
@ -992,7 +975,7 @@ class Choices {
return;
}
triggerEvent(this.passedElement.element, EVENTS.change, {
this.passedElement.triggerEvent(EVENTS.change, {
value,
});
}
@ -1102,7 +1085,7 @@ class Choices {
// Update choice keyCode
choice.keyCode = passedKeyCode;
triggerEvent(this.passedElement.element, EVENTS.choice, {
this.passedElement.triggerEvent(EVENTS.choice, {
choice,
});
@ -1155,6 +1138,7 @@ class Choices {
this._triggerChange(lastItem.value);
} else {
if (!hasHighlightedItems) {
// Highlight last item if none already highlighted
this.highlightItem(lastItem, false);
}
this.removeHighlightedItems(true);
@ -1312,26 +1296,26 @@ class Choices {
this.currentValue.trim() :
this.currentValue;
// If new value matches the desired length and is not the same as the current value with a space
if (newValue.length >= 1 && newValue !== `${currentValue} `) {
const haystack = this.store.getSearchableChoices();
const needle = newValue;
const keys = isType('Array', this.config.searchFields) ?
this.config.searchFields :
[this.config.searchFields];
const options = Object.assign(this.config.fuseOptions, { keys });
const fuse = new Fuse(haystack, options);
const results = fuse.search(needle);
this.currentValue = newValue;
this.highlightPosition = 0;
this.isSearching = true;
this.store.dispatch(filterChoices(results));
return results.length;
if (newValue.length < 1 && newValue === `${currentValue} `) {
return 0;
}
return 0;
// If new value matches the desired length and is not the same as the current value with a space
const haystack = this.store.getSearchableChoices();
const needle = newValue;
const keys = isType('Array', this.config.searchFields) ?
this.config.searchFields :
[this.config.searchFields];
const options = Object.assign(this.config.fuseOptions, { keys });
const fuse = new Fuse(haystack, options);
const results = fuse.search(needle);
this.currentValue = newValue;
this.highlightPosition = 0;
this.isSearching = true;
this.store.dispatch(filterChoices(results));
return results.length;
}
/**
@ -1352,7 +1336,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.element, EVENTS.search, {
this.passedElement.triggerEvent(EVENTS.search, {
value,
resultCount,
});
@ -1453,7 +1437,7 @@ class Choices {
this.canSearch = false;
if (
this.config.removeItems &&
!this.input.element.value &&
!this.input.getValue() &&
this.input.element === document.activeElement
) {
// Highlight items
@ -1465,7 +1449,7 @@ class Choices {
const onEnterKey = () => {
// If enter key is pressed and the input has a value
if (this.isTextElement && target.value) {
const value = this.input.element.value;
const value = this.input.getValue();
const canAddItem = this._canAddItem(activeItems, value);
// All is good, add
@ -1592,7 +1576,7 @@ class Choices {
return;
}
const value = this.input.element.value;
const value = this.input.getValue();
const activeItems = this.store.getItemsFilteredByActive();
const canAddItem = this._canAddItem(activeItems, value);
@ -1614,20 +1598,18 @@ class Choices {
this.hideDropdown();
}
} else {
const backKey = 46;
const deleteKey = 8;
const backKey = KEY_CODES.BACK_KEY;
const deleteKey = KEY_CODES.DELETE_KEY;
// If user has removed value...
if ((e.keyCode === backKey || e.keyCode === deleteKey) && !e.target.value) {
// ...and it is a multiple select input, activate choices (if searching)
if (!this.isTextElement && this.isSearching) {
this.isSearching = false;
this.store.dispatch(
activateChoices(true),
);
this.store.dispatch(activateChoices(true));
}
} else if (this.canSearch && canAddItem.response) {
this._handleSearch(this.input.element.value);
this._handleSearch(this.input.getValue());
}
}
// Re-establish canSearch value from changes in _onKeyDown
@ -1784,33 +1766,34 @@ class Choices {
*/
_onFocus(e) {
const target = e.target;
// If target is something that concerns us
if (this.containerOuter.element.contains(target)) {
const focusActions = {
text: () => {
if (target === this.input.element) {
this.containerOuter.addFocusState();
}
},
'select-one': () => {
this.containerOuter.addFocusState();
if (target === this.input.element) {
// Show dropdown if it isn't already showing
this.showDropdown();
}
},
'select-multiple': () => {
if (target === this.input.element) {
// If element is a select box, the focused element is the container and the dropdown
// isn't already open, focus and show dropdown
this.containerOuter.addFocusState();
this.showDropdown(true);
}
},
};
focusActions[this.passedElement.element.type]();
if (!this.containerOuter.element.contains(target)) {
return;
}
const focusActions = {
text: () => {
if (target === this.input.element) {
this.containerOuter.addFocusState();
}
},
'select-one': () => {
this.containerOuter.addFocusState();
if (target === this.input.element) {
// Show dropdown if it isn't already showing
this.showDropdown();
}
},
'select-multiple': () => {
if (target === this.input.element) {
// If element is a select box, the focused element is the container and the dropdown
// isn't already open, focus and show dropdown
this.containerOuter.addFocusState();
this.showDropdown(true);
}
},
};
focusActions[this.passedElement.element.type]();
}
/**
@ -1897,7 +1880,7 @@ class Choices {
choice.offsetTop;
const animateScroll = () => {
const strength = 4;
const strength = SCROLLING_SPEED;
const choiceListScrollTop = this.choiceList.scrollPos;
let continueAnimation = false;
let easing;
@ -2046,7 +2029,7 @@ class Choices {
// Trigger change event
if (group && group.value) {
triggerEvent(this.passedElement.element, EVENTS.addItem, {
this.passedElement.triggerEvent(EVENTS.addItem, {
id,
value: passedValue,
label: passedLabel,
@ -2054,7 +2037,7 @@ class Choices {
keyCode: passedKeyCode,
});
} else {
triggerEvent(this.passedElement.element, EVENTS.addItem, {
this.passedElement.triggerEvent(EVENTS.addItem, {
id,
value: passedValue,
label: passedLabel,
@ -2083,19 +2066,17 @@ class Choices {
const groupId = item.groupId;
const group = groupId >= 0 ? this.store.getGroupById(groupId) : null;
this.store.dispatch(
removeItem(id, choiceId),
);
this.store.dispatch(removeItem(id, choiceId));
if (group && group.value) {
triggerEvent(this.passedElement.element, EVENTS.removeItem, {
this.passedElement.triggerEvent(EVENTS.removeItem, {
id,
value,
label,
groupValue: group.value,
});
} else {
triggerEvent(this.passedElement.element, EVENTS.removeItem, {
this.passedElement.triggerEvent(EVENTS.removeItem, {
id,
value,
label,
@ -2263,10 +2244,16 @@ class Choices {
*/
_createInput() {
const direction = this.passedElement.element.getAttribute('dir') || 'ltr';
const containerOuter = this._getTemplate('containerOuter', direction);
const containerOuter = this._getTemplate('containerOuter',
direction,
this.isSelectElement,
this.isSelectOneElement,
this.config.searchEnabled,
this.passedElement.element.type,
);
const containerInner = this._getTemplate('containerInner');
const itemList = this._getTemplate('itemList');
const choiceList = this._getTemplate('choiceList');
const itemList = this._getTemplate('itemList', this.isSelectOneElement);
const choiceList = this._getTemplate('choiceList', this.isSelectOneElement);
const input = this._getTemplate('input');
const dropdown = this._getTemplate('dropdown');
@ -2281,7 +2268,6 @@ class Choices {
// Wrap input in container preserving DOM ordering
wrap(this.passedElement.element, this.containerInner.element);
// Wrapper inner container with outer container
wrap(this.containerInner.element, this.containerOuter.element);
@ -2304,21 +2290,21 @@ class Choices {
dropdown.appendChild(choiceList);
}
if (this.isSelectMultipleElement || this.isTextElement) {
if (!this.isSelectOneElement) {
this.containerInner.element.appendChild(this.input.element);
} else if (this.canSearch) {
dropdown.insertBefore(input, dropdown.firstChild);
}
if (this.isSelectElement) {
const passedGroups = Array.from(this.passedElement.element.getElementsByTagName('OPTGROUP'));
const passedGroups = this.passedElement.getOptionGroups();
this.highlightPosition = 0;
this.isSearching = false;
if (passedGroups && passedGroups.length) {
// If we have a placeholder option
const placeholderChoice = this.passedElement.element.querySelector('option[placeholder]');
const placeholderChoice = this.passedElement.getPlaceholderOption();
if (placeholderChoice && placeholderChoice.parentNode.tagName === 'SELECT') {
this._addChoice(
placeholderChoice.value,
@ -2335,7 +2321,7 @@ class Choices {
this._addGroup(group, (group.id || null));
});
} else {
const passedOptions = Array.from(this.passedElement.element.options);
const passedOptions = this.passedElement.getOptions();
const filter = this.config.sortFilter;
const allChoices = this.presetChoices;

View file

@ -32,27 +32,27 @@ describe('Choices', () => {
instance.destroy();
});
it('should be defined', () => {
it('is defined', () => {
expect(instance).to.not.be.undefined;
});
it('should have initialised', () => {
it('initialises', () => {
expect(instance.initialised).to.be.true;
});
it('should not re-initialise if passed element again', () => {
it('does not re-initialise if passed element again', () => {
const reinitialise = new Choices(instance.passedElement.element);
sinon.spy(reinitialise, '_createTemplates');
expect(reinitialise._createTemplates.callCount).to.equal(0);
});
it('should have a blank state', () => {
it('has a blank state', () => {
expect(instance.currentState.items.length).to.equal(0);
expect(instance.currentState.groups.length).to.equal(0);
expect(instance.currentState.choices.length).to.equal(0);
});
it('should have config options', () => {
it('has expected config options', () => {
expect(instance.config.silent).to.be.a('boolean');
expect(instance.config.items).to.be.an('array');
expect(instance.config.choices).to.be.an('array');
@ -90,7 +90,7 @@ describe('Choices', () => {
expect(instance.config.callbackOnCreateTemplates).to.be.null;
});
it('should expose public methods', () => {
it('exposes public methods', () => {
expect(instance.init).to.be.a('function');
expect(instance.destroy).to.be.a('function');
expect(instance.render).to.be.a('function');
@ -118,35 +118,35 @@ describe('Choices', () => {
expect(instance.clearInput).to.be.a('function');
});
it('should hide passed input', () => {
it('hides passed input', () => {
expect(instance.passedElement.element.style.display).to.equal('none');
});
it('should create an outer container', () => {
it('creates an outer container', () => {
expect(instance.containerOuter).to.be.an.instanceof(Container);
});
it('should create an inner container', () => {
it('creates an inner container', () => {
expect(instance.containerInner).to.be.an.instanceof(Container);
});
it('should create a choice list', () => {
it('creates a choice list', () => {
expect(instance.choiceList).to.be.an.instanceof(List);
});
it('should create an item list', () => {
it('creates an item list', () => {
expect(instance.itemList).to.be.an.instanceof(List);
});
it('should create an input', () => {
it('creates an input', () => {
expect(instance.input).to.be.an.instanceof(Input);
});
it('should create a dropdown', () => {
it('creates a dropdown', () => {
expect(instance.dropdown).to.be.an.instanceof(Dropdown);
});
it('should backup and recover original styles', () => {
it('backs up and recovers original styles', () => {
const origStyle = 'background-color: #ccc; margin: 5px padding: 10px;';
instance.destroy();
@ -165,7 +165,7 @@ describe('Choices', () => {
});
});
describe('should accept text inputs', () => {
describe('text inputs', () => {
let input;
let instance;
@ -182,12 +182,12 @@ describe('Choices', () => {
instance.destroy();
});
it('should wrap passed input', () => {
it('wraps passed input', () => {
instance = new Choices(input);
expect(instance.passedElement).to.be.an.instanceof(WrappedInput);
});
it('should accept a user inputted value', () => {
it('accepts a user inputted value', () => {
instance = new Choices(input);
instance.input.element.focus();
@ -202,13 +202,13 @@ describe('Choices', () => {
expect(instance.currentState.items[0].value).to.include(instance.input.element.value);
});
it('should copy the passed placeholder to the cloned input', () => {
it('copys the passed placeholder to the cloned input', () => {
instance = new Choices(input);
expect(instance.input.element.placeholder).to.equal(input.placeholder);
});
it('should not allow duplicates if duplicateItems is false', () => {
it('doesn\'t allow duplicate items if duplicateItems is false', () => {
instance = new Choices(input, {
duplicateItems: false,
items: ['test 1'],
@ -226,7 +226,7 @@ describe('Choices', () => {
expect(instance.currentState.items[instance.currentState.items.length - 1].value).to.equal(instance.input.element.value);
});
it('should filter input if regexFilter is passed', () => {
it('filters input if regexFilter is passed', () => {
instance = new Choices(input, {
regexFilter: /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/,
});
@ -255,7 +255,7 @@ describe('Choices', () => {
expect(lastItem.value).not.to.equal('not an email address');
});
it('should prepend and append values if passed', () => {
it('prepends and appends values if passed', () => {
instance = new Choices(input, {
prependValue: 'item-',
appendValue: '-value',
@ -277,7 +277,7 @@ describe('Choices', () => {
});
});
describe('should accept single select inputs', () => {
describe('single select inputs', () => {
let input;
let instance;
@ -302,23 +302,23 @@ describe('Choices', () => {
instance.destroy();
});
it('should wrap passed input', () => {
it('wraps passed input', () => {
instance = new Choices(input);
expect(instance.passedElement).to.be.an.instanceof(WrappedSelect);
});
it('should open the choice list on focusing', () => {
it('opens the choice list on focusing', () => {
instance = new Choices(input);
instance.input.element.focus();
expect(instance.dropdown.element.classList.contains(instance.config.classNames.activeState)).to.be.true;
});
it('should select the first choice', () => {
it('selects the first choice', () => {
instance = new Choices(input);
expect(instance.currentState.items[0].value).to.include('Value 1');
});
it('should highlight the choices on keydown', () => {
it('highlights the choices on keydown', () => {
instance = new Choices(input, {
renderChoiceLimit: -1,
});
@ -337,7 +337,7 @@ describe('Choices', () => {
expect(instance.highlightPosition).to.equal(2);
});
it('should select choice on enter key press', () => {
it('selects choice on enter key press', () => {
instance = new Choices(input);
instance.input.element.focus();
@ -360,7 +360,7 @@ describe('Choices', () => {
expect(instance.currentState.items.length).to.equal(2);
});
it('should trigger add/change event on selection', () => {
it('triggers add/change event on selection', () => {
instance = new Choices(input);
const onChangeStub = sinon.stub();
@ -394,7 +394,7 @@ describe('Choices', () => {
expect(addSpyStub.callCount).to.equal(1);
});
it('should open the dropdown on click', () => {
it('opens the dropdown on click', () => {
instance = new Choices(input);
const container = instance.containerOuter.element;
instance._onClick({
@ -406,7 +406,7 @@ describe('Choices', () => {
expect(document.activeElement === instance.input.element && container.classList.contains('is-open')).to.be.true;
});
it('should close the dropdown on double click', () => {
it('closes the dropdown on double click', () => {
instance = new Choices(input);
const container = instance.containerOuter.element;
const openState = instance.config.classNames.openState;
@ -426,7 +426,7 @@ describe('Choices', () => {
expect(document.activeElement === instance.input.element && container.classList.contains(openState)).to.be.false;
});
it('should trigger showDropdown on dropdown opening', () => {
it('triggers showDropdown on dropdown opening', () => {
instance = new Choices(input);
const container = instance.containerOuter.element;
@ -446,7 +446,7 @@ describe('Choices', () => {
expect(showDropdownStub.callCount).to.equal(1);
});
it('should trigger hideDropdown on dropdown closing', () => {
it('triggers hideDropdown on dropdown closing', () => {
instance = new Choices(input);
const container = instance.containerOuter.element;
@ -472,7 +472,7 @@ describe('Choices', () => {
expect(hideDropdownStub.callCount).to.equal(1);
});
it('should filter choices when searching', () => {
it('filters choices when searching', () => {
instance = new Choices(input);
const onSearchStub = sinon.spy();
@ -496,7 +496,7 @@ describe('Choices', () => {
expect(onSearchStub.callCount).to.equal(1);
});
it('shouldn\'t filter choices when searching', () => {
it('doesn\'t filter choices when searching', () => {
instance = new Choices(input, {
searchChoices: false,
});
@ -524,7 +524,7 @@ describe('Choices', () => {
expect(onSearchStub.callCount).to.equal(1);
});
it('shouldn\'t sort choices if shouldSort is false', () => {
it('doesn\'t sort choices if shouldSort is false', () => {
instance = new Choices(input, {
shouldSort: false,
choices: [
@ -544,7 +544,7 @@ describe('Choices', () => {
expect(instance.currentState.choices[0].value).to.equal('Value 5');
});
it('should sort choices if shouldSort is true', () => {
it('sorts choices if shouldSort is true', () => {
instance = new Choices(input, {
shouldSort: true,
choices: [
@ -565,7 +565,7 @@ describe('Choices', () => {
});
});
describe('should accept multiple select inputs', () => {
describe('multiple select inputs', () => {
let input;
let instance;
@ -613,24 +613,24 @@ describe('Choices', () => {
instance.destroy();
});
it('should wrap passed input', () => {
it('wraps passed input', () => {
expect(instance.passedElement).to.be.an.instanceof(WrappedSelect);
});
it('should add any pre-defined values', () => {
it('adds any pre-defined values', () => {
expect(instance.currentState.items.length).to.be.above(1);
});
it('should add options defined in the config + pre-defined options', () => {
it('adds options defined in the config + pre-defined options', () => {
expect(instance.currentState.choices.length).to.equal(6);
});
it('should add a placeholder defined in the config to the search input', () => {
it('adds a placeholder defined in the config to the search input', () => {
expect(instance.input.element.placeholder).to.equal('Placeholder text');
});
});
describe('should handle public methods on select input types', () => {
describe('handles public methods on select input types', () => {
let input;
let instance;
@ -661,7 +661,7 @@ describe('Choices', () => {
instance.destroy();
});
it('should handle highlightItem()', () => {
it('handles highlightItem()', () => {
const items = instance.currentState.items;
const randomItem = items[Math.floor(Math.random() * items.length)];
@ -670,7 +670,7 @@ describe('Choices', () => {
expect(randomItem.highlighted).to.be.true;
});
it('should handle unhighlightItem()', () => {
it('handles unhighlightItem()', () => {
const items = instance.currentState.items;
const randomItem = items[Math.floor(Math.random() * items.length)];
@ -679,7 +679,7 @@ describe('Choices', () => {
expect(randomItem.highlighted).to.be.false;
});
it('should handle highlightAll()', () => {
it('handles highlightAll()', () => {
const items = instance.currentState.items;
instance.highlightAll();
@ -689,7 +689,7 @@ describe('Choices', () => {
expect(unhighlightedItems).to.be.false;
});
it('should handle unhighlightAll()', () => {
it('handles unhighlightAll()', () => {
const items = instance.currentState.items;
instance.unhighlightAll();
@ -699,7 +699,7 @@ describe('Choices', () => {
expect(highlightedItems).to.be.false;
});
it('should handle removeHighlightedItems()', () => {
it('handles removeHighlightedItems()', () => {
const items = instance.currentState.items;
instance.highlightAll();
instance.removeHighlightedItems();
@ -709,7 +709,7 @@ describe('Choices', () => {
expect(activeItems).to.be.false;
});
it('should handle showDropdown()', () => {
it('handles showDropdown()', () => {
instance.showDropdown();
const hasOpenState = instance.containerOuter.element.classList.contains(instance.config.classNames.openState);
@ -719,7 +719,7 @@ describe('Choices', () => {
expect(hasOpenState && hasAttr && hasActiveState).to.be.true;
});
it('should handle hideDropdown()', () => {
it('handles hideDropdown()', () => {
instance.showDropdown();
instance.hideDropdown();
@ -731,14 +731,14 @@ describe('Choices', () => {
expect(hasOpenState && hasAttr && hasActiveState).to.be.false;
});
it('should handle toggleDropdown()', () => {
it('handles toggleDropdown()', () => {
sinon.spy(instance, 'hideDropdown');
instance.showDropdown();
instance.toggleDropdown();
expect(instance.hideDropdown.callCount).to.equal(1);
});
it('should handle getValue()', () => {
it('handles getValue()', () => {
const valueObjects = instance.getValue();
const valueStrings = instance.getValue(true);
@ -748,7 +748,7 @@ describe('Choices', () => {
expect(valueObjects.length).to.equal(5);
});
it('should handle setValue()', () => {
it('handles setValue()', () => {
instance.setValue(['Set value 1', 'Set value 2', 'Set value 3']);
const valueStrings = instance.getValue(true);
@ -757,7 +757,7 @@ describe('Choices', () => {
expect(valueStrings[valueStrings.length - 3]).to.equal('Set value 1');
});
it('should handle setValueByChoice()', () => {
it('handles setValueByChoice()', () => {
const choices = instance.store.getChoicesFilteredByActive();
const randomChoice = choices[Math.floor(Math.random() * choices.length)];
@ -770,7 +770,7 @@ describe('Choices', () => {
expect(value[0]).to.equal(randomChoice.value);
});
it('should handle setChoices()', () => {
it('handles setChoices()', () => {
instance.setChoices([{
label: 'Group one',
id: 1,
@ -818,7 +818,7 @@ describe('Choices', () => {
expect(choices[choices.length - 2].value).to.equal('Child Five');
});
it('should handle setChoices() with blank values', () => {
it('handles setChoices() with blank values', () => {
instance.setChoices([{
label: 'Choice one',
value: 'one',
@ -833,7 +833,7 @@ describe('Choices', () => {
expect(choices[1].value).to.equal('');
});
it('should handle clearStore()', () => {
it('handles clearStore()', () => {
instance.clearStore();
expect(instance.currentState.items).to.have.lengthOf(0);
@ -841,7 +841,7 @@ describe('Choices', () => {
expect(instance.currentState.groups).to.have.lengthOf(0);
});
it('should handle disable()', () => {
it('handles disable()', () => {
instance.disable();
expect(instance.input.element.disabled).to.be.true;
@ -853,7 +853,7 @@ describe('Choices', () => {
expect(instance.containerOuter.element.getAttribute('aria-disabled')).to.equal('true');
});
it('should handle enable()', () => {
it('handles enable()', () => {
instance.enable();
expect(instance.input.element.disabled).to.be.false;
@ -865,7 +865,7 @@ describe('Choices', () => {
expect(instance.containerOuter.element.hasAttribute('aria-disabled')).to.be.false;
});
it('should handle ajax()', () => {
it('handles ajax()', () => {
const dummyFn = sinon.spy();
instance.ajax(dummyFn);
@ -874,7 +874,7 @@ describe('Choices', () => {
});
});
describe('should handle public methods on select-one input types', () => {
describe('handles public methods on select-one input types', () => {
let input;
let instance;
@ -904,20 +904,20 @@ describe('Choices', () => {
instance.destroy();
});
it('should handle disable()', () => {
it('handles disable()', () => {
instance.disable();
expect(instance.containerOuter.element.getAttribute('tabindex')).to.equal('-1');
});
it('should handle enable()', () => {
it('handles enable()', () => {
instance.enable();
expect(instance.containerOuter.element.getAttribute('tabindex')).to.equal('0');
});
});
describe('should handle public methods on text input types', () => {
describe('handles public methods on text input types', () => {
let input;
let instance;
@ -935,12 +935,12 @@ describe('Choices', () => {
instance.destroy();
});
it('should handle clearInput()', () => {
it('handles clearInput()', () => {
instance.clearInput();
expect(instance.input.element.value).to.equal('');
});
it('should handle removeItemsByValue()', () => {
it('handles removeItemsByValue()', () => {
const items = instance.currentState.items;
const randomItem = items[Math.floor(Math.random() * items.length)];
@ -949,7 +949,7 @@ describe('Choices', () => {
});
});
describe('should react to config options', () => {
describe('reacts to config options', () => {
let input;
let instance;
@ -1056,7 +1056,7 @@ describe('Choices', () => {
});
});
describe('should allow custom properties provided by the user on items or choices', () => {
describe('allows custom properties provided by the user on items or choices', () => {
let input;
let instance;
@ -1072,7 +1072,7 @@ describe('Choices', () => {
instance.destroy();
});
it('should allow the user to supply custom properties for a choice that will be inherited by the item when the user selects the choice', () => {
it('allows the user to supply custom properties for a choice that will be inherited by the item when the user selects the choice', () => {
const expectedCustomProperties = {
isBestOptionEver: true,
};
@ -1093,7 +1093,7 @@ describe('Choices', () => {
expect(selectedItems[0].customProperties).to.equal(expectedCustomProperties);
});
it('should allow the user to supply custom properties when directly creating a selected item', () => {
it('allows the user to supply custom properties when directly creating a selected item', () => {
const expectedCustomProperties = {
isBestOptionEver: true,
};

View file

@ -13,6 +13,10 @@ export default class Container {
this.onBlur = this.onBlur.bind(this);
}
getElement() {
return this.element;
}
/**
* Add event listeners
*/
@ -155,6 +159,16 @@ export default class Container {
this.isDisabled = true;
}
revert(originalElement) {
// Move passed element back to original position
this.element.parentNode.insertBefore(
originalElement,
this.element,
);
// Remove container
this.element.parentNode.removeChild(this.element);
}
/**
* Add loading state to element
*/

View file

@ -8,6 +8,10 @@ export default class Dropdown {
this.isActive = false;
}
getElement() {
return this.element;
}
/**
* Determine how far the top of our element is from
* the top of the window

View file

@ -15,6 +15,10 @@ export default class Input {
this.onBlur = this.onBlur.bind(this);
}
getElement() {
return this.element;
}
addEventListeners() {
this.element.addEventListener('input', this.onInput);
this.element.addEventListener('paste', this.onPaste);

View file

@ -8,6 +8,10 @@ export default class List {
this.hasChildren = !!this.element.children;
}
getElement() {
return this.element;
}
/**
* Clear List contents
*/

View file

@ -1,8 +1,19 @@
import { dispatchEvent } from '../lib/utils';
export default class WrappedElement {
constructor(instance, element, classNames) {
this.parentInstance = instance;
this.element = element;
this.classNames = classNames;
this.isDisabled = false;
}
getElement() {
return this.element;
}
getValue() {
return this.element.value;
}
conceal() {
@ -50,4 +61,20 @@ export default class WrappedElement {
// Re-assign values - this is weird, I know
this.element.value = this.element.value;
}
enable() {
this.element.removeAttribute('disabled');
this.element.disabled = false;
this.isDisabled = false;
}
disable() {
this.element.setAttribute('disabled', '');
this.element.disabled = true;
this.isDisabled = true;
}
triggerEvent(eventType, data) {
dispatchEvent(this.element, eventType, data);
}
}

View file

@ -8,6 +8,10 @@ export default class WrappedInput extends WrappedElement {
this.classNames = classNames;
}
getElement() {
super.getElement();
}
conceal() {
super.conceal();
}
@ -15,4 +19,17 @@ export default class WrappedInput extends WrappedElement {
reveal() {
super.reveal();
}
enable() {
super.enable();
}
disable() {
super.enable();
}
setValue(value) {
this.element.setAttribute('value', value);
this.element.value = value;
}
}

View file

@ -8,6 +8,10 @@ export default class WrappedSelect extends WrappedElement {
this.classNames = classNames;
}
getElement() {
super.getElement();
}
conceal() {
super.conceal();
}
@ -15,4 +19,29 @@ export default class WrappedSelect extends WrappedElement {
reveal() {
super.reveal();
}
enable() {
super.enable();
}
disable() {
super.enable();
}
setOptions(options) {
this.element.innerHTML = '';
this.element.appendChild(options);
}
getPlaceholderOption() {
return this.element.querySelector('option[placeholder]');
}
getOptions() {
return Array.from(this.element.options);
}
getOptionGroups() {
return Array.from(this.element.getElementsByTagName('OPTGROUP'));
}
}

View file

@ -103,3 +103,5 @@ export const KEY_CODES = {
PAGE_UP_KEY: 33,
PAGE_DOWN_KEY: 34,
};
export const SCROLLING_SPEED = 4;

View file

@ -552,13 +552,13 @@ export const sortByAlpha = (a, b) => {
export const sortByScore = (a, b) => a.score - b.score;
/**
* Trigger native event
* Dispatch native event
* @param {NodeElement} element Element to trigger event on
* @param {String} type Type of event to trigger
* @param {Object} customArgs Data to pass with event
* @return {Object} Triggered event
*/
export const triggerEvent = (element, type, customArgs = null) => {
export const dispatchEvent = (element, type, customArgs = null) => {
const event = new CustomEvent(type, {
detail: customArgs,
bubbles: true,

View file

@ -2,12 +2,19 @@ import classNames from 'classnames';
import { strToEl } from './lib/utils';
export const TEMPLATES = {
containerOuter(globalClasses, direction) {
const tabIndex = this.isSelectOneElement ? 'tabindex="0"' : '';
let role = this.isSelectElement ? 'role="listbox"' : '';
containerOuter(
globalClasses,
direction,
isSelectElement,
isSelectOneElement,
searchEnabled,
passedElementType,
) {
const tabIndex = isSelectOneElement ? 'tabindex="0"' : '';
let role = isSelectElement ? 'role="listbox"' : '';
let ariaAutoComplete = '';
if (this.isSelectElement && this.config.searchEnabled) {
if (isSelectElement && searchEnabled) {
role = 'role="combobox"';
ariaAutoComplete = 'aria-autocomplete="list"';
}
@ -15,7 +22,7 @@ export const TEMPLATES = {
return strToEl(`
<div
class="${globalClasses.containerOuter}"
data-type="${this.passedElement.element.type}"
data-type="${passedElementType}"
${role}
${tabIndex}
${ariaAutoComplete}
@ -31,11 +38,11 @@ export const TEMPLATES = {
<div class="${globalClasses.containerInner}"></div>
`);
},
itemList(globalClasses) {
itemList(globalClasses, isSelectOneElement) {
const localClasses = classNames(
globalClasses.list, {
[globalClasses.listSingle]: (this.isSelectOneElement),
[globalClasses.listItems]: (!this.isSelectOneElement),
[globalClasses.listSingle]: (isSelectOneElement),
[globalClasses.listItems]: (!isSelectOneElement),
},
);
@ -50,7 +57,7 @@ export const TEMPLATES = {
</div>
`);
},
item(globalClasses, data) {
item(globalClasses, data, removeItemButton) {
const ariaSelected = data.active ? 'aria-selected="true"' : '';
const ariaDisabled = data.disabled ? 'aria-disabled="true"' : '';
@ -62,7 +69,7 @@ export const TEMPLATES = {
},
);
if (this.config.removeItemButton) {
if (removeItemButton) {
localClasses = classNames(
globalClasses.item, {
[globalClasses.highlightedState]: data.highlighted,
@ -107,8 +114,8 @@ export const TEMPLATES = {
</div>
`);
},
choiceList(globalClasses) {
const ariaMultiSelectable = !this.isSelectOneElement ?
choiceList(globalClasses, isSelectOneElement) {
const ariaMultiSelectable = !isSelectOneElement ?
'aria-multiselectable="true"' :
'';
@ -143,7 +150,7 @@ export const TEMPLATES = {
</div>
`);
},
choice(globalClasses, data) {
choice(globalClasses, data, itemSelectText) {
const role = data.groupId > 0 ? 'role="treeitem"' : 'role="option"';
const localClasses = classNames(
globalClasses.item,
@ -157,7 +164,7 @@ export const TEMPLATES = {
return strToEl(`
<div
class="${localClasses}"
data-select-text="${this.config.itemSelectText}"
data-select-text="${itemSelectText}"
data-choice
data-id="${data.id}"
data-value="${data.value}"