2016-03-17 16:00:22 +01:00
|
|
|
'use strict';
|
|
|
|
|
2016-07-11 16:27:58 +02:00
|
|
|
import './lib/polyfills.js';
|
2016-06-30 14:57:56 +02:00
|
|
|
import { addItem, removeItem, highlightItem, addChoice, filterChoices, activateChoices, addGroup, clearAll } from './actions/index';
|
|
|
|
import { isScrolledIntoView, getAdjacentEl, findAncestor, wrap, isType, isElement, strToEl, extend, getWidthOfInput, debounce } from './lib/utils.js';
|
2016-05-03 15:55:38 +02:00
|
|
|
import Fuse from 'fuse.js';
|
2016-05-07 13:36:50 +02:00
|
|
|
import Store from './store/index.js';
|
2016-04-13 15:40:41 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Choices
|
|
|
|
*/
|
2016-03-18 13:26:38 +01:00
|
|
|
export class Choices {
|
2016-06-30 14:57:56 +02:00
|
|
|
constructor(element = '[data-choice]', userConfig = {}) {
|
2016-04-17 13:09:46 +02:00
|
|
|
|
2016-04-10 22:23:42 +02:00
|
|
|
// If there are multiple elements, create a new instance
|
|
|
|
// for each element besides the first one (as that already has an instance)
|
|
|
|
if(isType('String', element)) {
|
2016-04-12 23:42:56 +02:00
|
|
|
const elements = document.querySelectorAll(element);
|
2016-04-10 22:23:42 +02:00
|
|
|
if(elements.length > 1) {
|
|
|
|
for (let i = 1; i < elements.length; i++) {
|
2016-05-03 22:31:05 +02:00
|
|
|
const el = elements[i];
|
2016-06-29 15:47:58 +02:00
|
|
|
new Choices(el, userConfig);
|
2016-04-10 22:23:42 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-06-29 15:47:58 +02:00
|
|
|
const defaultConfig = {
|
2016-04-07 14:57:57 +02:00
|
|
|
items: [],
|
2016-07-30 17:03:20 +02:00
|
|
|
choices: [],
|
2016-06-27 15:46:12 +02:00
|
|
|
maxItemCount: -1,
|
2016-03-24 15:42:03 +01:00
|
|
|
addItems: true,
|
2016-03-22 23:47:29 +01:00
|
|
|
removeItems: true,
|
2016-06-27 15:46:12 +02:00
|
|
|
removeItemButton: false,
|
2016-03-21 19:53:26 +01:00
|
|
|
editItems: false,
|
2016-06-27 15:46:12 +02:00
|
|
|
duplicateItems: true,
|
2016-03-18 12:05:50 +01:00
|
|
|
delimiter: ',',
|
2016-06-27 15:46:12 +02:00
|
|
|
paste: true,
|
2016-07-02 14:04:38 +02:00
|
|
|
search: true,
|
2016-06-22 00:06:23 +02:00
|
|
|
regexFilter: null,
|
2016-04-15 10:19:02 +02:00
|
|
|
placeholder: true,
|
2016-06-22 00:06:23 +02:00
|
|
|
placeholderValue: null,
|
|
|
|
prependValue: null,
|
|
|
|
appendValue: null,
|
2016-06-01 19:56:08 +02:00
|
|
|
loadingText: 'Loading...',
|
2016-04-10 23:54:56 +02:00
|
|
|
classNames: {
|
2016-04-11 15:13:50 +02:00
|
|
|
containerOuter: 'choices',
|
|
|
|
containerInner: 'choices__inner',
|
|
|
|
input: 'choices__input',
|
|
|
|
inputCloned: 'choices__input--cloned',
|
|
|
|
list: 'choices__list',
|
2016-05-07 13:36:50 +02:00
|
|
|
listItems: 'choices__list--multiple',
|
|
|
|
listSingle: 'choices__list--single',
|
2016-04-11 15:13:50 +02:00
|
|
|
listDropdown: 'choices__list--dropdown',
|
|
|
|
item: 'choices__item',
|
|
|
|
itemSelectable: 'choices__item--selectable',
|
2016-04-12 23:42:56 +02:00
|
|
|
itemDisabled: 'choices__item--disabled',
|
2016-06-27 15:57:33 +02:00
|
|
|
itemChoice: 'choices__item--choice',
|
2016-04-16 18:06:27 +02:00
|
|
|
group: 'choices__group',
|
|
|
|
groupHeading : 'choices__heading',
|
2016-05-11 15:51:32 +02:00
|
|
|
button: 'choices__button',
|
2016-04-11 15:13:50 +02:00
|
|
|
activeState: 'is-active',
|
2016-05-05 22:46:56 +02:00
|
|
|
focusState: 'is-focused',
|
|
|
|
openState: 'is-open',
|
2016-04-11 15:13:50 +02:00
|
|
|
disabledState: 'is-disabled',
|
2016-04-26 15:36:02 +02:00
|
|
|
highlightedState: 'is-highlighted',
|
2016-04-11 15:13:50 +02:00
|
|
|
hiddenState: 'is-hidden',
|
2016-04-21 15:42:57 +02:00
|
|
|
flippedState: 'is-flipped',
|
2016-06-30 14:57:56 +02:00
|
|
|
loadingState: 'is-loading',
|
2016-04-10 23:54:56 +02:00
|
|
|
},
|
2016-05-08 01:02:52 +02:00
|
|
|
callbackOnInit: () => {},
|
2016-06-27 15:46:12 +02:00
|
|
|
callbackOnAddItem: (id, value, passedInput) => {},
|
|
|
|
callbackOnRemoveItem: (id, value, passedInput) => {},
|
2016-07-31 21:02:46 +02:00
|
|
|
callbackOnChange: (value, passedInput) => {},
|
2016-03-17 16:00:22 +01:00
|
|
|
};
|
2016-03-15 23:42:10 +01:00
|
|
|
|
2016-03-17 16:00:22 +01:00
|
|
|
// Merge options with user options
|
2016-06-29 15:47:58 +02:00
|
|
|
this.config = extend(defaultConfig, userConfig);
|
2016-04-04 22:44:32 +02:00
|
|
|
|
|
|
|
// Create data store
|
2016-06-22 00:06:23 +02:00
|
|
|
this.store = new Store(this.render);
|
2016-05-04 15:31:29 +02:00
|
|
|
|
|
|
|
// State tracking
|
2016-06-01 19:45:35 +02:00
|
|
|
this.initialised = false;
|
2016-05-04 15:31:29 +02:00
|
|
|
this.currentState = {};
|
2016-06-01 19:45:35 +02:00
|
|
|
this.prevState = {};
|
2016-06-07 15:20:37 +02:00
|
|
|
this.currentValue = '';
|
2016-03-30 16:04:21 +02:00
|
|
|
|
2016-06-29 15:47:58 +02:00
|
|
|
// Retrieve triggering element (i.e. element with 'data-option' trigger)
|
2016-04-10 22:23:42 +02:00
|
|
|
this.passedElement = isType('String', element) ? document.querySelector(element) : element;
|
2016-03-31 15:51:41 +02:00
|
|
|
|
2016-05-02 22:39:33 +02:00
|
|
|
this.highlightPosition = 0;
|
2016-07-02 14:04:38 +02:00
|
|
|
this.canSearch = this.config.search;
|
2016-05-02 22:39:33 +02:00
|
|
|
|
2016-07-30 17:03:20 +02:00
|
|
|
// Assing preset choices from passed object
|
|
|
|
this.presetChoices = this.config.choices;
|
2016-07-30 16:12:22 +02:00
|
|
|
|
2016-06-01 19:45:35 +02:00
|
|
|
// Assign preset items from passed object first
|
2016-06-29 15:47:58 +02:00
|
|
|
this.presetItems = this.config.items;
|
2016-07-30 16:12:22 +02:00
|
|
|
|
2016-06-01 19:45:35 +02:00
|
|
|
// Then add any values passed from attribute
|
2016-06-29 15:47:58 +02:00
|
|
|
if(this.passedElement.value) {
|
|
|
|
this.presetItems = this.presetItems.concat(this.passedElement.value.split(this.config.delimiter));
|
2016-04-07 14:57:57 +02:00
|
|
|
}
|
|
|
|
|
2016-03-17 16:00:22 +01:00
|
|
|
// Bind methods
|
2016-06-01 19:45:35 +02:00
|
|
|
this.init = this.init.bind(this);
|
2016-06-22 00:06:23 +02:00
|
|
|
this.render = this.render.bind(this);
|
2016-04-08 10:07:41 +02:00
|
|
|
this.destroy = this.destroy.bind(this);
|
2016-05-08 01:02:52 +02:00
|
|
|
this.disable = this.disable.bind(this);
|
2016-04-29 19:23:06 +02:00
|
|
|
|
|
|
|
// Bind event handlers
|
2016-06-08 15:45:29 +02:00
|
|
|
this._onFocus = this._onFocus.bind(this);
|
|
|
|
this._onBlur = this._onBlur.bind(this);
|
|
|
|
this._onKeyUp = this._onKeyUp.bind(this);
|
|
|
|
this._onKeyDown = this._onKeyDown.bind(this);
|
|
|
|
this._onMouseDown = this._onMouseDown.bind(this);
|
|
|
|
this._onMouseOver = this._onMouseOver.bind(this);
|
|
|
|
this._onPaste = this._onPaste.bind(this);
|
|
|
|
this._onInput = this._onInput.bind(this);
|
2016-04-29 16:18:53 +02:00
|
|
|
|
2016-05-07 13:36:50 +02:00
|
|
|
// 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
|
2016-07-02 14:50:24 +02:00
|
|
|
const canInit = this.passedElement && isElement(this.passedElement) && ['select-one', 'select-multiple', 'text'].some(type => type === this.passedElement.type);
|
2016-05-07 13:36:50 +02:00
|
|
|
|
|
|
|
if(canInit) {
|
2016-06-29 15:47:58 +02:00
|
|
|
// If element has already been initalised with Choices
|
2016-07-15 00:21:28 +02:00
|
|
|
if(this.passedElement.getAttribute('data-choice') === 'active') return;
|
2016-06-29 15:47:58 +02:00
|
|
|
|
|
|
|
// Let's go
|
|
|
|
this.init();
|
2016-05-07 13:36:50 +02:00
|
|
|
} else {
|
2016-06-29 15:47:58 +02:00
|
|
|
console.error('Incompatible input passed');
|
2016-05-07 13:36:50 +02:00
|
|
|
}
|
2016-03-17 16:00:22 +01:00
|
|
|
}
|
2016-06-08 15:45:29 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Initialise Choices
|
|
|
|
* @return
|
|
|
|
* @public
|
|
|
|
*/
|
|
|
|
init(callback) {
|
|
|
|
if(this.initialised !== true) {
|
|
|
|
|
|
|
|
this.initialised = true;
|
|
|
|
|
|
|
|
// Create required elements
|
|
|
|
this._createTemplates();
|
|
|
|
|
|
|
|
// Generate input markup
|
|
|
|
this._createInput();
|
|
|
|
|
2016-06-22 00:06:23 +02:00
|
|
|
this.store.subscribe(this.render);
|
2016-06-08 15:45:29 +02:00
|
|
|
|
|
|
|
// Render any items
|
2016-06-22 00:06:23 +02:00
|
|
|
this.render();
|
2016-06-08 15:45:29 +02:00
|
|
|
|
|
|
|
// Trigger event listeners
|
|
|
|
this._addEventListeners();
|
|
|
|
|
|
|
|
// Run callback if it is a function
|
2016-06-29 15:47:58 +02:00
|
|
|
if(callback = this.config.callbackOnInit){
|
2016-06-08 15:45:29 +02:00
|
|
|
if(isType('Function', callback)) {
|
|
|
|
callback();
|
|
|
|
} else {
|
|
|
|
console.error('callbackOnInit: Callback is not a function');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2016-04-29 19:06:46 +02:00
|
|
|
|
2016-06-08 15:45:29 +02:00
|
|
|
/**
|
|
|
|
* Destroy Choices and nullify values
|
|
|
|
* @return
|
|
|
|
* @public
|
|
|
|
*/
|
|
|
|
destroy() {
|
2016-07-31 21:02:46 +02:00
|
|
|
this._removeEventListeners();
|
|
|
|
|
2016-06-29 15:47:58 +02:00
|
|
|
this.passedElement.classList.remove(this.config.classNames.input, this.config.classNames.hiddenState);
|
2016-06-08 15:45:29 +02:00
|
|
|
this.passedElement.tabIndex = '';
|
|
|
|
this.passedElement.removeAttribute('style', 'display:none;');
|
|
|
|
this.passedElement.removeAttribute('aria-hidden');
|
|
|
|
|
|
|
|
this.containerOuter.outerHTML = this.passedElement.outerHTML;
|
|
|
|
|
|
|
|
this.passedElement = null;
|
2016-06-29 15:47:58 +02:00
|
|
|
this.userConfig = null;
|
|
|
|
this.config = null;
|
2016-06-08 15:45:29 +02:00
|
|
|
this.store = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Select item (a selected item can be deleted)
|
|
|
|
* @param {Element} item Element to select
|
|
|
|
* @return {Object} Class instance
|
|
|
|
* @public
|
|
|
|
*/
|
2016-06-30 14:57:56 +02:00
|
|
|
highlightItem(item) {
|
2016-06-08 15:45:29 +02:00
|
|
|
if(!item) return;
|
|
|
|
const id = item.id;
|
2016-06-30 14:57:56 +02:00
|
|
|
this.store.dispatch(highlightItem(id, true));
|
2016-06-08 15:45:29 +02:00
|
|
|
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Deselect item
|
|
|
|
* @param {Element} item Element to de-select
|
|
|
|
* @return {Object} Class instance
|
|
|
|
* @public
|
|
|
|
*/
|
2016-06-30 14:57:56 +02:00
|
|
|
unhighlightItem(item) {
|
2016-06-08 15:45:29 +02:00
|
|
|
if(!item) return;
|
|
|
|
const id = item.id;
|
2016-06-30 14:57:56 +02:00
|
|
|
this.store.dispatch(highlightItem(id, false));
|
2016-06-08 15:45:29 +02:00
|
|
|
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2016-06-22 00:06:23 +02:00
|
|
|
* Highlight items within store
|
2016-06-08 15:45:29 +02:00
|
|
|
* @return {Object} Class instance
|
|
|
|
* @public
|
|
|
|
*/
|
2016-06-22 00:06:23 +02:00
|
|
|
highlightAll() {
|
2016-06-08 15:45:29 +02:00
|
|
|
const items = this.store.getItems();
|
|
|
|
items.forEach((item) => {
|
2016-06-30 14:57:56 +02:00
|
|
|
this.highlightItem(item);
|
2016-06-08 15:45:29 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Deselect items within store
|
|
|
|
* @return {Object} Class instance
|
|
|
|
* @public
|
|
|
|
*/
|
2016-06-30 14:57:56 +02:00
|
|
|
unhighlightAll() {
|
2016-06-08 15:45:29 +02:00
|
|
|
const items = this.store.getItems();
|
|
|
|
items.forEach((item) => {
|
2016-06-30 14:57:56 +02:00
|
|
|
this.unhighlightItem(item);
|
2016-06-08 15:45:29 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove an item from the store by its value
|
|
|
|
* @param {String} value Value to search for
|
|
|
|
* @return {Object} Class instance
|
|
|
|
* @public
|
|
|
|
*/
|
|
|
|
removeItemsByValue(value) {
|
|
|
|
if(!value || !isType('String', value)) console.error('removeItemsByValue: No value was passed to be removed'); return;
|
|
|
|
|
|
|
|
const items = this.store.getItemsFilteredByActive();
|
|
|
|
|
|
|
|
items.forEach((item) => {
|
|
|
|
if(item.value === value) {
|
2016-06-22 00:06:23 +02:00
|
|
|
this._removeItem(item);
|
2016-06-08 15:45:29 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove all items from store array
|
|
|
|
* Note: removed items are soft deleted
|
2016-06-22 00:06:23 +02:00
|
|
|
* @param {Number} excludedId Optionally exclude item by ID
|
2016-06-08 15:45:29 +02:00
|
|
|
* @return {Object} Class instance
|
|
|
|
* @public
|
|
|
|
*/
|
|
|
|
removeActiveItems(excludedId) {
|
|
|
|
const items = this.store.getItemsFilteredByActive();
|
|
|
|
|
|
|
|
items.forEach((item) => {
|
|
|
|
if(item.active && excludedId !== item.id) {
|
2016-06-22 00:06:23 +02:00
|
|
|
this._removeItem(item);
|
2016-06-08 15:45:29 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove all selected items from store
|
|
|
|
* Note: removed items are soft deleted
|
|
|
|
* @return {Object} Class instance
|
|
|
|
* @public
|
|
|
|
*/
|
2016-06-30 14:57:56 +02:00
|
|
|
removeHighlightedItems() {
|
2016-06-08 15:45:29 +02:00
|
|
|
const items = this.store.getItemsFilteredByActive();
|
|
|
|
|
|
|
|
items.forEach((item) => {
|
2016-06-30 14:57:56 +02:00
|
|
|
if(item.highlighted && item.active) {
|
2016-06-22 00:06:23 +02:00
|
|
|
this._removeItem(item);
|
2016-06-08 15:45:29 +02:00
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Show dropdown to user by adding active state class
|
|
|
|
* @return {Object} Class instance
|
|
|
|
* @public
|
|
|
|
*/
|
|
|
|
showDropdown() {
|
2016-06-29 15:47:58 +02:00
|
|
|
this.containerOuter.classList.add(this.config.classNames.openState);
|
|
|
|
this.dropdown.classList.add(this.config.classNames.activeState);
|
2016-06-08 15:45:29 +02:00
|
|
|
|
|
|
|
const dimensions = this.dropdown.getBoundingClientRect();
|
|
|
|
const shouldFlip = dimensions.top + dimensions.height >= document.body.offsetHeight;
|
|
|
|
|
|
|
|
// Whether or not the dropdown should appear above or below input
|
|
|
|
if(shouldFlip) {
|
2016-06-29 15:47:58 +02:00
|
|
|
this.containerOuter.classList.add(this.config.classNames.flippedState);
|
2016-06-08 15:45:29 +02:00
|
|
|
} else {
|
2016-06-29 15:47:58 +02:00
|
|
|
this.containerOuter.classList.remove(this.config.classNames.flippedState);
|
2016-06-08 15:45:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Hide dropdown from user
|
|
|
|
* @return {Object} Class instance
|
|
|
|
* @public
|
|
|
|
*/
|
|
|
|
hideDropdown() {
|
|
|
|
// A dropdown flips if it does not have space below the input
|
2016-06-29 15:47:58 +02:00
|
|
|
const isFlipped = this.containerOuter.classList.contains(this.config.classNames.flippedState);
|
2016-06-08 15:45:29 +02:00
|
|
|
|
2016-06-29 15:47:58 +02:00
|
|
|
this.containerOuter.classList.remove(this.config.classNames.openState);
|
|
|
|
this.dropdown.classList.remove(this.config.classNames.activeState);
|
2016-06-08 15:45:29 +02:00
|
|
|
|
|
|
|
if(isFlipped) {
|
2016-06-29 15:47:58 +02:00
|
|
|
this.containerOuter.classList.remove(this.config.classNames.flippedState);
|
2016-06-08 15:45:29 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determine whether to hide or show dropdown based on its current state
|
|
|
|
* @return {Object} Class instance
|
|
|
|
* @public
|
|
|
|
*/
|
|
|
|
toggleDropdown() {
|
2016-06-29 15:47:58 +02:00
|
|
|
if(this.dropdown.classList.contains(this.config.classNames.activeState)) {
|
|
|
|
this.hideDropdown()
|
|
|
|
} else {
|
|
|
|
this.showDropdown();
|
|
|
|
}
|
2016-06-08 15:45:29 +02:00
|
|
|
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
2016-08-02 08:45:08 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Get value(s) of input (i.e. inputted items (text) or selected choices (select))
|
|
|
|
* @param {Boolean} valueOnly Get only values of selected items, otherwise return selected items
|
|
|
|
* @return {Array/String} selected value (select-one) or array of selected items (inputs & select-multiple)
|
|
|
|
* @public
|
|
|
|
*/
|
|
|
|
getValue(valueOnly = false){
|
|
|
|
const items = this.store.getItemsFilteredByActive();
|
|
|
|
let selectedItems = [];
|
|
|
|
|
|
|
|
items.forEach((item) => {
|
|
|
|
if (this.passedElement.type === 'text'){
|
|
|
|
selectedItems.push(valueOnly ? item.value : item);
|
|
|
|
} else if(item.active) {
|
|
|
|
selectedItems.push(valueOnly ? item.value : item);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
if (this.passedElement.type == 'select-one') {
|
|
|
|
return selectedItems[0];
|
|
|
|
} else {
|
|
|
|
return selectedItems;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-06-08 15:45:29 +02:00
|
|
|
/**
|
2016-07-31 22:05:17 +02:00
|
|
|
* Set value of input. If the input is a select box, a choice will be created and selected otherwise
|
|
|
|
* an item will created directly.
|
2016-06-08 15:45:29 +02:00
|
|
|
* @param {Array} args Array of value objects or value strings
|
|
|
|
* @return {Object} Class instance
|
|
|
|
* @public
|
|
|
|
*/
|
|
|
|
setValue(args) {
|
2016-07-31 21:02:46 +02:00
|
|
|
if(this.initialised === true) {
|
|
|
|
// Convert args to an itterable array
|
|
|
|
const values = [...args];
|
|
|
|
|
|
|
|
values.forEach((item, index) => {
|
|
|
|
if(isType('Object', item)) {
|
|
|
|
if(!item.value) return;
|
|
|
|
// If we are dealing with a select input, we need to create an option first
|
|
|
|
// that is then selected. For text inputs we can just add items normally.
|
|
|
|
if(this.passedElement.type !== 'text') {
|
|
|
|
this._addChoice(true, false, item.value, item.label, -1);
|
|
|
|
} else {
|
|
|
|
this._addItem(item.value, item.label, item.id);
|
|
|
|
}
|
|
|
|
} else if(isType('String', item)) {
|
|
|
|
if(this.passedElement.type !== 'text') {
|
|
|
|
this._addChoice(true, false, item, item, -1);
|
|
|
|
} else {
|
|
|
|
this._addItem(item);
|
|
|
|
}
|
2016-06-08 15:45:29 +02:00
|
|
|
}
|
2016-07-31 21:02:46 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
2016-07-31 22:05:17 +02:00
|
|
|
/**
|
|
|
|
* Select value of select box via the value of an existing choice
|
|
|
|
* @param {Array/String} value An array of strings of a single string
|
|
|
|
* @return {Object} Class instance
|
|
|
|
* @public
|
|
|
|
*/
|
|
|
|
setValueByChoice(value) {
|
|
|
|
if (this.passedElement.type !== 'text') {
|
|
|
|
const choices = this.store.getChoices();
|
|
|
|
|
|
|
|
// If only one value has been passed, convert to array
|
|
|
|
if (!isType('Array', value)) {
|
|
|
|
value = [value];
|
|
|
|
}
|
|
|
|
|
|
|
|
// Loop through each value and
|
|
|
|
value.forEach((val, index) => {
|
|
|
|
const foundChoice = choices.find((choice) => {
|
|
|
|
// Check 'value' property exists and the choice isn't already selected
|
|
|
|
if(choice.value === val) {
|
|
|
|
return true;
|
|
|
|
} else {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
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 that does not exist');
|
|
|
|
}
|
|
|
|
})
|
|
|
|
}
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
2016-07-31 21:02:46 +02:00
|
|
|
/**
|
|
|
|
* Direct populate choices
|
|
|
|
* @param {Array} choices - Choices to insert
|
|
|
|
* @param {string} value - Name of 'value' property
|
|
|
|
* @param {string} label - Name of 'label' property
|
|
|
|
* @return {Object} Class instance
|
|
|
|
* @public
|
|
|
|
*/
|
|
|
|
setChoices(choices, value, label){
|
|
|
|
if(this.initialised === true) {
|
|
|
|
if(this.passedElement.type === 'select-one' || this.passedElement.type === 'select-multiple') {
|
|
|
|
if(!isType('Array', choices) || !value) return;
|
|
|
|
|
|
|
|
if(choices && choices.length) {
|
|
|
|
this.containerOuter.classList.remove(this.config.classNames.loadingState);
|
|
|
|
choices.forEach((result, index) => {
|
|
|
|
// Select first choice in list if single select input
|
|
|
|
if(index === 0 && this.passedElement.type === 'select-one') {
|
2016-07-31 22:05:17 +02:00
|
|
|
this._addChoice(true, result.disabled ? result.disabled : false, result[value], result[label]);
|
2016-07-31 21:02:46 +02:00
|
|
|
} else {
|
2016-07-31 22:05:17 +02:00
|
|
|
this._addChoice(result.selected ? result.selected : false, result.disabled ? result.disabled : false, result[value], result[label]);
|
2016-07-31 21:02:46 +02:00
|
|
|
}
|
|
|
|
});
|
2016-06-08 15:45:29 +02:00
|
|
|
}
|
|
|
|
}
|
2016-07-31 21:02:46 +02:00
|
|
|
}
|
2016-06-08 15:45:29 +02:00
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Clear value of inputs
|
|
|
|
* @return {Object} Class instance
|
|
|
|
* @public
|
|
|
|
*/
|
|
|
|
clearValue() {
|
|
|
|
this.store.dispatch(clearAll());
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Disable
|
|
|
|
* @return {Object} Class instance
|
|
|
|
* @public
|
|
|
|
*/
|
|
|
|
disable() {
|
|
|
|
this.passedElement.disabled = true;
|
|
|
|
if(this.initialised) {
|
|
|
|
this.input.disabled = true;
|
2016-06-29 15:47:58 +02:00
|
|
|
this.containerOuter.classList.add(this.config.classNames.disabledState);
|
2016-06-08 15:45:29 +02:00
|
|
|
}
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Populate options via ajax callback
|
|
|
|
* @param {Function} fn Passed
|
|
|
|
* @return {Object} Class instance
|
|
|
|
* @public
|
|
|
|
*/
|
|
|
|
ajax(fn) {
|
2016-06-29 15:47:58 +02:00
|
|
|
if(this.initialised === true) {
|
|
|
|
if(this.passedElement.type === 'select-one' || this.passedElement.type === 'select-multiple') {
|
2016-06-30 14:57:56 +02:00
|
|
|
this.containerOuter.classList.add(this.config.classNames.loadingState);
|
2016-06-30 15:11:09 +02:00
|
|
|
if(this.passedElement.type === 'select-one') {
|
|
|
|
const placeholderItem = this._getTemplate('item', { id: -1, value: 'Loading', label: this.config.loadingText, active: true});
|
|
|
|
this.itemList.appendChild(placeholderItem);
|
|
|
|
}
|
2016-06-29 15:47:58 +02:00
|
|
|
const callback = (results, value, label) => {
|
|
|
|
if(!isType('Array', results) || !value) return;
|
|
|
|
|
|
|
|
if(results && results.length) {
|
2016-06-30 14:57:56 +02:00
|
|
|
this.containerOuter.classList.remove(this.config.classNames.loadingState);
|
2016-06-29 15:47:58 +02:00
|
|
|
results.forEach((result, index) => {
|
|
|
|
// Select first choice in list if single select input
|
|
|
|
if(index === 0 && this.passedElement.type === 'select-one') {
|
2016-06-30 23:08:40 +02:00
|
|
|
this._addChoice(true, false, result[value], result[label]);
|
2016-06-29 15:47:58 +02:00
|
|
|
} else {
|
|
|
|
this._addChoice(false, false, result[value], result[label]);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
}
|
|
|
|
};
|
|
|
|
fn(callback);
|
|
|
|
}
|
2016-06-28 15:27:25 +02:00
|
|
|
}
|
2016-06-08 15:45:29 +02:00
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set value of input to blank
|
|
|
|
* @return {Object} Class instance
|
|
|
|
* @public
|
|
|
|
*/
|
|
|
|
clearInput() {
|
|
|
|
if (this.input.value) this.input.value = '';
|
|
|
|
if(this.passedElement.type !== 'select-one') {
|
|
|
|
this.input.style.width = getWidthOfInput(this.input);
|
|
|
|
}
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
2016-07-31 21:02:46 +02:00
|
|
|
/**
|
|
|
|
* Call change callback
|
|
|
|
* @param {string} value - last added/deleted/selected value
|
|
|
|
* @return
|
|
|
|
* @private
|
|
|
|
*/
|
|
|
|
_triggerChange(value) {
|
|
|
|
if(!value) return;
|
|
|
|
// Run callback if it is a function
|
|
|
|
if(this.config.callbackOnChange){
|
|
|
|
const callback = this.config.callbackOnChange;
|
|
|
|
if(isType('Function', callback)) {
|
|
|
|
callback(value, this.passedElement);
|
|
|
|
} else {
|
|
|
|
console.error('callbackOnChange: Callback is not a function');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-04-04 22:44:32 +02:00
|
|
|
/**
|
2016-04-29 19:06:46 +02:00
|
|
|
* Process enter key event
|
|
|
|
* @param {Array} activeItems Items that are currently active
|
|
|
|
* @return
|
2016-06-08 15:45:29 +02:00
|
|
|
* @private
|
2016-04-04 22:44:32 +02:00
|
|
|
*/
|
2016-06-08 15:45:29 +02:00
|
|
|
_handleEnter(activeItems, value) {
|
2016-04-17 12:23:38 +02:00
|
|
|
let canUpdate = true;
|
|
|
|
|
2016-06-29 15:47:58 +02:00
|
|
|
if(this.config.addItems) {
|
|
|
|
if (this.config.maxItemCount && this.config.maxItemCount > 0 && this.config.maxItemCount <= this.itemList.children.length) {
|
2016-05-08 01:02:52 +02:00
|
|
|
// If there is a max entry limit and we have reached that limit
|
|
|
|
// don't update
|
2016-04-17 12:23:38 +02:00
|
|
|
canUpdate = false;
|
2016-06-29 15:47:58 +02:00
|
|
|
} else if(this.config.duplicateItems === false && this.passedElement.value) {
|
2016-05-08 01:02:52 +02:00
|
|
|
// If no duplicates are allowed, and the value already exists
|
|
|
|
// in the array, don't update
|
2016-06-30 14:57:56 +02:00
|
|
|
canUpdate = !activeItems.some((item) => item.value === value);
|
2016-05-08 01:02:52 +02:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
canUpdate = false;
|
|
|
|
}
|
2016-04-17 12:23:38 +02:00
|
|
|
|
2016-05-08 01:02:52 +02:00
|
|
|
if (canUpdate) {
|
|
|
|
let canAddItem = true;
|
2016-04-17 12:23:38 +02:00
|
|
|
|
2016-05-08 01:02:52 +02:00
|
|
|
// If a user has supplied a regular expression filter
|
2016-06-29 15:47:58 +02:00
|
|
|
if(this.config.regexFilter) {
|
2016-05-08 01:02:52 +02:00
|
|
|
// Determine whether we can update based on whether
|
|
|
|
// our regular expression passes
|
2016-06-08 15:45:29 +02:00
|
|
|
canAddItem = this._regexFilter(value);
|
2016-05-08 01:02:52 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// All is good, add
|
|
|
|
if(canAddItem) {
|
|
|
|
this.toggleDropdown();
|
2016-06-22 00:06:23 +02:00
|
|
|
this._addItem(value);
|
2016-07-31 21:02:46 +02:00
|
|
|
this._triggerChange(value);
|
2016-05-08 01:02:52 +02:00
|
|
|
this.clearInput(this.passedElement);
|
2016-04-17 12:23:38 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2016-04-29 19:06:46 +02:00
|
|
|
/**
|
|
|
|
* Process back space event
|
|
|
|
* @param {Array} Active items
|
|
|
|
* @return
|
2016-06-08 15:45:29 +02:00
|
|
|
* @private
|
2016-04-29 19:06:46 +02:00
|
|
|
*/
|
2016-06-08 15:45:29 +02:00
|
|
|
_handleBackspace(activeItems) {
|
2016-06-29 15:47:58 +02:00
|
|
|
if(this.config.removeItems && activeItems) {
|
2016-04-17 12:23:38 +02:00
|
|
|
const lastItem = activeItems[activeItems.length - 1];
|
2016-06-30 14:57:56 +02:00
|
|
|
const hasHighlightedItems = activeItems.some((item) => item.highlighted === true);
|
2016-04-17 12:23:38 +02:00
|
|
|
|
|
|
|
// If editing the last item is allowed and there are not other selected items,
|
|
|
|
// we can edit the item value. Otherwise if we can remove items, remove all selected items
|
2016-06-30 14:57:56 +02:00
|
|
|
if(this.config.editItems && !hasHighlightedItems && lastItem) {
|
2016-04-17 12:23:38 +02:00
|
|
|
this.input.value = lastItem.value;
|
2016-06-22 00:06:23 +02:00
|
|
|
this._removeItem(lastItem);
|
2016-07-31 21:02:46 +02:00
|
|
|
this._triggerChange(lastItem.value);
|
2016-04-17 12:23:38 +02:00
|
|
|
} else {
|
2016-06-30 14:57:56 +02:00
|
|
|
if(!hasHighlightedItems) { this.highlightItem(lastItem); }
|
|
|
|
this.removeHighlightedItems();
|
2016-04-17 12:23:38 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
2016-04-29 19:06:46 +02:00
|
|
|
/**
|
|
|
|
* Key down event
|
2016-04-04 22:44:32 +02:00
|
|
|
* @param {Object} e Event
|
|
|
|
* @return
|
|
|
|
*/
|
2016-06-08 15:45:29 +02:00
|
|
|
_onKeyDown(e) {
|
2016-04-29 19:23:06 +02:00
|
|
|
if(e.target !== this.input) return;
|
2016-05-07 13:36:50 +02:00
|
|
|
|
2016-04-04 00:07:10 +02:00
|
|
|
const ctrlDownKey = e.ctrlKey || e.metaKey;
|
2016-06-01 19:45:35 +02:00
|
|
|
const backKey = 46;
|
|
|
|
const deleteKey = 8;
|
|
|
|
const enterKey = 13;
|
|
|
|
const aKey = 65;
|
|
|
|
const escapeKey = 27;
|
|
|
|
const upKey = 38;
|
|
|
|
const downKey = 40;
|
|
|
|
|
|
|
|
const activeItems = this.store.getItemsFilteredByActive();
|
2016-06-27 15:57:33 +02:00
|
|
|
const activeChoices = this.store.getChoicesFilteredByActive();
|
2016-06-01 19:45:35 +02:00
|
|
|
|
|
|
|
const hasFocusedInput = this.input === document.activeElement;
|
2016-06-29 15:47:58 +02:00
|
|
|
const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState);
|
2016-06-01 19:45:35 +02:00
|
|
|
const hasItems = this.itemList && this.itemList.children;
|
2016-07-02 14:04:38 +02:00
|
|
|
const keyString = String.fromCharCode(e.keyCode);
|
2016-04-04 00:07:10 +02:00
|
|
|
|
2016-04-29 19:23:06 +02:00
|
|
|
// If a user is typing and the dropdown is not active
|
2016-05-07 14:30:07 +02:00
|
|
|
if(this.passedElement.type !== 'text' && /[a-zA-Z0-9-_ ]/.test(keyString) && !hasActiveDropdown) {
|
2016-05-08 01:02:52 +02:00
|
|
|
this.showDropdown();
|
2016-04-29 19:23:06 +02:00
|
|
|
}
|
2016-04-26 15:36:02 +02:00
|
|
|
|
2016-07-02 14:04:38 +02:00
|
|
|
this.canSearch = this.config.search;
|
2016-06-07 15:20:37 +02:00
|
|
|
|
2016-04-29 19:23:06 +02:00
|
|
|
switch (e.keyCode) {
|
|
|
|
case aKey:
|
|
|
|
// If CTRL + A or CMD + A have been pressed and there are items to select
|
|
|
|
if(ctrlDownKey && hasItems) {
|
2016-06-07 15:20:37 +02:00
|
|
|
this.canSearch = false;
|
2016-06-29 15:47:58 +02:00
|
|
|
if(this.config.removeItems && !this.input.value && this.input === document.activeElement) {
|
2016-06-22 00:06:23 +02:00
|
|
|
// Highlight items
|
|
|
|
this.highlightAll(this.itemList.children);
|
2016-04-29 16:18:53 +02:00
|
|
|
}
|
2016-04-29 19:23:06 +02:00
|
|
|
}
|
|
|
|
break;
|
2016-05-02 16:29:05 +02:00
|
|
|
|
2016-04-29 19:23:06 +02:00
|
|
|
case enterKey:
|
|
|
|
// If enter key is pressed and the input has a value
|
|
|
|
if(e.target.value && this.passedElement.type === 'text') {
|
|
|
|
const value = this.input.value;
|
2016-06-08 15:45:29 +02:00
|
|
|
this._handleEnter(activeItems, value);
|
2016-04-29 19:23:06 +02:00
|
|
|
}
|
|
|
|
|
2016-05-08 01:02:52 +02:00
|
|
|
if(hasActiveDropdown) {
|
2016-06-29 15:47:58 +02:00
|
|
|
const highlighted = this.dropdown.querySelector(`.${this.config.classNames.highlightedState}`);
|
2016-04-29 19:23:06 +02:00
|
|
|
|
|
|
|
if(highlighted) {
|
2016-05-02 16:29:05 +02:00
|
|
|
const value = highlighted.getAttribute('data-value');
|
2016-04-29 19:23:06 +02:00
|
|
|
const label = highlighted.innerHTML;
|
2016-06-01 19:45:35 +02:00
|
|
|
const id = highlighted.getAttribute('data-id');
|
2016-06-22 00:06:23 +02:00
|
|
|
this._addItem(value, label, id);
|
2016-07-31 21:02:46 +02:00
|
|
|
this._triggerChange(value);
|
2016-05-02 16:29:05 +02:00
|
|
|
this.clearInput(this.passedElement);
|
2016-06-02 15:37:06 +02:00
|
|
|
|
|
|
|
if(this.passedElement.type === 'select-one') {
|
|
|
|
this.isSearching = false;
|
2016-06-27 15:46:12 +02:00
|
|
|
this.store.dispatch(activateChoices());
|
2016-06-02 15:37:06 +02:00
|
|
|
this.toggleDropdown();
|
|
|
|
}
|
2016-04-29 16:18:53 +02:00
|
|
|
}
|
2016-04-29 19:23:06 +02:00
|
|
|
}
|
2016-06-30 14:57:56 +02:00
|
|
|
|
2016-04-29 19:23:06 +02:00
|
|
|
break;
|
2016-05-02 16:29:05 +02:00
|
|
|
|
2016-04-29 19:23:06 +02:00
|
|
|
case escapeKey:
|
2016-06-29 15:47:58 +02:00
|
|
|
if(hasActiveDropdown) this.toggleDropdown();
|
2016-04-29 19:23:06 +02:00
|
|
|
break;
|
2016-05-02 16:29:05 +02:00
|
|
|
|
2016-04-29 19:23:06 +02:00
|
|
|
case downKey:
|
|
|
|
case upKey:
|
|
|
|
// If up or down key is pressed, traverse through options
|
2016-05-08 01:02:52 +02:00
|
|
|
if(hasActiveDropdown) {
|
2016-06-29 15:47:58 +02:00
|
|
|
const currentEl = this.dropdown.querySelector(`.${this.config.classNames.highlightedState}`);
|
2016-05-02 22:39:33 +02:00
|
|
|
const directionInt = e.keyCode === downKey ? 1 : -1;
|
2016-05-02 14:22:53 +02:00
|
|
|
let nextEl;
|
2016-04-28 16:30:43 +02:00
|
|
|
|
2016-06-07 15:20:37 +02:00
|
|
|
this.canSearch = false;
|
|
|
|
|
2016-05-02 14:22:53 +02:00
|
|
|
if(currentEl) {
|
2016-05-02 22:39:33 +02:00
|
|
|
nextEl = getAdjacentEl(currentEl, '[data-option-selectable]', directionInt);
|
2016-05-02 14:22:53 +02:00
|
|
|
} else {
|
2016-05-02 16:29:05 +02:00
|
|
|
nextEl = this.dropdown.querySelector('[data-option-selectable]');
|
2016-04-29 19:23:06 +02:00
|
|
|
}
|
2016-05-02 14:22:53 +02:00
|
|
|
|
|
|
|
if(nextEl) {
|
2016-05-03 22:31:05 +02:00
|
|
|
// We prevent default to stop the cursor moving
|
|
|
|
// when pressing the arrow
|
2016-06-27 15:46:12 +02:00
|
|
|
if(!isScrolledIntoView(nextEl, this.choiceList, directionInt)) {
|
|
|
|
this._scrollToChoice(nextEl, directionInt);
|
2016-05-02 16:29:05 +02:00
|
|
|
}
|
2016-06-27 15:46:12 +02:00
|
|
|
this._highlightChoice(nextEl);
|
2016-04-28 16:30:43 +02:00
|
|
|
}
|
2016-06-07 15:20:37 +02:00
|
|
|
|
|
|
|
// Prevent default to maintain cursor position whilst
|
|
|
|
// traversing dropdown options
|
|
|
|
e.preventDefault();
|
2016-04-29 19:23:06 +02:00
|
|
|
}
|
|
|
|
break
|
2016-05-02 16:29:05 +02:00
|
|
|
|
2016-04-29 19:23:06 +02:00
|
|
|
case backKey:
|
|
|
|
case deleteKey:
|
|
|
|
// If backspace or delete key is pressed and the input has no value
|
2016-06-02 15:37:06 +02:00
|
|
|
if(hasFocusedInput && !e.target.value && this.passedElement.type !== 'select-one') {
|
2016-06-08 15:45:29 +02:00
|
|
|
this._handleBackspace(activeItems);
|
2016-04-29 19:23:06 +02:00
|
|
|
e.preventDefault();
|
|
|
|
}
|
|
|
|
break;
|
2016-05-02 16:29:05 +02:00
|
|
|
|
2016-04-29 19:23:06 +02:00
|
|
|
default:
|
|
|
|
break;
|
2016-03-15 23:42:10 +01:00
|
|
|
}
|
2016-03-17 16:00:22 +01:00
|
|
|
}
|
2016-03-15 23:42:10 +01:00
|
|
|
|
2016-04-29 19:06:46 +02:00
|
|
|
/**
|
|
|
|
* Key up event
|
|
|
|
* @param {Object} e Event
|
|
|
|
* @return
|
2016-06-08 15:45:29 +02:00
|
|
|
* @private
|
2016-04-29 19:06:46 +02:00
|
|
|
*/
|
2016-06-08 15:45:29 +02:00
|
|
|
_onKeyUp(e) {
|
2016-04-29 19:23:06 +02:00
|
|
|
if(e.target !== this.input) return;
|
2016-07-13 22:40:59 +02:00
|
|
|
const keyString = String.fromCharCode(e.keyCode);
|
2016-05-07 13:36:50 +02:00
|
|
|
|
2016-05-07 14:30:07 +02:00
|
|
|
// We are typing into a text input and have a value, we want to show a dropdown
|
|
|
|
// notice. Otherwise hide the dropdown
|
|
|
|
if(this.passedElement.type === 'text') {
|
2016-06-29 15:47:58 +02:00
|
|
|
const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState);
|
2016-05-07 15:14:05 +02:00
|
|
|
let dropdownItem;
|
2016-05-07 14:34:59 +02:00
|
|
|
if(this.input.value) {
|
2016-05-16 15:46:04 +02:00
|
|
|
const activeItems = this.store.getItemsFilteredByActive();
|
|
|
|
const isUnique = !activeItems.some((item) => item.value === this.input.value);
|
|
|
|
|
2016-06-29 15:47:58 +02:00
|
|
|
if (this.config.maxItemCount && this.config.maxItemCount > 0 && this.config.maxItemCount <= this.itemList.children.length) {
|
|
|
|
dropdownItem = this._getTemplate('notice', `Only ${ this.config.maxItemCount } options can be added.`);
|
|
|
|
} else if(!this.config.duplicateItems && !isUnique) {
|
2016-06-08 15:45:29 +02:00
|
|
|
dropdownItem = this._getTemplate('notice', `Only unique values can be added.`);
|
2016-05-07 15:14:05 +02:00
|
|
|
} else {
|
2016-06-08 15:45:29 +02:00
|
|
|
dropdownItem = this._getTemplate('notice', `Add "${ this.input.value }"`);
|
2016-05-07 15:14:05 +02:00
|
|
|
}
|
|
|
|
|
2016-06-29 15:47:58 +02:00
|
|
|
if((this.config.regexFilter && this._regexFilter(this.input.value)) || !this.config.regexFilter) {
|
2016-05-08 13:22:56 +02:00
|
|
|
this.dropdown.innerHTML = dropdownItem.outerHTML;
|
2016-06-29 15:47:58 +02:00
|
|
|
if(!this.dropdown.classList.contains(this.config.classNames.activeState)) {
|
2016-05-08 13:22:56 +02:00
|
|
|
this.showDropdown();
|
|
|
|
}
|
2016-05-07 14:30:07 +02:00
|
|
|
}
|
2016-05-08 13:22:56 +02:00
|
|
|
|
2016-05-07 14:30:07 +02:00
|
|
|
} else {
|
2016-06-29 15:47:58 +02:00
|
|
|
if(hasActiveDropdown) {
|
|
|
|
this.hideDropdown();
|
|
|
|
}
|
2016-05-07 14:30:07 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-06-07 15:20:37 +02:00
|
|
|
// If we have enabled text search
|
|
|
|
if(this.canSearch) {
|
2016-06-30 14:57:56 +02:00
|
|
|
// .. and our input is in focus
|
2016-04-29 19:23:06 +02:00
|
|
|
if(this.input === document.activeElement) {
|
2016-06-30 14:57:56 +02:00
|
|
|
const choices = this.store.getChoices();
|
|
|
|
const hasUnactiveChoices = choices.some((option) => option.active !== true);
|
2016-05-03 15:55:38 +02:00
|
|
|
|
2016-06-07 15:20:37 +02:00
|
|
|
// Check that we have a value to search and the input was an alphanumeric character
|
2016-07-13 22:40:59 +02:00
|
|
|
if(this.input.value && this.input.value.length > 1) {
|
2016-05-08 01:02:52 +02:00
|
|
|
const handleFilter = () => {
|
2016-07-31 19:38:00 +02:00
|
|
|
const newValue = isType('String', this.input.value) ? this.input.value.trim() : this.input.value;
|
|
|
|
const currentValue = isType('String', this.currentValue) ? this.currentValue.trim() : this.currentValue;
|
2016-05-04 15:31:29 +02:00
|
|
|
|
2016-06-07 15:20:37 +02:00
|
|
|
if(newValue.length >= 1 && newValue !== currentValue + ' ') {
|
2016-06-27 15:46:12 +02:00
|
|
|
const haystack = this.store.getChoicesFiltedBySelectable();
|
2016-06-07 15:20:37 +02:00
|
|
|
const needle = newValue;
|
2016-05-04 15:31:29 +02:00
|
|
|
const fuse = new Fuse(haystack, {
|
2016-05-03 22:31:05 +02:00
|
|
|
keys: ['label', 'value'],
|
|
|
|
shouldSort: true,
|
|
|
|
include: 'score',
|
|
|
|
});
|
2016-05-04 15:31:29 +02:00
|
|
|
const results = fuse.search(needle);
|
|
|
|
|
2016-06-07 15:20:37 +02:00
|
|
|
this.currentValue = newValue;
|
2016-05-16 15:46:04 +02:00
|
|
|
this.highlightPosition = 0;
|
2016-05-03 22:31:05 +02:00
|
|
|
this.isSearching = true;
|
2016-06-27 15:46:12 +02:00
|
|
|
this.store.dispatch(filterChoices(results));
|
2016-05-03 22:31:05 +02:00
|
|
|
}
|
2016-05-08 01:02:52 +02:00
|
|
|
};
|
2016-07-31 23:48:36 +02:00
|
|
|
|
2016-05-03 22:31:05 +02:00
|
|
|
handleFilter();
|
2016-06-27 15:57:33 +02:00
|
|
|
} else if(hasUnactiveChoices) {
|
2016-06-30 14:57:56 +02:00
|
|
|
// Otherwise reset choices to active
|
2016-05-03 22:31:05 +02:00
|
|
|
this.isSearching = false;
|
2016-06-29 15:47:58 +02:00
|
|
|
this.store.dispatch(activateChoices(true));
|
2016-04-22 20:45:50 +02:00
|
|
|
}
|
2016-04-29 19:23:06 +02:00
|
|
|
}
|
|
|
|
}
|
2016-05-04 10:02:22 +02:00
|
|
|
}
|
2016-04-22 20:45:50 +02:00
|
|
|
|
2016-06-07 15:20:37 +02:00
|
|
|
/**
|
|
|
|
* Input event
|
|
|
|
* @param {Object} e Event
|
|
|
|
* @return
|
2016-06-08 15:45:29 +02:00
|
|
|
* @private
|
2016-06-07 15:20:37 +02:00
|
|
|
*/
|
2016-06-08 15:45:29 +02:00
|
|
|
_onInput(e) {
|
2016-05-18 23:40:32 +02:00
|
|
|
if(this.passedElement.type !== 'select-one') {
|
2016-06-29 15:47:58 +02:00
|
|
|
this.input.style.width = getWidthOfInput(this.input);
|
2016-05-18 23:40:32 +02:00
|
|
|
}
|
2016-05-16 15:46:04 +02:00
|
|
|
}
|
|
|
|
|
2016-04-29 19:06:46 +02:00
|
|
|
/**
|
|
|
|
* Click event
|
2016-04-04 22:44:32 +02:00
|
|
|
* @param {Object} e Event
|
|
|
|
* @return
|
2016-06-08 15:45:29 +02:00
|
|
|
* @private
|
2016-04-04 22:44:32 +02:00
|
|
|
*/
|
2016-06-08 15:45:29 +02:00
|
|
|
_onMouseDown(e) {
|
2016-08-01 21:00:24 +02:00
|
|
|
const activeItems = this.store.getItemsFilteredByActive();
|
|
|
|
const target = e.target || e.touches[0].target;
|
2016-04-17 13:09:46 +02:00
|
|
|
|
2016-08-01 21:00:24 +02:00
|
|
|
// If click is affecting a child node of our element
|
|
|
|
if(this.containerOuter.contains(target)) {
|
2016-05-10 10:02:59 +02:00
|
|
|
|
2016-08-01 21:00:24 +02:00
|
|
|
// Prevent blur event triggering causing dropdown to close
|
|
|
|
// in a race condition
|
|
|
|
e.preventDefault();
|
2016-05-08 13:22:56 +02:00
|
|
|
|
2016-08-01 21:00:24 +02:00
|
|
|
const hasShiftKey = e.shiftKey ? true : false;
|
2016-05-08 13:22:56 +02:00
|
|
|
|
2016-08-01 21:00:24 +02:00
|
|
|
if(!this.dropdown.classList.contains(this.config.classNames.activeState)) {
|
|
|
|
if(this.passedElement.type !== 'text') {
|
|
|
|
// For select inputs we always want to show the dropdown if it isn't already showing
|
|
|
|
this.showDropdown();
|
|
|
|
}
|
|
|
|
|
|
|
|
// If input is not in focus, it ought to be
|
|
|
|
if(this.input !== document.activeElement) {
|
|
|
|
this.input.focus();
|
2016-06-01 19:56:08 +02:00
|
|
|
}
|
2016-08-01 21:00:24 +02:00
|
|
|
} else if(this.passedElement.type === 'select-one' && this.dropdown.classList.contains(this.config.classNames.activeState) && e.target === this.containerInner) {
|
|
|
|
this.hideDropdown();
|
|
|
|
}
|
2016-06-01 19:56:08 +02:00
|
|
|
|
2016-08-01 21:00:24 +02:00
|
|
|
if(target.hasAttribute('data-button')) {
|
|
|
|
// If we are clicking on a button
|
|
|
|
if(this.config.removeItems && this.config.removeItemButton) {
|
|
|
|
const itemId = target.parentNode.getAttribute('data-id');
|
|
|
|
const itemToRemove = activeItems.find((item) => item.id === parseInt(itemId));
|
2016-08-01 15:27:27 +02:00
|
|
|
|
2016-08-01 21:00:24 +02:00
|
|
|
// Remove item associated with button
|
|
|
|
this._removeItem(itemToRemove);
|
|
|
|
this._triggerChange(itemToRemove.value);
|
|
|
|
}
|
|
|
|
} else if(target.hasAttribute('data-item')) {
|
|
|
|
// If we are clicking on an item
|
|
|
|
if(this.config.removeItems) {
|
|
|
|
const passedId = target.getAttribute('data-id');
|
|
|
|
|
|
|
|
// We only want to select one item with a click
|
|
|
|
// so we deselect any items that aren't the target
|
|
|
|
// unless shift is being pressed
|
|
|
|
activeItems.forEach((item) => {
|
|
|
|
if(item.id === parseInt(passedId) && !item.highlighted) {
|
|
|
|
this.highlightItem(item);
|
|
|
|
} else if(!hasShiftKey) {
|
|
|
|
this.unhighlightItem(item);
|
2016-05-07 13:36:50 +02:00
|
|
|
}
|
2016-08-01 21:00:24 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
} else if(target.hasAttribute('data-option')) {
|
|
|
|
// If we are clicking on an option
|
|
|
|
const id = target.getAttribute('data-id');
|
|
|
|
const choices = this.store.getChoicesFilteredByActive();
|
|
|
|
const choice = choices.find((choice) => choice.id === parseInt(id));
|
|
|
|
|
|
|
|
if(!choice.selected && !choice.disabled) {
|
|
|
|
this._addItem(choice.value, choice.label, choice.id);
|
|
|
|
this._triggerChange(choice.value);
|
|
|
|
if(this.passedElement.type === 'select-one') {
|
|
|
|
this.input.value = "";
|
|
|
|
this.isSearching = false;
|
|
|
|
this.store.dispatch(activateChoices(true));
|
|
|
|
this.toggleDropdown();
|
2016-05-07 13:36:50 +02:00
|
|
|
}
|
2016-04-12 21:16:36 +02:00
|
|
|
}
|
2016-08-01 21:00:24 +02:00
|
|
|
}
|
2016-04-12 15:31:07 +02:00
|
|
|
|
2016-08-01 21:00:24 +02:00
|
|
|
} else {
|
|
|
|
// Click is outside of our element so close dropdown and de-select items
|
|
|
|
const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState);
|
|
|
|
const hasHighlightedItems = activeItems.some((item) => item.highlighted === true);
|
2016-04-29 19:06:46 +02:00
|
|
|
|
2016-08-01 21:00:24 +02:00
|
|
|
// De-select any highlighted items
|
|
|
|
if(hasHighlightedItems) {
|
|
|
|
this.unhighlightAll();
|
|
|
|
}
|
|
|
|
|
|
|
|
// Remove focus state
|
|
|
|
this.containerOuter.classList.remove(this.config.classNames.focusState);
|
2016-05-02 22:53:21 +02:00
|
|
|
|
2016-08-01 21:00:24 +02:00
|
|
|
// Close all other dropdowns
|
|
|
|
if(hasActiveDropdown) {
|
|
|
|
this.toggleDropdown();
|
2016-05-11 15:51:32 +02:00
|
|
|
}
|
2016-08-01 21:00:24 +02:00
|
|
|
}
|
2016-04-14 15:54:47 +02:00
|
|
|
}
|
|
|
|
|
2016-04-29 19:06:46 +02:00
|
|
|
/**
|
2016-06-08 15:45:29 +02:00
|
|
|
* Mouse over (hover) event
|
2016-04-29 19:06:46 +02:00
|
|
|
* @param {Object} e Event
|
|
|
|
* @return
|
2016-06-08 15:45:29 +02:00
|
|
|
* @private
|
2016-04-29 19:06:46 +02:00
|
|
|
*/
|
2016-06-08 15:45:29 +02:00
|
|
|
_onMouseOver(e) {
|
|
|
|
// If the dropdown is either the target or one of its children is the target
|
2016-06-29 15:47:58 +02:00
|
|
|
if((e.target === this.dropdown || findAncestor(e.target, this.config.classNames.listDropdown))) {
|
|
|
|
if(e.target.hasAttribute('data-option')) this._highlightChoice(e.target);
|
2016-04-14 15:54:47 +02:00
|
|
|
}
|
2016-04-09 12:29:56 +02:00
|
|
|
}
|
|
|
|
|
2016-05-02 13:23:12 +02:00
|
|
|
/**
|
2016-06-08 15:45:29 +02:00
|
|
|
* Paste event
|
2016-05-02 13:23:12 +02:00
|
|
|
* @param {Object} e Event
|
|
|
|
* @return
|
2016-06-08 15:45:29 +02:00
|
|
|
* @private
|
2016-05-02 13:23:12 +02:00
|
|
|
*/
|
2016-06-08 15:45:29 +02:00
|
|
|
_onPaste(e) {
|
|
|
|
if(e.target !== this.input) return;
|
|
|
|
// Disable pasting into the input if option has been set
|
2016-06-29 15:47:58 +02:00
|
|
|
if(!this.config.paste) {
|
|
|
|
e.preventDefault();
|
2016-05-02 13:23:12 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-06-08 15:45:29 +02:00
|
|
|
|
2016-04-29 19:06:46 +02:00
|
|
|
/**
|
|
|
|
* Focus event
|
|
|
|
* @param {Object} e Event
|
|
|
|
* @return
|
2016-06-08 15:45:29 +02:00
|
|
|
* @private
|
2016-04-29 19:06:46 +02:00
|
|
|
*/
|
2016-06-08 15:45:29 +02:00
|
|
|
_onFocus(e) {
|
2016-06-29 15:47:58 +02:00
|
|
|
const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState);
|
2016-05-08 01:02:52 +02:00
|
|
|
if(e.target === this.input && !hasActiveDropdown) {
|
2016-06-29 15:47:58 +02:00
|
|
|
this.containerOuter.classList.add(this.config.classNames.focusState);
|
2016-05-08 01:02:52 +02:00
|
|
|
if(this.passedElement.type === 'select-one' || this.passedElement.type === 'select-multiple'){
|
2016-07-31 21:02:46 +02:00
|
|
|
this.showDropdown();
|
|
|
|
}
|
|
|
|
} else if(this.passedElement.type === 'select-one' && e.target === this.containerOuter && !hasActiveDropdown) {
|
|
|
|
this.containerOuter.classList.add(this.config.classNames.focusState);
|
|
|
|
this.showDropdown();
|
2016-07-31 23:48:36 +02:00
|
|
|
if(this.canSearch) {
|
2016-07-31 21:02:46 +02:00
|
|
|
this.input.focus();
|
2016-05-08 01:02:52 +02:00
|
|
|
}
|
2016-05-02 22:53:21 +02:00
|
|
|
}
|
2016-04-21 15:42:57 +02:00
|
|
|
}
|
|
|
|
|
2016-04-29 19:06:46 +02:00
|
|
|
/**
|
|
|
|
* Blur event
|
|
|
|
* @param {Object} e Event
|
|
|
|
* @return
|
2016-06-08 15:45:29 +02:00
|
|
|
* @private
|
2016-04-29 19:06:46 +02:00
|
|
|
*/
|
2016-06-08 15:45:29 +02:00
|
|
|
_onBlur(e) {
|
2016-06-29 15:47:58 +02:00
|
|
|
const hasActiveDropdown = this.dropdown.classList.contains(this.config.classNames.activeState);
|
2016-07-31 21:02:46 +02:00
|
|
|
|
|
|
|
// If the blurred element is this input
|
|
|
|
if(e.target === this.input) {
|
|
|
|
// Remove the focus state
|
2016-06-29 15:47:58 +02:00
|
|
|
this.containerOuter.classList.remove(this.config.classNames.focusState);
|
2016-07-31 21:02:46 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Close the dropdown if there is one
|
|
|
|
if(hasActiveDropdown) {
|
2016-05-08 01:02:52 +02:00
|
|
|
this.hideDropdown();
|
2016-05-02 22:53:21 +02:00
|
|
|
}
|
2016-04-21 15:42:57 +02:00
|
|
|
}
|
|
|
|
|
2016-03-15 23:42:10 +01:00
|
|
|
|
2016-04-04 22:44:32 +02:00
|
|
|
/**
|
|
|
|
* Tests value against a regular expression
|
2016-05-02 22:39:33 +02:00
|
|
|
* @param {string} value Value to test
|
|
|
|
* @return {Boolean} Whether test passed/failed
|
2016-06-08 15:45:29 +02:00
|
|
|
* @private
|
2016-04-04 22:44:32 +02:00
|
|
|
*/
|
2016-06-08 15:45:29 +02:00
|
|
|
_regexFilter(value) {
|
2016-05-10 10:02:59 +02:00
|
|
|
if(!value) return;
|
2016-06-29 15:47:58 +02:00
|
|
|
const expression = new RegExp(this.config.regexFilter, 'i');
|
2016-05-08 01:02:52 +02:00
|
|
|
return expression.test(value);
|
2016-03-24 15:42:03 +01:00
|
|
|
}
|
|
|
|
|
2016-05-02 22:39:33 +02:00
|
|
|
/**
|
|
|
|
* Scroll to an option element
|
|
|
|
* @param {HTMLElement} option Option to scroll to
|
|
|
|
* @param {Number} direction Whether option is above or below
|
|
|
|
* @return
|
2016-06-08 15:45:29 +02:00
|
|
|
* @private
|
2016-05-02 22:39:33 +02:00
|
|
|
*/
|
2016-06-30 14:57:56 +02:00
|
|
|
_scrollToChoice(choice, direction) {
|
|
|
|
if(!choice) return;
|
2016-05-03 22:31:05 +02:00
|
|
|
|
2016-06-27 15:46:12 +02:00
|
|
|
const dropdownHeight = this.choiceList.offsetHeight;
|
2016-06-30 14:57:56 +02:00
|
|
|
const choiceHeight = choice.offsetHeight;
|
2016-06-02 15:37:06 +02:00
|
|
|
|
2016-05-02 22:39:33 +02:00
|
|
|
// Distance from bottom of element to top of parent
|
2016-06-30 14:57:56 +02:00
|
|
|
const choicePos = choice.offsetTop + choiceHeight;
|
2016-06-01 19:45:35 +02:00
|
|
|
|
2016-05-03 22:31:05 +02:00
|
|
|
// Scroll position of dropdown
|
2016-06-27 15:46:12 +02:00
|
|
|
const containerScrollPos = this.choiceList.scrollTop + dropdownHeight;
|
2016-06-01 19:45:35 +02:00
|
|
|
|
2016-06-30 14:57:56 +02:00
|
|
|
// Difference between the choice and scroll position
|
|
|
|
let endPoint = direction > 0 ? ((this.choiceList.scrollTop + choicePos) - containerScrollPos) : choice.offsetTop;
|
2016-05-05 22:31:09 +02:00
|
|
|
|
|
|
|
const animateScroll = (time, endPoint, direction) => {
|
|
|
|
let continueAnimation = false;
|
|
|
|
let easing, distance;
|
2016-06-08 15:45:29 +02:00
|
|
|
const strength = 4;
|
2016-03-22 15:36:01 +01:00
|
|
|
|
2016-06-08 15:45:29 +02:00
|
|
|
if(direction > 0) {
|
2016-06-27 15:46:12 +02:00
|
|
|
easing = (endPoint - this.choiceList.scrollTop)/strength;
|
2016-06-08 15:45:29 +02:00
|
|
|
distance = easing > 1 ? easing : 1;
|
2016-04-17 13:02:28 +02:00
|
|
|
|
2016-06-27 15:46:12 +02:00
|
|
|
this.choiceList.scrollTop = this.choiceList.scrollTop + distance;
|
|
|
|
if(this.choiceList.scrollTop < endPoint) {
|
2016-06-08 15:45:29 +02:00
|
|
|
continueAnimation = true;
|
|
|
|
}
|
|
|
|
} else {
|
2016-06-27 15:46:12 +02:00
|
|
|
easing = (this.choiceList.scrollTop - endPoint)/strength;
|
2016-06-08 15:45:29 +02:00
|
|
|
distance = easing > 1 ? easing : 1;
|
2016-04-17 13:02:28 +02:00
|
|
|
|
2016-06-27 15:46:12 +02:00
|
|
|
this.choiceList.scrollTop = this.choiceList.scrollTop - distance;
|
|
|
|
if(this.choiceList.scrollTop > endPoint) {
|
2016-06-08 15:45:29 +02:00
|
|
|
continueAnimation = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if(continueAnimation) {
|
|
|
|
requestAnimationFrame((time) => {
|
|
|
|
animateScroll(time, endPoint, direction);
|
|
|
|
});
|
2016-04-17 13:02:28 +02:00
|
|
|
}
|
2016-06-08 15:45:29 +02:00
|
|
|
};
|
|
|
|
|
|
|
|
requestAnimationFrame((time) => {
|
|
|
|
animateScroll(time, endPoint, direction);
|
2016-04-17 13:02:28 +02:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2016-04-04 22:44:32 +02:00
|
|
|
/**
|
2016-06-30 14:57:56 +02:00
|
|
|
* Highlight choice
|
2016-06-08 15:45:29 +02:00
|
|
|
* @param {HTMLElement} el Element to highlight
|
2016-04-04 22:44:32 +02:00
|
|
|
* @return
|
2016-06-08 15:45:29 +02:00
|
|
|
* @private
|
2016-04-04 22:44:32 +02:00
|
|
|
*/
|
2016-06-27 15:46:12 +02:00
|
|
|
_highlightChoice(el) {
|
2016-06-08 15:45:29 +02:00
|
|
|
// Highlight first element in dropdown
|
2016-06-30 14:57:56 +02:00
|
|
|
const choices = Array.from(this.dropdown.querySelectorAll('[data-option-selectable]'));
|
2016-03-21 19:53:26 +01:00
|
|
|
|
2016-06-30 14:57:56 +02:00
|
|
|
if(choices && choices.length) {
|
|
|
|
const highlightedChoices = Array.from(this.dropdown.querySelectorAll(`.${this.config.classNames.highlightedState}`));
|
2016-06-08 15:45:29 +02:00
|
|
|
|
2016-06-30 14:57:56 +02:00
|
|
|
// Remove any highlighted choices
|
|
|
|
highlightedChoices.forEach((el) => {
|
2016-06-29 15:47:58 +02:00
|
|
|
el.classList.remove(this.config.classNames.highlightedState);
|
2016-06-08 15:45:29 +02:00
|
|
|
});
|
2016-04-17 12:23:38 +02:00
|
|
|
|
2016-06-08 15:45:29 +02:00
|
|
|
if(el){
|
|
|
|
// Highlight given option
|
2016-06-29 15:47:58 +02:00
|
|
|
el.classList.add(this.config.classNames.highlightedState);
|
2016-06-30 14:57:56 +02:00
|
|
|
this.highlightPosition = choices.indexOf(el);
|
2016-06-08 15:45:29 +02:00
|
|
|
} else {
|
|
|
|
// Highlight option based on last known highlight location
|
|
|
|
let el;
|
2016-04-17 12:23:38 +02:00
|
|
|
|
2016-06-30 14:57:56 +02:00
|
|
|
if(choices.length > this.highlightPosition) {
|
2016-06-08 15:45:29 +02:00
|
|
|
// If we have an option to highlight
|
2016-06-30 14:57:56 +02:00
|
|
|
el = choices[this.highlightPosition];
|
2016-06-08 15:45:29 +02:00
|
|
|
} else {
|
|
|
|
// Otherwise highlight the option before
|
2016-06-30 14:57:56 +02:00
|
|
|
el = choices[choices.length - 1];
|
2016-06-08 15:45:29 +02:00
|
|
|
}
|
|
|
|
|
2016-06-30 14:57:56 +02:00
|
|
|
if(!el) el = choices[0];
|
2016-06-29 15:47:58 +02:00
|
|
|
el.classList.add(this.config.classNames.highlightedState);
|
2016-06-08 15:45:29 +02:00
|
|
|
}
|
|
|
|
}
|
2016-03-21 19:53:26 +01:00
|
|
|
}
|
2016-04-09 12:29:56 +02:00
|
|
|
|
2016-06-22 00:06:23 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Add item to store with correct value
|
|
|
|
* @param {String} value Value to add to store
|
|
|
|
* @return {Object} Class instance
|
|
|
|
* @public
|
|
|
|
*/
|
2016-07-13 22:40:59 +02:00
|
|
|
_addItem(value, label, choiceId = -1) {
|
2016-06-22 00:06:23 +02:00
|
|
|
const items = this.store.getItems();
|
2016-07-31 19:38:00 +02:00
|
|
|
let passedValue = isType('String', value) ? value.trim() : value;
|
2016-06-22 00:06:23 +02:00
|
|
|
let passedLabel = label || passedValue;
|
2016-07-13 22:40:59 +02:00
|
|
|
let passedOptionId = parseInt(choiceId) || -1;
|
2016-06-22 00:06:23 +02:00
|
|
|
|
|
|
|
// If a prepended value has been passed, prepend it
|
2016-06-29 15:47:58 +02:00
|
|
|
if(this.config.prependValue) {
|
|
|
|
passedValue = this.config.prependValue + passedValue.toString();
|
2016-06-22 00:06:23 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// If an appended value has been passed, append it
|
2016-06-29 15:47:58 +02:00
|
|
|
if(this.config.appendValue) {
|
|
|
|
passedValue = passedValue + this.config.appendValue.toString();
|
2016-06-22 00:06:23 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Generate unique id
|
|
|
|
const id = items ? items.length + 1 : 1;
|
|
|
|
|
|
|
|
this.store.dispatch(addItem(passedValue, passedLabel, id, passedOptionId));
|
|
|
|
|
|
|
|
if(this.passedElement.type === 'select-one') {
|
|
|
|
this.removeActiveItems(id);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Run callback if it is a function
|
2016-07-13 22:40:59 +02:00
|
|
|
if(this.config.callbackOnAddItem){
|
|
|
|
const callback = this.config.callbackOnAddItem;
|
2016-06-22 00:06:23 +02:00
|
|
|
if(isType('Function', callback)) {
|
|
|
|
callback(id, passedValue, this.passedElement);
|
|
|
|
} else {
|
|
|
|
console.error('callbackOnAddItem: Callback is not a function');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove item from store
|
|
|
|
* @param
|
|
|
|
* @return {Object} Class instance
|
|
|
|
* @public
|
|
|
|
*/
|
2016-06-29 15:47:58 +02:00
|
|
|
_removeItem(item, callback = this.config.callbackOnRemoveItem) {
|
2016-06-22 00:06:23 +02:00
|
|
|
if(!item || !isType('Object', item)) {
|
|
|
|
console.error('removeItem: No item object was passed to be removed');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const id = item.id;
|
|
|
|
const value = item.value;
|
2016-06-27 15:46:12 +02:00
|
|
|
const choiceId = item.choiceId;
|
2016-06-22 00:06:23 +02:00
|
|
|
|
2016-06-27 15:46:12 +02:00
|
|
|
this.store.dispatch(removeItem(id, choiceId));
|
2016-06-22 00:06:23 +02:00
|
|
|
|
|
|
|
// Run callback
|
|
|
|
if(callback){
|
|
|
|
if(!isType('Function', callback)) console.error('callbackOnRemoveItem: Callback is not a function'); return;
|
|
|
|
callback(id, value, this.passedElement);
|
|
|
|
}
|
|
|
|
|
|
|
|
return this;
|
|
|
|
}
|
|
|
|
|
2016-05-08 01:02:52 +02:00
|
|
|
/**
|
2016-06-27 15:46:12 +02:00
|
|
|
* Add choice to dropdoww
|
2016-05-08 01:02:52 +02:00
|
|
|
* @return
|
2016-06-08 15:45:29 +02:00
|
|
|
* @private
|
2016-05-08 01:02:52 +02:00
|
|
|
*/
|
2016-06-27 15:46:12 +02:00
|
|
|
_addChoice(isSelected, isDisabled, value, label, groupId = -1) {
|
2016-05-08 13:22:56 +02:00
|
|
|
if(!value) return
|
|
|
|
|
|
|
|
if(!label) { label = value; }
|
|
|
|
|
2016-05-08 01:02:52 +02:00
|
|
|
// Generate unique id
|
2016-06-27 15:46:12 +02:00
|
|
|
const choices = this.store.getChoices();
|
|
|
|
const id = choices ? choices.length + 1 : 1;
|
2016-05-08 01:02:52 +02:00
|
|
|
|
2016-06-27 15:46:12 +02:00
|
|
|
this.store.dispatch(addChoice(value, label, id, groupId, isDisabled));
|
2016-05-08 01:02:52 +02:00
|
|
|
|
2016-06-08 15:45:29 +02:00
|
|
|
if(isSelected && !isDisabled) {
|
2016-06-22 00:06:23 +02:00
|
|
|
this._addItem(value, label, id);
|
2016-05-08 01:02:52 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Add group to dropdown
|
|
|
|
* @param {Object} group Group to add
|
|
|
|
* @param {Number} index Whether this is the first group to add
|
2016-06-08 15:45:29 +02:00
|
|
|
* @return
|
|
|
|
* @private
|
2016-05-08 01:02:52 +02:00
|
|
|
*/
|
2016-06-08 15:45:29 +02:00
|
|
|
_addGroup(group, id, isFirst) {
|
2016-06-30 14:57:56 +02:00
|
|
|
const groupChoices = Array.from(group.getElementsByTagName('OPTION'));
|
2016-06-01 19:45:35 +02:00
|
|
|
const groupId = id;
|
2016-05-08 01:02:52 +02:00
|
|
|
|
2016-06-30 14:57:56 +02:00
|
|
|
if(groupChoices) {
|
2016-05-08 01:02:52 +02:00
|
|
|
this.store.dispatch(addGroup(group.label, groupId, true, group.disabled));
|
2016-06-30 14:57:56 +02:00
|
|
|
groupChoices.forEach((option, optionIndex) => {
|
2016-06-08 15:45:29 +02:00
|
|
|
const isDisabled = option.disabled || option.parentNode.disabled;
|
2016-06-27 15:46:12 +02:00
|
|
|
this._addChoice(option.selected, isDisabled, option.value, option.innerHTML, groupId);
|
2016-05-08 01:02:52 +02:00
|
|
|
});
|
|
|
|
} else {
|
|
|
|
this.store.dispatch(addGroup(group.label, group.id, false, group.disabled));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get template from name
|
|
|
|
* @param {String} template Name of template to get
|
|
|
|
* @param {...} args Data to pass to template
|
|
|
|
* @return {HTMLElement} Template
|
2016-06-08 15:45:29 +02:00
|
|
|
* @private
|
2016-05-08 01:02:52 +02:00
|
|
|
*/
|
2016-06-08 15:45:29 +02:00
|
|
|
_getTemplate(template, ...args) {
|
2016-05-04 15:31:29 +02:00
|
|
|
if(!template) return;
|
2016-06-29 15:47:58 +02:00
|
|
|
const templates = this.config.templates;
|
2016-05-04 15:31:29 +02:00
|
|
|
return templates[template](...args);
|
2016-04-16 18:06:27 +02:00
|
|
|
}
|
2016-04-22 20:45:50 +02:00
|
|
|
|
2016-04-29 19:06:46 +02:00
|
|
|
/**
|
2016-05-04 15:31:29 +02:00
|
|
|
* Create HTML element based on type and arguments
|
2016-05-08 01:02:52 +02:00
|
|
|
* @return
|
2016-06-08 15:45:29 +02:00
|
|
|
* @private
|
2016-04-29 19:06:46 +02:00
|
|
|
*/
|
2016-06-08 15:45:29 +02:00
|
|
|
_createTemplates() {
|
2016-06-29 15:47:58 +02:00
|
|
|
const classNames = this.config.classNames;
|
2016-05-04 15:31:29 +02:00
|
|
|
const templates = {
|
|
|
|
containerOuter: () => {
|
2016-07-31 21:02:46 +02:00
|
|
|
if(this.passedElement.type === 'select-one') {
|
|
|
|
return strToEl(`
|
|
|
|
<div class="${ classNames.containerOuter }" data-type="${ this.passedElement.type }" tabindex="0"></div>
|
|
|
|
`);
|
|
|
|
} else {
|
|
|
|
return strToEl(`
|
|
|
|
<div class="${ classNames.containerOuter }" data-type="${ this.passedElement.type }"></div>
|
|
|
|
`);
|
|
|
|
}
|
2016-05-04 15:31:29 +02:00
|
|
|
},
|
|
|
|
containerInner: () => {
|
|
|
|
return strToEl(`<div class="${ classNames.containerInner }"></div>`);
|
|
|
|
},
|
2016-05-18 23:40:32 +02:00
|
|
|
itemList: () => {
|
2016-05-07 13:36:50 +02:00
|
|
|
return strToEl(`<div class="${ classNames.list } ${ this.passedElement.type === 'select-one' ? classNames.listSingle : classNames.listItems }"></div>`);
|
2016-05-04 15:31:29 +02:00
|
|
|
},
|
|
|
|
item: (data) => {
|
2016-06-29 15:47:58 +02:00
|
|
|
if(this.config.removeItemButton && this.passedElement.type !== 'select-one') {
|
2016-05-11 15:51:32 +02:00
|
|
|
return strToEl(`
|
2016-06-30 14:57:56 +02:00
|
|
|
<div class="${ classNames.item } ${ data.highlighted ? classNames.highlightedState : ''} ${ !data.disabled ? classNames.itemSelectable : '' }" data-item data-id="${ data.id }" data-value="${ data.value }" data-deletable>
|
2016-05-11 15:51:32 +02:00
|
|
|
${ data.label }
|
|
|
|
<button class="${ classNames.button }" data-button>Remove item</button>
|
|
|
|
</div>
|
|
|
|
`);
|
|
|
|
} else {
|
|
|
|
return strToEl(`
|
2016-06-30 14:57:56 +02:00
|
|
|
<div class="${ classNames.item } ${ data.highlighted ? classNames.highlightedState : classNames.itemSelectable }" data-item data-id="${ data.id }" data-value="${ data.value }">
|
2016-05-11 15:51:32 +02:00
|
|
|
${ data.label }
|
|
|
|
</div>
|
|
|
|
`);
|
|
|
|
}
|
2016-05-04 15:31:29 +02:00
|
|
|
},
|
2016-06-30 14:57:56 +02:00
|
|
|
choiceList: () => {
|
|
|
|
return strToEl(`<div class="${ classNames.list }"></div>`);
|
|
|
|
},
|
|
|
|
choiceGroup: (data) => {
|
|
|
|
return strToEl(`
|
|
|
|
<div class="${ classNames.group } ${ data.disabled ? classNames.itemDisabled : '' }" data-group data-id="${ data.id }" data-value="${ data.value }">
|
|
|
|
<div class="${ classNames.groupHeading }">${ data.value }</div>
|
|
|
|
</div>
|
|
|
|
`);
|
|
|
|
},
|
|
|
|
choice: (data) => {
|
|
|
|
return strToEl(`
|
|
|
|
<div class="${ classNames.item } ${ classNames.itemChoice } ${ data.disabled ? classNames.itemDisabled : classNames.itemSelectable }" data-option ${ data.disabled ? 'data-option-disabled' : 'data-option-selectable' } data-id="${ data.id }" data-value="${ data.value }">
|
|
|
|
${ data.label }
|
|
|
|
</div>
|
|
|
|
`);
|
|
|
|
},
|
|
|
|
input: () => {
|
|
|
|
return strToEl(`<input type="text" class="${ classNames.input } ${ classNames.inputCloned }">`);
|
|
|
|
},
|
|
|
|
dropdown: () => {
|
|
|
|
return strToEl(`<div class="${ classNames.list } ${ classNames.listDropdown }"></div>`);
|
|
|
|
},
|
|
|
|
notice: (label, clickable) => {
|
|
|
|
return strToEl(`<div class="${ classNames.item } ${ classNames.itemChoice }">${ label }</div>`);
|
|
|
|
},
|
|
|
|
option: (data) => {
|
2016-07-31 19:38:00 +02:00
|
|
|
return strToEl(`<option value="${ data.value }" selected>${ data.label }</option>`);
|
2016-06-30 14:57:56 +02:00
|
|
|
},
|
2016-05-04 15:31:29 +02:00
|
|
|
};
|
2016-04-22 20:45:50 +02:00
|
|
|
|
2016-06-30 15:11:09 +02:00
|
|
|
// this.config.templates = extend(this.config.templates, templates);
|
|
|
|
this.config.templates = templates;
|
2016-04-22 20:45:50 +02:00
|
|
|
}
|
2016-03-21 19:53:26 +01:00
|
|
|
|
2016-04-08 23:33:13 +02:00
|
|
|
/**
|
|
|
|
* Create DOM structure around passed select element
|
|
|
|
* @return
|
2016-06-08 15:45:29 +02:00
|
|
|
* @private
|
2016-04-08 23:33:13 +02:00
|
|
|
*/
|
2016-06-08 15:45:29 +02:00
|
|
|
_createInput() {
|
|
|
|
const containerOuter = this._getTemplate('containerOuter');
|
|
|
|
const containerInner = this._getTemplate('containerInner');
|
|
|
|
const itemList = this._getTemplate('itemList');
|
2016-06-27 15:46:12 +02:00
|
|
|
const choiceList = this._getTemplate('choiceList');
|
2016-06-08 15:45:29 +02:00
|
|
|
const input = this._getTemplate('input');
|
|
|
|
const dropdown = this._getTemplate('dropdown');
|
2016-03-16 15:41:13 +01:00
|
|
|
|
2016-05-08 01:02:52 +02:00
|
|
|
this.containerOuter = containerOuter;
|
|
|
|
this.containerInner = containerInner;
|
2016-06-01 19:45:35 +02:00
|
|
|
this.input = input;
|
2016-06-27 15:46:12 +02:00
|
|
|
this.choiceList = choiceList;
|
2016-06-01 19:45:35 +02:00
|
|
|
this.itemList = itemList;
|
|
|
|
this.dropdown = dropdown;
|
2016-05-08 01:02:52 +02:00
|
|
|
|
2016-03-17 16:00:22 +01:00
|
|
|
// Hide passed input
|
2016-06-29 15:47:58 +02:00
|
|
|
this.passedElement.classList.add(this.config.classNames.input, this.config.classNames.hiddenState);
|
2016-04-10 22:23:42 +02:00
|
|
|
this.passedElement.tabIndex = '-1';
|
|
|
|
this.passedElement.setAttribute('style', 'display:none;');
|
|
|
|
this.passedElement.setAttribute('aria-hidden', 'true');
|
2016-06-30 14:57:56 +02:00
|
|
|
this.passedElement.setAttribute('data-choice', 'active');
|
2016-03-16 21:24:11 +01:00
|
|
|
|
2016-03-18 00:10:16 +01:00
|
|
|
// Wrap input in container preserving DOM ordering
|
2016-04-10 22:23:42 +02:00
|
|
|
wrap(this.passedElement, containerInner);
|
2016-03-18 13:26:38 +01:00
|
|
|
|
2016-03-18 12:05:50 +01:00
|
|
|
// Wrapper inner container with outer container
|
2016-03-18 00:10:16 +01:00
|
|
|
wrap(containerInner, containerOuter);
|
2016-04-29 18:11:20 +02:00
|
|
|
|
2016-04-15 10:19:02 +02:00
|
|
|
// If placeholder has been enabled and we have a value
|
2016-06-29 15:47:58 +02:00
|
|
|
if (this.config.placeholder && (this.config.placeholderValue || this.passedElement.placeholder)) {
|
|
|
|
const placeholder = this.config.placeholderValue || this.passedElement.placeholder;
|
2016-06-02 15:37:06 +02:00
|
|
|
input.placeholder = placeholder;
|
2016-05-08 13:22:56 +02:00
|
|
|
if(this.passedElement.type !== 'select-one') {
|
|
|
|
input.style.width = getWidthOfInput(input);
|
|
|
|
}
|
2016-03-18 12:05:50 +01:00
|
|
|
}
|
2016-03-18 13:26:38 +01:00
|
|
|
|
2016-06-29 15:47:58 +02:00
|
|
|
if(!this.config.addItems) this.disable();
|
2016-03-24 15:42:03 +01:00
|
|
|
|
2016-04-04 23:52:49 +02:00
|
|
|
containerOuter.appendChild(containerInner);
|
2016-05-07 14:30:07 +02:00
|
|
|
containerOuter.appendChild(dropdown);
|
2016-05-18 23:40:32 +02:00
|
|
|
containerInner.appendChild(itemList);
|
2016-06-27 15:46:12 +02:00
|
|
|
dropdown.appendChild(choiceList);
|
2016-05-18 23:40:32 +02:00
|
|
|
|
|
|
|
if(this.passedElement.type === 'select-multiple' || this.passedElement.type === 'text') {
|
|
|
|
containerInner.appendChild(input);
|
2016-07-02 14:04:38 +02:00
|
|
|
} else if(this.config.search) {
|
2016-05-18 23:40:32 +02:00
|
|
|
dropdown.insertBefore(input, dropdown.firstChild);
|
|
|
|
}
|
2016-04-29 18:11:20 +02:00
|
|
|
|
2016-05-07 13:36:50 +02:00
|
|
|
if(this.passedElement.type === 'select-multiple' || this.passedElement.type === 'select-one') {
|
2016-05-07 14:30:07 +02:00
|
|
|
const passedGroups = Array.from(this.passedElement.getElementsByTagName('OPTGROUP'));
|
2016-07-02 14:04:38 +02:00
|
|
|
|
|
|
|
this.highlightPosition = 0;
|
2016-05-03 22:31:05 +02:00
|
|
|
this.isSearching = false;
|
2016-07-02 14:04:38 +02:00
|
|
|
|
2016-05-08 13:22:56 +02:00
|
|
|
if(passedGroups && passedGroups.length) {
|
2016-04-29 18:11:20 +02:00
|
|
|
passedGroups.forEach((group, index) => {
|
2016-04-29 19:06:46 +02:00
|
|
|
const isFirst = index === 0 ? true : false;
|
2016-06-08 15:45:29 +02:00
|
|
|
this._addGroup(group, index, isFirst);
|
2016-04-29 18:11:20 +02:00
|
|
|
});
|
|
|
|
} else {
|
|
|
|
const passedOptions = Array.from(this.passedElement.options);
|
2016-07-30 17:03:20 +02:00
|
|
|
let allChoices = [];
|
2016-07-30 16:12:22 +02:00
|
|
|
|
|
|
|
// Create array of options from option elements
|
|
|
|
passedOptions.forEach((o) => {
|
2016-07-30 17:03:20 +02:00
|
|
|
allChoices.push({
|
2016-07-30 16:12:22 +02:00
|
|
|
value: o.value,
|
|
|
|
label: o.innerHTML,
|
|
|
|
selected: o.selected,
|
|
|
|
disabled: o.disabled || o.parentNode.disabled
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2016-07-31 23:48:36 +02:00
|
|
|
// Join choices with preset choices and add them
|
2016-07-30 17:03:20 +02:00
|
|
|
allChoices
|
|
|
|
.concat(this.presetChoices)
|
2016-07-31 22:05:17 +02:00
|
|
|
.forEach((o, index) => {
|
|
|
|
if(index === 0) {
|
|
|
|
this._addChoice(true, o.disabled ? o.disabled : false, o.value, o.label);
|
|
|
|
} else {
|
|
|
|
this._addChoice(o.selected ? o.selected : false, o.disabled ? o.disabled : false, o.value, o.label);
|
|
|
|
}
|
2016-07-30 17:03:20 +02:00
|
|
|
});
|
2016-04-29 18:11:20 +02:00
|
|
|
}
|
|
|
|
} else if(this.passedElement.type === 'text') {
|
|
|
|
// Add any preset values seperated by delimiter
|
2016-06-01 19:45:35 +02:00
|
|
|
this.presetItems.forEach((item) => {
|
|
|
|
if(isType('Object', item)) {
|
|
|
|
if(!item.value) return;
|
2016-06-22 00:06:23 +02:00
|
|
|
this._addItem(item.value, item.label, item.id);
|
2016-06-01 19:45:35 +02:00
|
|
|
} else if(isType('String', item)) {
|
2016-06-22 00:06:23 +02:00
|
|
|
this._addItem(item);
|
2016-06-01 19:45:35 +02:00
|
|
|
}
|
2016-04-29 18:11:20 +02:00
|
|
|
});
|
|
|
|
}
|
2016-04-14 15:54:47 +02:00
|
|
|
}
|
|
|
|
|
2016-06-08 15:45:29 +02:00
|
|
|
/**
|
2016-06-30 14:57:56 +02:00
|
|
|
* Render group choices into a DOM fragment and append to choice list
|
2016-06-08 15:45:29 +02:00
|
|
|
* @param {Array} groups Groups to add to list
|
2016-06-30 14:57:56 +02:00
|
|
|
* @param {Array} choices Choices to add to groups
|
2016-06-08 15:45:29 +02:00
|
|
|
* @param {DocumentFragment} fragment Fragment to add groups and options to (optional)
|
|
|
|
* @return {DocumentFragment} Populated options fragment
|
|
|
|
* @private
|
|
|
|
*/
|
2016-06-30 14:57:56 +02:00
|
|
|
renderGroups(groups, choices, fragment) {
|
2016-05-10 10:02:59 +02:00
|
|
|
const groupFragment = fragment || document.createDocumentFragment();
|
|
|
|
|
2016-07-31 23:48:36 +02:00
|
|
|
groups.forEach((group, i) => {
|
|
|
|
// Grab options that are children of this group
|
|
|
|
const groupChoices = choices.filter((choice) => {
|
|
|
|
if(this.passedElement.type === 'select-one') {
|
|
|
|
return choice.groupId === group.id
|
|
|
|
} else {
|
|
|
|
return choice.groupId === group.id && !choice.selected;
|
|
|
|
}
|
|
|
|
});
|
2016-04-22 20:45:50 +02:00
|
|
|
|
2016-07-31 23:48:36 +02:00
|
|
|
if(groupChoices.length >= 1) {
|
|
|
|
const dropdownGroup = this._getTemplate('choiceGroup', group);
|
2016-04-25 15:59:58 +02:00
|
|
|
|
2016-07-31 23:48:36 +02:00
|
|
|
groupFragment.appendChild(dropdownGroup);
|
2016-04-29 16:18:53 +02:00
|
|
|
|
2016-07-31 23:48:36 +02:00
|
|
|
this.renderChoices(groupChoices, groupFragment);
|
|
|
|
}
|
|
|
|
});
|
2016-05-18 23:40:32 +02:00
|
|
|
|
|
|
|
return groupFragment;
|
2016-05-04 15:31:29 +02:00
|
|
|
}
|
2016-04-04 22:44:32 +02:00
|
|
|
|
2016-06-08 15:45:29 +02:00
|
|
|
/**
|
2016-06-30 14:57:56 +02:00
|
|
|
* Render choices into a DOM fragment and append to choice list
|
|
|
|
* @param {Array} choices Choices to add to list
|
|
|
|
* @param {DocumentFragment} fragment Fragment to add choices to (optional)
|
|
|
|
* @return {DocumentFragment} Populated choices fragment
|
2016-06-08 15:45:29 +02:00
|
|
|
* @private
|
|
|
|
*/
|
2016-06-28 15:27:25 +02:00
|
|
|
renderChoices(choices, fragment) {
|
2016-05-10 10:02:59 +02:00
|
|
|
// Create a fragment to store our list items (so we don't have to update the DOM for each item)
|
2016-06-28 15:27:25 +02:00
|
|
|
const choicesFragment = fragment || document.createDocumentFragment();
|
2016-05-10 10:02:59 +02:00
|
|
|
|
2016-07-31 23:48:36 +02:00
|
|
|
choices.forEach((choice, i) => {
|
|
|
|
const dropdownItem = this._getTemplate('choice', choice);
|
2016-05-10 10:02:59 +02:00
|
|
|
|
2016-07-31 23:48:36 +02:00
|
|
|
if(this.passedElement.type === 'select-one') {
|
|
|
|
choicesFragment.appendChild(dropdownItem);
|
|
|
|
} else if(!choice.selected) {
|
|
|
|
choicesFragment.appendChild(dropdownItem);
|
|
|
|
}
|
|
|
|
});
|
2016-05-18 23:40:32 +02:00
|
|
|
|
2016-06-28 15:27:25 +02:00
|
|
|
return choicesFragment;
|
2016-05-04 15:31:29 +02:00
|
|
|
}
|
2016-04-13 15:40:41 +02:00
|
|
|
|
2016-06-08 15:45:29 +02:00
|
|
|
/**
|
|
|
|
* Render items into a DOM fragment and append to items list
|
|
|
|
* @param {Array} items Items to add to list
|
|
|
|
* @param {DocumentFragment} fragment Fragrment to add items to (optional)
|
|
|
|
* @return
|
|
|
|
* @private
|
|
|
|
*/
|
2016-06-22 00:06:23 +02:00
|
|
|
renderItems(items, fragment) {
|
2016-06-01 19:45:35 +02:00
|
|
|
// Create fragment to add elements to
|
2016-05-10 10:02:59 +02:00
|
|
|
const itemListFragment = fragment || document.createDocumentFragment();
|
2016-05-04 15:31:29 +02:00
|
|
|
// Simplify store data to just values
|
2016-06-02 15:37:06 +02:00
|
|
|
const itemsFiltered = this.store.getItemsReducedToValues(items);
|
2016-04-04 15:43:22 +02:00
|
|
|
|
2016-05-11 15:25:34 +02:00
|
|
|
if(this.passedElement.type === 'text') {
|
|
|
|
// Assign hidden input array of values
|
2016-06-29 15:47:58 +02:00
|
|
|
this.passedElement.setAttribute('value', itemsFiltered.join(this.config.delimiter));
|
2016-05-11 15:25:34 +02:00
|
|
|
} else {
|
|
|
|
const selectedOptionsFragment = document.createDocumentFragment();
|
|
|
|
|
|
|
|
// Add each list item to list
|
|
|
|
items.forEach((item) => {
|
2016-05-11 15:29:26 +02:00
|
|
|
// Create a standard select option
|
2016-06-30 14:57:56 +02:00
|
|
|
const option = this._getTemplate('option', item);
|
2016-05-11 15:25:34 +02:00
|
|
|
|
2016-05-11 15:29:26 +02:00
|
|
|
// Append it to fragment
|
2016-05-11 15:25:34 +02:00
|
|
|
selectedOptionsFragment.appendChild(option);
|
|
|
|
});
|
|
|
|
|
2016-06-30 14:57:56 +02:00
|
|
|
// Update selected choices
|
2016-05-11 15:25:34 +02:00
|
|
|
this.passedElement.innerHTML = "";
|
|
|
|
this.passedElement.appendChild(selectedOptionsFragment);
|
|
|
|
}
|
2016-04-04 15:43:22 +02:00
|
|
|
|
2016-05-04 15:31:29 +02:00
|
|
|
// Add each list item to list
|
|
|
|
items.forEach((item) => {
|
|
|
|
// Create new list element
|
2016-06-08 15:45:29 +02:00
|
|
|
const listItem = this._getTemplate('item', item);
|
2016-04-13 15:40:41 +02:00
|
|
|
|
2016-05-04 15:31:29 +02:00
|
|
|
// Append it to list
|
2016-05-10 10:02:59 +02:00
|
|
|
itemListFragment.appendChild(listItem);
|
2016-05-04 15:31:29 +02:00
|
|
|
});
|
2016-04-04 15:43:22 +02:00
|
|
|
|
2016-06-08 15:45:29 +02:00
|
|
|
return itemListFragment;
|
2016-05-02 16:29:05 +02:00
|
|
|
}
|
|
|
|
|
2016-05-02 13:23:12 +02:00
|
|
|
/**
|
2016-05-04 15:31:29 +02:00
|
|
|
* Render DOM with values
|
|
|
|
* @return
|
2016-06-08 15:45:29 +02:00
|
|
|
* @private
|
2016-05-02 13:23:12 +02:00
|
|
|
*/
|
2016-06-22 00:06:23 +02:00
|
|
|
render() {
|
2016-05-04 15:31:29 +02:00
|
|
|
this.currentState = this.store.getState();
|
2016-06-27 15:57:33 +02:00
|
|
|
|
2016-06-22 00:06:23 +02:00
|
|
|
// Only render if our state has actually changed
|
2016-05-04 15:31:29 +02:00
|
|
|
if(this.currentState !== this.prevState) {
|
2016-06-30 23:25:00 +02:00
|
|
|
|
|
|
|
// Choices
|
2016-06-28 15:27:25 +02:00
|
|
|
if((this.currentState.choices !== this.prevState.choices || this.currentState.groups !== this.prevState.groups)) {
|
2016-05-07 13:36:50 +02:00
|
|
|
if(this.passedElement.type === 'select-multiple' || this.passedElement.type === 'select-one') {
|
2016-06-30 14:57:56 +02:00
|
|
|
// Get active groups/choices
|
2016-06-01 19:45:35 +02:00
|
|
|
const activeGroups = this.store.getGroupsFilteredByActive();
|
2016-06-27 15:57:33 +02:00
|
|
|
const activeChoices = this.store.getChoicesFilteredByActive();
|
2016-06-08 15:45:29 +02:00
|
|
|
|
2016-06-27 15:57:33 +02:00
|
|
|
let choiceListFragment = document.createDocumentFragment();
|
2016-05-07 13:36:50 +02:00
|
|
|
|
2016-06-30 14:57:56 +02:00
|
|
|
// Clear choices
|
2016-06-27 15:46:12 +02:00
|
|
|
this.choiceList.innerHTML = '';
|
2016-07-31 23:48:36 +02:00
|
|
|
// Scroll back to top of choices list
|
|
|
|
this.choiceList.scrollTop = 0;
|
2016-05-07 13:36:50 +02:00
|
|
|
|
|
|
|
// If we have grouped options
|
|
|
|
if(activeGroups.length >= 1 && this.isSearching !== true) {
|
2016-06-27 15:57:33 +02:00
|
|
|
choiceListFragment = this.renderGroups(activeGroups, activeChoices, choiceListFragment);
|
|
|
|
} else if(activeChoices.length >= 1) {
|
2016-06-28 15:27:25 +02:00
|
|
|
choiceListFragment = this.renderChoices(activeChoices, choiceListFragment);
|
2016-05-07 13:36:50 +02:00
|
|
|
}
|
2016-07-31 21:02:46 +02:00
|
|
|
|
|
|
|
if(choiceListFragment.childNodes && choiceListFragment.childNodes.length > 0) {
|
2016-05-10 10:02:59 +02:00
|
|
|
// If we actually have anything to add to our dropdown
|
2016-06-30 14:57:56 +02:00
|
|
|
// append it and highlight the first choice
|
2016-06-27 15:57:33 +02:00
|
|
|
this.choiceList.appendChild(choiceListFragment);
|
2016-06-27 15:46:12 +02:00
|
|
|
this._highlightChoice();
|
2016-05-07 13:36:50 +02:00
|
|
|
} else {
|
2016-05-10 10:02:59 +02:00
|
|
|
// Otherwise show a notice
|
2016-06-27 15:57:33 +02:00
|
|
|
const dropdownItem = this.isSearching ? this._getTemplate('notice', 'No results found') : this._getTemplate('notice', 'No choices to choose from');
|
2016-06-27 15:46:12 +02:00
|
|
|
this.choiceList.appendChild(dropdownItem);
|
2016-05-07 13:36:50 +02:00
|
|
|
}
|
2016-05-04 15:31:29 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-05-11 15:29:26 +02:00
|
|
|
// Items
|
2016-05-04 15:31:29 +02:00
|
|
|
if(this.currentState.items !== this.prevState.items) {
|
|
|
|
const activeItems = this.store.getItemsFilteredByActive();
|
|
|
|
if(activeItems) {
|
|
|
|
// Create a fragment to store our list items (so we don't have to update the DOM for each item)
|
2016-06-22 00:06:23 +02:00
|
|
|
const itemListFragment = this.renderItems(activeItems);
|
2016-06-08 15:45:29 +02:00
|
|
|
|
|
|
|
// Clear list
|
|
|
|
this.itemList.innerHTML = '';
|
|
|
|
|
|
|
|
// If we have items to add
|
2016-07-02 14:50:24 +02:00
|
|
|
if(itemListFragment.children && itemListFragment.children.length) {
|
2016-06-08 15:45:29 +02:00
|
|
|
// Update list
|
|
|
|
this.itemList.appendChild(itemListFragment);
|
|
|
|
}
|
2016-05-04 15:31:29 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.prevState = this.currentState;
|
|
|
|
}
|
2016-05-02 13:23:12 +02:00
|
|
|
}
|
|
|
|
|
2016-04-04 22:44:32 +02:00
|
|
|
/**
|
2016-04-29 18:11:20 +02:00
|
|
|
* Trigger event listeners
|
2016-04-29 19:06:46 +02:00
|
|
|
* @return
|
2016-06-08 15:45:29 +02:00
|
|
|
* @private
|
2016-04-04 22:44:32 +02:00
|
|
|
*/
|
2016-06-08 15:45:29 +02:00
|
|
|
_addEventListeners() {
|
|
|
|
document.addEventListener('keyup', this._onKeyUp);
|
|
|
|
document.addEventListener('keydown', this._onKeyDown);
|
|
|
|
document.addEventListener('mousedown', this._onMouseDown);
|
|
|
|
document.addEventListener('mouseover', this._onMouseOver);
|
|
|
|
|
2016-07-31 21:02:46 +02:00
|
|
|
if(this.passedElement.type && this.passedElement.type === 'select-one') {
|
|
|
|
this.containerOuter.addEventListener('focus', this._onFocus);
|
|
|
|
}
|
|
|
|
|
2016-06-08 15:45:29 +02:00
|
|
|
this.input.addEventListener('input', this._onInput);
|
|
|
|
this.input.addEventListener('paste', this._onPaste);
|
|
|
|
this.input.addEventListener('focus', this._onFocus);
|
|
|
|
this.input.addEventListener('blur', this._onBlur);
|
2016-04-29 18:11:20 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Destroy event listeners
|
2016-04-29 19:06:46 +02:00
|
|
|
* @return
|
2016-06-08 15:45:29 +02:00
|
|
|
* @private
|
2016-04-29 18:11:20 +02:00
|
|
|
*/
|
2016-06-08 15:45:29 +02:00
|
|
|
_removeEventListeners() {
|
|
|
|
document.removeEventListener('keyup', this._onKeyUp);
|
|
|
|
document.removeEventListener('keydown', this._onKeyDown);
|
|
|
|
document.removeEventListener('mousedown', this._onMouseDown);
|
|
|
|
document.removeEventListener('mouseover', this._onMouseOver);
|
2016-07-31 21:02:46 +02:00
|
|
|
|
|
|
|
if(this.passedElement.type && this.passedElement.type === 'select-one') {
|
|
|
|
this.containerOuter.removeEventListener('focus', this._onFocus);
|
|
|
|
}
|
2016-05-08 13:22:56 +02:00
|
|
|
|
2016-06-08 15:45:29 +02:00
|
|
|
this.input.removeEventListener('input', this._onInput);
|
|
|
|
this.input.removeEventListener('paste', this._onPaste);
|
|
|
|
this.input.removeEventListener('focus', this._onFocus);
|
|
|
|
this.input.removeEventListener('blur', this._onBlur);
|
2016-03-17 16:00:22 +01:00
|
|
|
}
|
|
|
|
};
|
2016-03-15 23:42:10 +01:00
|
|
|
|
2016-04-25 19:00:30 +02:00
|
|
|
window.Choices = module.exports = Choices;
|