2016-03-17 16:00:22 +01:00
|
|
|
'use strict';
|
|
|
|
|
2016-03-30 16:04:21 +02:00
|
|
|
import { createStore } from 'redux';
|
2016-03-31 15:51:41 +02:00
|
|
|
import choices from './reducers/index.js';
|
2016-04-04 22:44:32 +02:00
|
|
|
import { addItemToStore, removeItemFromStore, selectItemFromStore } from './actions/index';
|
|
|
|
import { hasClass, wrap, getSiblings, isType, strToEl, extend } from './lib/utils.js';
|
2016-03-18 12:05:50 +01:00
|
|
|
|
2016-03-18 13:26:38 +01:00
|
|
|
export class Choices {
|
2016-03-17 16:00:22 +01:00
|
|
|
constructor(options) {
|
2016-04-04 00:07:10 +02:00
|
|
|
const fakeEl = document.createElement("fakeel");
|
|
|
|
const userOptions = options || {};
|
|
|
|
const defaultOptions = {
|
2016-03-17 16:00:22 +01:00
|
|
|
element: document.querySelector('[data-choice]'),
|
|
|
|
disabled: false,
|
2016-04-07 14:57:57 +02:00
|
|
|
items: [],
|
2016-03-24 15:42:03 +01:00
|
|
|
addItems: true,
|
2016-03-22 23:47:29 +01:00
|
|
|
removeItems: true,
|
2016-03-21 19:53:26 +01:00
|
|
|
editItems: false,
|
2016-03-18 12:05:50 +01:00
|
|
|
maxItems: false,
|
|
|
|
delimiter: ',',
|
|
|
|
allowDuplicates: true,
|
2016-03-24 15:42:03 +01:00
|
|
|
regexFilter: false,
|
2016-03-18 00:10:16 +01:00
|
|
|
debug: false,
|
2016-03-17 16:00:22 +01:00
|
|
|
placeholder: false,
|
2016-03-24 15:42:03 +01:00
|
|
|
prependValue: false,
|
|
|
|
appendValue: false,
|
2016-04-04 00:07:10 +02:00
|
|
|
selectAll: true,
|
2016-03-18 13:26:38 +01:00
|
|
|
callbackOnInit: function() {},
|
|
|
|
callbackOnRender: function() {},
|
2016-03-24 00:00:32 +01:00
|
|
|
callbackOnRemoveItem: function() {},
|
|
|
|
callbackOnAddItem: function() {}
|
2016-03-17 16:00:22 +01:00
|
|
|
};
|
2016-03-15 23:42:10 +01:00
|
|
|
|
2016-04-04 22:44:32 +02:00
|
|
|
// Initial instance state
|
|
|
|
this.initialised = false;
|
|
|
|
|
2016-03-17 16:00:22 +01:00
|
|
|
// Merge options with user options
|
2016-04-04 22:44:32 +02:00
|
|
|
this.options = extend(defaultOptions, userOptions || {});
|
|
|
|
|
|
|
|
// Create data store
|
2016-04-04 15:43:22 +02:00
|
|
|
this.store = createStore(choices);
|
2016-03-30 16:04:21 +02:00
|
|
|
|
2016-04-04 22:44:32 +02:00
|
|
|
// Cutting the mustard
|
2016-04-04 00:07:10 +02:00
|
|
|
this.supports = 'querySelector' in document && 'addEventListener' in document && 'classList' in fakeEl;
|
2016-03-17 16:00:22 +01:00
|
|
|
|
2016-04-04 00:07:10 +02:00
|
|
|
// Retrieve triggering element (i.e. element with 'data-choice' trigger)
|
2016-04-08 23:33:13 +02:00
|
|
|
this.passedInput = this.options.element;
|
2016-03-31 15:51:41 +02:00
|
|
|
|
2016-04-07 14:57:57 +02:00
|
|
|
// Set preset items
|
|
|
|
this.presetItems = [];
|
|
|
|
if(this.options.items.length) {
|
|
|
|
this.presetItems = this.options.items;
|
2016-04-08 23:33:13 +02:00
|
|
|
} else if(this.passedInput.value !== '') {
|
|
|
|
this.presetItems = this.passedInput.value.split(this.options.delimiter);
|
2016-04-07 14:57:57 +02:00
|
|
|
}
|
|
|
|
|
2016-03-17 16:00:22 +01:00
|
|
|
// Bind methods
|
2016-04-08 10:07:41 +02:00
|
|
|
this.init = this.init.bind(this);
|
|
|
|
this.render = this.render.bind(this);
|
|
|
|
this.destroy = this.destroy.bind(this);
|
2016-03-17 16:00:22 +01:00
|
|
|
this.onKeyDown = this.onKeyDown.bind(this);
|
2016-04-04 00:07:10 +02:00
|
|
|
this.onClick = this.onClick.bind(this);
|
2016-04-08 23:33:13 +02:00
|
|
|
this.onFocus = this.onFocus.bind(this);
|
2016-04-09 12:29:56 +02:00
|
|
|
this.onBlur = this.onBlur.bind(this);
|
2016-03-18 13:26:38 +01:00
|
|
|
|
2016-04-04 22:44:32 +02:00
|
|
|
// Let's have it large
|
2016-03-18 13:26:38 +01:00
|
|
|
this.init();
|
2016-03-17 16:00:22 +01:00
|
|
|
}
|
2016-03-16 10:03:59 +01:00
|
|
|
|
2016-04-04 22:44:32 +02:00
|
|
|
/* State tests */
|
2016-03-18 12:13:35 +01:00
|
|
|
|
2016-04-04 22:44:32 +02:00
|
|
|
/**
|
|
|
|
* Whether input is disabled
|
|
|
|
* @return {Boolean}
|
2016-03-17 16:00:22 +01:00
|
|
|
*/
|
|
|
|
isDisabled() {
|
2016-04-04 22:44:32 +02:00
|
|
|
return (this.input.disabled) ? true : false;
|
2016-03-17 16:00:22 +01:00
|
|
|
}
|
2016-03-15 23:42:10 +01:00
|
|
|
|
2016-04-04 22:44:32 +02:00
|
|
|
/**
|
|
|
|
* Whether there are no values
|
|
|
|
* @return {Boolean}
|
|
|
|
*/
|
2016-03-17 16:00:22 +01:00
|
|
|
isEmpty() {
|
2016-04-04 22:44:32 +02:00
|
|
|
return (this.store.getState().length === 0) ? true : false;
|
2016-03-17 16:00:22 +01:00
|
|
|
}
|
2016-03-17 00:15:03 +01:00
|
|
|
|
2016-03-17 16:00:22 +01:00
|
|
|
/* Event handling */
|
|
|
|
|
2016-04-04 22:44:32 +02:00
|
|
|
/**
|
|
|
|
* Handle keydown event
|
|
|
|
* @param {Object} e Event
|
|
|
|
* @return
|
|
|
|
*/
|
2016-03-17 16:00:22 +01:00
|
|
|
onKeyDown(e) {
|
2016-04-04 22:44:32 +02:00
|
|
|
const storeValues = this.store.getState();
|
2016-04-04 00:07:10 +02:00
|
|
|
const ctrlDownKey = e.ctrlKey || e.metaKey;
|
|
|
|
const deleteKey = 8 || 46;
|
|
|
|
const enterKey = 13;
|
|
|
|
const aKey = 65;
|
|
|
|
|
|
|
|
// If we are typing in the input
|
|
|
|
if(e.target === this.input) {
|
2016-04-04 22:44:32 +02:00
|
|
|
|
2016-04-04 00:07:10 +02:00
|
|
|
// If CTRL + A or CMD + A have been pressed and there are items to select
|
|
|
|
if (ctrlDownKey && e.keyCode === aKey && this.list && this.list.children) {
|
|
|
|
let handleSelectAll = () => {
|
2016-04-08 23:33:13 +02:00
|
|
|
if(this.options.removeItems && !this.input.value && this.options.selectAll && this.input === document.activeElement) {
|
2016-04-04 00:07:10 +02:00
|
|
|
this.selectAll(this.list.children);
|
|
|
|
}
|
|
|
|
};
|
2016-03-18 00:10:16 +01:00
|
|
|
|
2016-04-04 00:07:10 +02:00
|
|
|
handleSelectAll();
|
|
|
|
}
|
2016-03-17 16:00:22 +01:00
|
|
|
|
2016-04-04 00:07:10 +02:00
|
|
|
// If enter key is pressed and the input has a value
|
|
|
|
if (e.keyCode === enterKey && e.target.value) {
|
|
|
|
let value = this.input.value;
|
2016-03-17 16:00:22 +01:00
|
|
|
|
2016-04-04 00:07:10 +02:00
|
|
|
let handleEnter = () => {
|
|
|
|
let canUpdate = true;
|
2016-03-18 00:10:16 +01:00
|
|
|
|
2016-04-04 00:07:10 +02:00
|
|
|
// If there is a max entry limit and we have reached that limit
|
|
|
|
// don't update
|
|
|
|
if (this.options.maxItems && this.options.maxItems <= this.list.children.length) {
|
2016-03-18 00:10:16 +01:00
|
|
|
canUpdate = false;
|
|
|
|
}
|
2016-03-24 15:42:03 +01:00
|
|
|
|
2016-04-04 00:07:10 +02:00
|
|
|
// If no duplicates are allowed, and the value already exists
|
|
|
|
// in the array, don't update
|
2016-04-08 23:33:13 +02:00
|
|
|
if (this.options.allowDuplicates === false && this.passedInput.value) {
|
2016-04-04 22:44:32 +02:00
|
|
|
canUpdate = !storeValues.some((item) => {
|
|
|
|
return item.value === value;
|
|
|
|
});
|
2016-04-04 00:07:10 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// All is good, update
|
|
|
|
if (canUpdate) {
|
2016-04-08 23:33:13 +02:00
|
|
|
if(this.passedInput.type === 'text') {
|
2016-04-04 00:07:10 +02:00
|
|
|
let canAddItem = true;
|
|
|
|
|
|
|
|
// If a user has supplied a regular expression filter
|
|
|
|
if(this.options.regexFilter) {
|
|
|
|
// Determine whether we can update based on whether
|
|
|
|
// our regular expression passes
|
|
|
|
canAddItem = this.regexFilter(value);
|
|
|
|
}
|
|
|
|
|
|
|
|
// All is good, add
|
|
|
|
if(canAddItem) {
|
2016-04-04 15:43:22 +02:00
|
|
|
this.addItem(value);
|
2016-04-08 23:33:13 +02:00
|
|
|
this.clearInput(this.passedInput);
|
2016-04-04 00:07:10 +02:00
|
|
|
}
|
2016-03-24 15:42:03 +01:00
|
|
|
}
|
2016-03-21 20:27:52 +01:00
|
|
|
}
|
2016-04-04 00:07:10 +02:00
|
|
|
};
|
2016-03-18 00:10:16 +01:00
|
|
|
|
2016-04-04 00:07:10 +02:00
|
|
|
handleEnter();
|
|
|
|
}
|
2016-03-15 23:42:10 +01:00
|
|
|
}
|
|
|
|
|
2016-03-22 23:47:29 +01:00
|
|
|
// If backspace or delete key is pressed and the input has no value
|
2016-04-04 00:07:10 +02:00
|
|
|
if (e.keyCode === deleteKey && !e.target.value) {
|
2016-03-18 13:26:38 +01:00
|
|
|
|
2016-03-17 16:00:22 +01:00
|
|
|
let handleBackspaceKey = () => {
|
2016-03-22 23:47:29 +01:00
|
|
|
if(this.options.removeItems) {
|
2016-04-08 23:33:13 +02:00
|
|
|
let currentListItems = this.list.querySelectorAll('[data-choice-item]');
|
2016-03-22 23:47:29 +01:00
|
|
|
let selectedItems = this.list.querySelectorAll('.is-selected');
|
|
|
|
let lastItem = currentListItems[currentListItems.length - 1];
|
2016-04-08 23:33:13 +02:00
|
|
|
let inputIsFocussed = this.input === document.activeElement;
|
2016-03-22 23:47:29 +01:00
|
|
|
|
2016-04-08 23:33:13 +02:00
|
|
|
if(lastItem && !this.options.editItems && inputIsFocussed) {
|
2016-04-04 00:07:10 +02:00
|
|
|
this.selectItem(lastItem);
|
2016-03-22 23:47:29 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
// If editing the last item is allowed and there is a last item and
|
|
|
|
// there are not other selected items (minus the last item), we can edit
|
|
|
|
// the item value. Otherwise if we can remove items, remove all items
|
2016-04-08 23:33:13 +02:00
|
|
|
|
|
|
|
if(this.options.editItems && lastItem && selectedItems.length === 0 && inputIsFocussed) {
|
2016-03-22 23:47:29 +01:00
|
|
|
this.input.value = lastItem.innerHTML;
|
|
|
|
this.removeItem(lastItem);
|
|
|
|
} else {
|
|
|
|
this.removeAll(currentListItems);
|
|
|
|
}
|
2016-03-21 19:53:26 +01:00
|
|
|
}
|
2016-03-17 16:00:22 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
handleBackspaceKey();
|
2016-03-17 00:15:03 +01:00
|
|
|
|
2016-03-17 16:00:22 +01:00
|
|
|
e.preventDefault();
|
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-04 22:44:32 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Handle click event
|
|
|
|
* @param {Object} e Event
|
|
|
|
* @return
|
|
|
|
*/
|
2016-03-17 16:00:22 +01:00
|
|
|
onClick(e) {
|
2016-04-08 23:33:13 +02:00
|
|
|
// I don't like the look of this
|
|
|
|
if(e.target.hasAttribute('data-choice-item')) {
|
2016-04-04 00:07:10 +02:00
|
|
|
let item = e.target;
|
2016-03-17 00:15:03 +01:00
|
|
|
|
2016-04-04 00:07:10 +02:00
|
|
|
let handleClick = (item) => {
|
|
|
|
let passedId = item.getAttribute('data-choice-id');
|
|
|
|
let items = this.list.children;
|
2016-03-15 23:42:10 +01:00
|
|
|
|
2016-04-04 00:07:10 +02:00
|
|
|
// We only want to select one item with a click
|
2016-04-04 22:44:32 +02:00
|
|
|
// so we deselect any items that aren't the target
|
2016-04-04 00:07:10 +02:00
|
|
|
for (var i = 0; i < items.length; i++) {
|
|
|
|
let singleItem = items[i];
|
|
|
|
let id = singleItem.getAttribute('data-choice-id');;
|
2016-03-15 23:42:10 +01:00
|
|
|
|
2016-04-04 00:07:10 +02:00
|
|
|
if(id === passedId && !singleItem.classList.contains('is-selected')) {
|
|
|
|
this.selectItem(singleItem);
|
|
|
|
} else {
|
2016-04-04 22:44:32 +02:00
|
|
|
this.deselectItem(singleItem);
|
2016-04-04 00:07:10 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
handleClick(item);
|
|
|
|
}
|
2016-03-17 16:00:22 +01:00
|
|
|
}
|
2016-03-15 23:42:10 +01:00
|
|
|
|
2016-04-08 23:33:13 +02:00
|
|
|
onFocus(e) {
|
2016-04-09 12:29:56 +02:00
|
|
|
if(this.dropdown) {
|
|
|
|
this.toggleDropdown();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
onBlur(e) {
|
|
|
|
if(this.dropdown) {
|
|
|
|
this.toggleDropdown();
|
2016-04-08 23:33:13 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-03-17 16:00:22 +01:00
|
|
|
/* Methods */
|
2016-03-15 23:42:10 +01:00
|
|
|
|
2016-04-04 22:44:32 +02:00
|
|
|
/**
|
|
|
|
* Set value of input to blank
|
|
|
|
* @return
|
|
|
|
*/
|
|
|
|
clearInput() {
|
|
|
|
if (this.input.value) this.input.value = '';
|
|
|
|
}
|
2016-03-15 23:42:10 +01:00
|
|
|
|
2016-04-04 22:44:32 +02:00
|
|
|
/**
|
|
|
|
* Tests value against a regular expression
|
|
|
|
* @param {string} value Value to test
|
|
|
|
* @return {Boolean} Whether test passed/failed
|
|
|
|
*/
|
2016-03-24 15:42:03 +01:00
|
|
|
regexFilter(value) {
|
|
|
|
let expression = new RegExp(this.options.regexFilter, 'i');
|
|
|
|
let passesTest = expression.test(value);
|
|
|
|
|
|
|
|
return passesTest;
|
|
|
|
}
|
|
|
|
|
2016-04-08 23:33:13 +02:00
|
|
|
/**
|
|
|
|
* Get Element based on a given value
|
|
|
|
* @param {String} value Value to search for
|
|
|
|
* @return {Element} First Element with given value
|
|
|
|
*/
|
|
|
|
getItemByValue(value) {
|
|
|
|
let state = this.store.getState()
|
|
|
|
|
|
|
|
let stateObject = state.find((item) => {
|
|
|
|
return item.value === value;
|
|
|
|
});
|
|
|
|
|
|
|
|
let item = this.list.querySelector(`[data-choice-id='${stateObject.id}']`)
|
|
|
|
|
|
|
|
return item;
|
|
|
|
}
|
|
|
|
|
2016-04-04 22:44:32 +02:00
|
|
|
/**
|
|
|
|
* Select item (a selected item can be deleted)
|
|
|
|
* @param {Element} item Element to select
|
|
|
|
* @return
|
|
|
|
*/
|
2016-04-04 00:07:10 +02:00
|
|
|
selectItem(item) {
|
|
|
|
let id = item.getAttribute('data-choice-id');
|
|
|
|
this.store.dispatch(selectItemFromStore(id, true));
|
2016-03-24 00:00:32 +01:00
|
|
|
}
|
|
|
|
|
2016-04-04 22:44:32 +02:00
|
|
|
/**
|
|
|
|
* Deselect item
|
|
|
|
* @param {Element} item Element to de-select
|
|
|
|
* @return
|
|
|
|
*/
|
|
|
|
deselectItem(item) {
|
2016-04-04 00:07:10 +02:00
|
|
|
let id = item.getAttribute('data-choice-id');
|
|
|
|
this.store.dispatch(selectItemFromStore(id, false));
|
|
|
|
}
|
2016-03-24 00:00:32 +01:00
|
|
|
|
2016-04-04 22:44:32 +02:00
|
|
|
/**
|
|
|
|
* Select items within array
|
|
|
|
* @param {Array} items Array of items to select
|
|
|
|
* @return
|
|
|
|
*/
|
2016-04-04 00:07:10 +02:00
|
|
|
selectAll(items) {
|
2016-03-24 00:00:32 +01:00
|
|
|
for (let i = 0; i < items.length; i++) {
|
|
|
|
let item = items[i];
|
2016-04-04 00:07:10 +02:00
|
|
|
this.selectItem(item);
|
2016-03-24 00:00:32 +01:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
2016-04-04 22:44:32 +02:00
|
|
|
/**
|
|
|
|
* Add item to store with correct value
|
|
|
|
* @param {String} value Value to add to store
|
|
|
|
*/
|
2016-04-04 15:43:22 +02:00
|
|
|
addItem(value) {
|
2016-03-18 13:26:38 +01:00
|
|
|
if (this.options.debug) console.debug('Add item');
|
|
|
|
|
2016-03-24 15:42:03 +01:00
|
|
|
let passedValue = value;
|
|
|
|
|
|
|
|
// If a prepended value has been passed, prepend it
|
|
|
|
if(this.options.prependValue) {
|
|
|
|
passedValue = this.options.prependValue + passedValue.toString();
|
|
|
|
}
|
|
|
|
|
|
|
|
// If an appended value has been passed, append it
|
|
|
|
if(this.options.appendValue) {
|
|
|
|
passedValue = passedValue + this.options.appendValue.toString();
|
|
|
|
}
|
|
|
|
|
2016-04-04 15:43:22 +02:00
|
|
|
// Generate unique id
|
2016-03-31 15:51:41 +02:00
|
|
|
let id = this.store.getState().length + 1;
|
|
|
|
|
2016-03-24 15:42:03 +01:00
|
|
|
// Run callback if it is a function
|
2016-03-24 00:00:32 +01:00
|
|
|
if(this.options.callbackOnAddItem){
|
|
|
|
if(isType('Function', this.options.callbackOnAddItem)) {
|
2016-04-04 15:43:22 +02:00
|
|
|
this.options.callbackOnAddItem(id, value);
|
2016-03-24 00:00:32 +01:00
|
|
|
} else {
|
2016-03-24 15:42:03 +01:00
|
|
|
console.error('callbackOnAddItem: Callback is not a function');
|
2016-03-24 00:00:32 +01:00
|
|
|
}
|
|
|
|
}
|
2016-03-31 15:51:41 +02:00
|
|
|
|
2016-04-04 15:43:22 +02:00
|
|
|
this.store.dispatch(addItemToStore(passedValue, id));
|
2016-03-17 16:00:22 +01:00
|
|
|
}
|
2016-03-15 23:42:10 +01:00
|
|
|
|
2016-04-04 22:44:32 +02:00
|
|
|
/**
|
|
|
|
* Remove item from store
|
|
|
|
* @param
|
|
|
|
*/
|
2016-04-08 10:07:41 +02:00
|
|
|
removeItem(itemOrValue) {
|
|
|
|
if(!itemOrValue) {
|
|
|
|
console.error('removeItem: No item or value was passed to be removed');
|
2016-03-24 00:00:32 +01:00
|
|
|
return;
|
|
|
|
}
|
2016-03-22 15:36:01 +01:00
|
|
|
|
2016-04-08 10:07:41 +02:00
|
|
|
// We are re-assigning a variable here. Probably shouldn't be doing that...
|
|
|
|
let item;
|
|
|
|
if(itemOrValue.nodeType) {
|
|
|
|
item = itemOrValue;
|
|
|
|
} else {
|
|
|
|
for (var i = this.list.children.length - 1; i >= 0; i--) {
|
|
|
|
let listItem = this.list.children[i];
|
|
|
|
if(listItem.innerHTML === itemOrValue.toString()) {
|
|
|
|
item = listItem;
|
|
|
|
break;
|
|
|
|
}
|
2016-03-22 15:36:01 +01:00
|
|
|
}
|
2016-03-24 00:00:32 +01:00
|
|
|
}
|
2016-03-31 15:51:41 +02:00
|
|
|
|
2016-04-08 10:07:41 +02:00
|
|
|
if(item) {
|
|
|
|
let id = item.getAttribute('data-choice-id');
|
|
|
|
let value = item.innerHTML;
|
|
|
|
|
|
|
|
// Run callback
|
|
|
|
if(this.options.callbackOnRemoveItem){
|
|
|
|
if(isType('Function', this.options.callbackOnRemoveItem)) {
|
|
|
|
this.options.callbackOnRemoveItem(value);
|
|
|
|
} else {
|
|
|
|
console.error('callbackOnRemoveItem: Callback is not a function');
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
this.store.dispatch(removeItemFromStore(id));
|
|
|
|
} else {
|
|
|
|
console.error('Item not found');
|
|
|
|
}
|
|
|
|
|
2016-03-22 15:36:01 +01:00
|
|
|
}
|
|
|
|
|
2016-04-04 22:44:32 +02:00
|
|
|
/**
|
|
|
|
* Remove all items from array
|
|
|
|
* @param {Array} items Items to remove from store
|
|
|
|
* @return
|
|
|
|
*/
|
2016-03-21 19:53:26 +01:00
|
|
|
removeAll(items) {
|
|
|
|
for (let i = 0; i < items.length; i++) {
|
|
|
|
let item = items[i];
|
|
|
|
|
|
|
|
if (item.classList.contains('is-selected')) {
|
|
|
|
this.removeItem(item);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
}
|
2016-04-09 12:29:56 +02:00
|
|
|
|
|
|
|
toggleDropdown() {
|
|
|
|
if(!this.dropdown) {
|
|
|
|
console.error('No dropdown set');
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
const isActive = this.dropdown.classList.contains('is-active');
|
|
|
|
|
|
|
|
this.dropdown.classList[isActive ? 'remove' : 'add']('is-active');
|
|
|
|
}
|
|
|
|
|
|
|
|
addItemToDropdown(value) {
|
|
|
|
const dropdownItem = strToEl(`<li class="choices__item choices__item--selectable" data-choice-selectable data-choice-value="${value}">${value}</li>`);
|
|
|
|
this.dropdown.appendChild(dropdownItem);
|
|
|
|
}
|
2016-04-04 22:44:32 +02:00
|
|
|
|
|
|
|
/* Rendering */
|
2016-03-21 19:53:26 +01:00
|
|
|
|
2016-04-04 22:44:32 +02:00
|
|
|
/**
|
|
|
|
* Create DOM structure around passed text element
|
|
|
|
* @return
|
|
|
|
*/
|
2016-03-18 12:05:50 +01:00
|
|
|
renderTextInput() {
|
2016-04-04 00:07:10 +02:00
|
|
|
/*
|
|
|
|
Template:
|
|
|
|
|
|
|
|
<div class="choices choices--active">
|
|
|
|
<div class="choices__inner">
|
|
|
|
<input id="1" type="text" data-choice="" class="choices__input choices__input--hidden" tabindex="-1" style="display:none;" aria-hidden="true">
|
|
|
|
<ul class="choices__list choices__list--items"></ul>
|
|
|
|
<input type="text" class="choices__input choices__input--cloned">
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
*/
|
|
|
|
|
2016-04-08 23:33:13 +02:00
|
|
|
let containerOuter = strToEl(`<div class="choices choices--active"></div>`);
|
|
|
|
let containerInner = strToEl(`<div class="choices__inner"></div>`);
|
|
|
|
|
|
|
|
// Hide passed input
|
|
|
|
this.passedInput.classList.add('choices__input', 'choices__input--hidden');
|
|
|
|
this.passedInput.tabIndex = '-1';
|
|
|
|
this.passedInput.setAttribute('style', 'display:none;');
|
|
|
|
this.passedInput.setAttribute('aria-hidden', 'true');
|
|
|
|
|
|
|
|
// Wrap input in container preserving DOM ordering
|
|
|
|
wrap(this.passedInput, containerInner);
|
|
|
|
|
|
|
|
// Wrapper inner container with outer container
|
|
|
|
wrap(containerInner, containerOuter);
|
|
|
|
|
|
|
|
let list = strToEl(`<div class="choices__list choices__list--items"></div>`);
|
|
|
|
let input = strToEl(`<input type="text" class="choices__input choices__input--cloned">`);
|
|
|
|
|
|
|
|
if (this.passedInput.placeholder) {
|
|
|
|
input.placeholder = this.passedInput.placeholder;
|
|
|
|
}
|
|
|
|
|
|
|
|
if(!this.options.addItems) {
|
|
|
|
input.disabled = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
containerOuter.appendChild(containerInner);
|
|
|
|
containerInner.appendChild(list);
|
|
|
|
containerInner.appendChild(input);
|
|
|
|
|
|
|
|
this.containerOuter = containerOuter;
|
|
|
|
this.containerInner = containerInner;
|
|
|
|
this.input = input;
|
|
|
|
this.list = list;
|
|
|
|
|
|
|
|
// Add any preset values seperated by delimiter
|
|
|
|
this.presetItems.forEach((value) => {
|
|
|
|
this.addItem(value);
|
|
|
|
});
|
|
|
|
|
|
|
|
// Subscribe to store
|
|
|
|
this.store.subscribe(this.render);
|
|
|
|
|
|
|
|
// Render any items
|
|
|
|
this.render();
|
|
|
|
|
|
|
|
// Trigger event listeners
|
|
|
|
this.addEventListeners();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Create DOM structure around passed select element
|
|
|
|
* @return
|
|
|
|
*/
|
|
|
|
renderMultipleSelectInput() {
|
|
|
|
/*
|
|
|
|
Template:
|
|
|
|
|
|
|
|
<div class="choices choices--active">
|
|
|
|
<div class="choices__inner">
|
|
|
|
<input id="1" type="text" data-choice="" class="choices__input choices__input--hidden" tabindex="-1" style="display:none;" aria-hidden="true">
|
|
|
|
<ul class="choices__list choices__list--items"></ul>
|
|
|
|
<input type="text" class="choices__input choices__input--cloned">
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
*/
|
|
|
|
|
2016-04-04 00:07:10 +02:00
|
|
|
let containerOuter = strToEl('<div class="choices choices--active"></div>');
|
|
|
|
let containerInner = strToEl('<div class="choices__inner"></div>');
|
2016-03-16 15:41:13 +01:00
|
|
|
|
2016-03-17 16:00:22 +01:00
|
|
|
// Hide passed input
|
2016-04-08 23:33:13 +02:00
|
|
|
this.passedInput.classList.add('choices__input', 'choices__input--hidden');
|
|
|
|
this.passedInput.tabIndex = '-1';
|
|
|
|
this.passedInput.setAttribute('style', 'display:none;');
|
|
|
|
this.passedInput.setAttribute('aria-hidden', 'true');
|
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-08 23:33:13 +02:00
|
|
|
wrap(this.passedInput, 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-04 23:52:49 +02:00
|
|
|
let list = strToEl('<div class="choices__list choices__list--items"></div>');
|
2016-04-04 00:07:10 +02:00
|
|
|
let input = strToEl('<input type="text" class="choices__input choices__input--cloned">');
|
2016-04-08 23:33:13 +02:00
|
|
|
let dropdown = strToEl('<div class="choices__list choices__list--dropdown"></div>');
|
2016-03-18 12:05:50 +01:00
|
|
|
|
2016-03-18 13:26:38 +01:00
|
|
|
if (input.placeholder) {
|
2016-04-08 23:33:13 +02:00
|
|
|
input.placeholder = this.passedInput.placeholder;
|
2016-03-18 12:05:50 +01:00
|
|
|
}
|
2016-03-18 13:26:38 +01:00
|
|
|
|
2016-03-24 15:42:03 +01:00
|
|
|
if(!this.options.addItems) {
|
|
|
|
input.disabled = true;
|
|
|
|
}
|
|
|
|
|
2016-04-04 23:52:49 +02:00
|
|
|
containerOuter.appendChild(containerInner);
|
2016-04-08 23:33:13 +02:00
|
|
|
containerOuter.appendChild(dropdown);
|
2016-03-18 00:10:16 +01:00
|
|
|
containerInner.appendChild(list);
|
|
|
|
containerInner.appendChild(input);
|
2016-04-04 23:52:49 +02:00
|
|
|
|
2016-03-18 00:10:16 +01:00
|
|
|
this.containerOuter = containerOuter;
|
|
|
|
this.containerInner = containerInner;
|
2016-03-17 16:00:22 +01:00
|
|
|
this.input = input;
|
|
|
|
this.list = list;
|
2016-04-08 23:33:13 +02:00
|
|
|
this.dropdown = dropdown;
|
2016-03-17 16:00:22 +01:00
|
|
|
|
2016-04-07 14:57:57 +02:00
|
|
|
// Add any preset values seperated by delimiter
|
|
|
|
this.presetItems.forEach((value) => {
|
2016-04-04 22:44:32 +02:00
|
|
|
this.addItem(value);
|
|
|
|
});
|
2016-03-15 23:42:10 +01:00
|
|
|
|
2016-04-09 12:29:56 +02:00
|
|
|
const unselectedOptions = this.passedInput.options;
|
|
|
|
for (var i = 0; i < unselectedOptions.length; i++) {
|
|
|
|
let option = unselectedOptions[i];
|
|
|
|
this.addItemToDropdown(option.value);
|
|
|
|
}
|
|
|
|
|
2016-04-04 22:44:32 +02:00
|
|
|
// Subscribe to store
|
|
|
|
this.store.subscribe(this.render);
|
|
|
|
|
|
|
|
// Render any items
|
|
|
|
this.render();
|
2016-04-04 23:52:49 +02:00
|
|
|
|
|
|
|
// Trigger event listeners
|
|
|
|
this.addEventListeners();
|
|
|
|
}
|
|
|
|
|
|
|
|
addEventListeners() {
|
|
|
|
document.addEventListener('keydown', this.onKeyDown);
|
|
|
|
this.list.addEventListener('click', this.onClick);
|
2016-04-08 23:33:13 +02:00
|
|
|
this.input.addEventListener('focus', this.onFocus);
|
2016-04-09 12:29:56 +02:00
|
|
|
this.input.addEventListener('blur', this.onBlur);
|
2016-04-04 23:52:49 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
removeEventListeners() {
|
|
|
|
document.removeEventListener('keydown', this.onKeyDown);
|
|
|
|
this.list.removeEventListener('click', this.onClick);
|
2016-04-08 23:33:13 +02:00
|
|
|
this.input.removeEventListener('focus', this.onFocus);
|
2016-04-09 12:29:56 +02:00
|
|
|
this.input.removeEventListener('blur', this.onBlur);
|
2016-03-17 16:00:22 +01:00
|
|
|
}
|
2016-03-16 15:41:13 +01:00
|
|
|
|
2016-04-04 22:44:32 +02:00
|
|
|
/**
|
|
|
|
* Render DOM with values
|
|
|
|
* @return
|
|
|
|
*/
|
|
|
|
render() {
|
|
|
|
let state = this.store.getState();
|
2016-04-04 15:43:22 +02:00
|
|
|
|
2016-04-04 22:44:32 +02:00
|
|
|
// Simplify store data to just values
|
|
|
|
let valueArray = state.reduce((prev, current) => {
|
|
|
|
prev.push(current.value);
|
|
|
|
return prev;
|
|
|
|
}, []);
|
2016-03-21 23:45:49 +01:00
|
|
|
|
2016-04-04 22:44:32 +02:00
|
|
|
// Assign hidden input array of values
|
2016-04-08 23:33:13 +02:00
|
|
|
this.passedInput.value = valueArray.join(this.options.delimiter);
|
2016-04-04 22:44:32 +02:00
|
|
|
|
|
|
|
// Clear list
|
|
|
|
this.list.innerHTML = '';
|
|
|
|
|
|
|
|
// Add each list item to list
|
|
|
|
state.forEach((item) => {
|
2016-04-04 15:43:22 +02:00
|
|
|
if(item.active) {
|
|
|
|
// Create new list element
|
2016-04-08 23:33:13 +02:00
|
|
|
let listItem = strToEl(`<div class="choices__item ${ item.selected ? 'is-selected' : '' }" data-choice-item data-choice-id="${ item.id }" data-choice-selected="${ item.selected }">${ item.value }</div>`);
|
2016-04-04 15:43:22 +02:00
|
|
|
|
|
|
|
// Append it to list
|
|
|
|
this.list.appendChild(listItem);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
2016-04-04 22:44:32 +02:00
|
|
|
console.log(state);
|
2016-04-04 15:43:22 +02:00
|
|
|
}
|
|
|
|
|
2016-04-04 22:44:32 +02:00
|
|
|
/**
|
|
|
|
* Determine how an input should be rendered
|
|
|
|
* @return {Element} Input to test
|
|
|
|
*/
|
|
|
|
renderInput(input) {
|
2016-03-18 13:26:38 +01:00
|
|
|
if (this.options.debug) console.debug('Render');
|
2016-03-18 12:05:50 +01:00
|
|
|
|
2016-04-04 22:44:32 +02:00
|
|
|
switch (input.type) {
|
2016-03-18 12:05:50 +01:00
|
|
|
case "text":
|
|
|
|
this.renderTextInput();
|
|
|
|
break;
|
|
|
|
case "select-one":
|
2016-04-04 00:07:10 +02:00
|
|
|
// this.renderSelectInput();
|
2016-03-18 12:05:50 +01:00
|
|
|
break;
|
|
|
|
case "select-multiple":
|
2016-04-08 23:33:13 +02:00
|
|
|
this.renderMultipleSelectInput();
|
2016-03-18 12:05:50 +01:00
|
|
|
break;
|
|
|
|
default:
|
2016-04-04 00:07:10 +02:00
|
|
|
this.renderTextInput();
|
2016-03-18 12:05:50 +01:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-04-04 22:44:32 +02:00
|
|
|
/**
|
|
|
|
* Initialise Choices
|
|
|
|
* @return
|
|
|
|
*/
|
|
|
|
init() {
|
|
|
|
if (!this.supports) console.error('init: Your browser doesn\'nt support shit');
|
|
|
|
this.initialised = true;
|
2016-04-08 23:33:13 +02:00
|
|
|
this.renderInput(this.passedInput);
|
2016-04-04 22:44:32 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Destroy Choices and nullify values
|
|
|
|
* @return
|
|
|
|
*/
|
2016-03-17 16:00:22 +01:00
|
|
|
destroy() {
|
|
|
|
this.options = null;
|
2016-04-08 23:33:13 +02:00
|
|
|
this.passedInput = null;
|
2016-03-18 00:10:16 +01:00
|
|
|
this.initialised = null;
|
2016-03-17 16:00:22 +01:00
|
|
|
}
|
|
|
|
};
|
2016-03-15 23:42:10 +01:00
|
|
|
|
2016-04-08 23:33:13 +02:00
|
|
|
document.addEventListener('DOMContentLoaded', () => {
|
2016-03-18 12:05:50 +01:00
|
|
|
let input1 = document.getElementById(1);
|
|
|
|
let input2 = document.getElementById(2);
|
|
|
|
let input3 = document.getElementById(3);
|
2016-03-21 23:45:49 +01:00
|
|
|
let input4 = document.getElementById(4);
|
2016-03-24 15:42:03 +01:00
|
|
|
let input5 = document.getElementById(5);
|
2016-04-07 14:57:57 +02:00
|
|
|
let input6 = document.getElementById(6);
|
2016-04-08 23:33:13 +02:00
|
|
|
let input7 = document.getElementById(7);
|
2016-03-17 16:00:22 +01:00
|
|
|
|
2016-03-18 12:05:50 +01:00
|
|
|
let choices1 = new Choices({
|
|
|
|
element : input1,
|
2016-04-04 22:44:32 +02:00
|
|
|
delimiter: ' ',
|
2016-04-04 15:43:22 +02:00
|
|
|
editItems: true,
|
|
|
|
maxItems: 5,
|
2016-04-08 10:07:41 +02:00
|
|
|
callbackOnRemoveItem: function(value) {
|
|
|
|
console.log(value);
|
|
|
|
},
|
|
|
|
callbackOnAddItem: function(item, value) {
|
|
|
|
console.log(item, value);
|
|
|
|
}
|
2016-04-04 15:43:22 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
let choices2 = new Choices({
|
|
|
|
element : input2,
|
|
|
|
allowDuplicates: false,
|
|
|
|
editItems: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
let choices3 = new Choices({
|
|
|
|
element : input3,
|
|
|
|
allowDuplicates: false,
|
|
|
|
editItems: true,
|
|
|
|
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,}))$/
|
|
|
|
});
|
|
|
|
|
|
|
|
let choices4 = new Choices({
|
|
|
|
element : input4,
|
|
|
|
addItems: false
|
2016-03-18 12:05:50 +01:00
|
|
|
});
|
2016-03-15 23:42:10 +01:00
|
|
|
|
2016-04-04 15:43:22 +02:00
|
|
|
let choices5 = new Choices({
|
|
|
|
element: input5,
|
|
|
|
prependValue: 'item-',
|
|
|
|
appendValue: `-${Date.now()}`
|
|
|
|
});
|
2016-04-07 14:57:57 +02:00
|
|
|
|
|
|
|
let choices6 = new Choices({
|
|
|
|
element: input6,
|
|
|
|
items: ['josh@joshuajohnson.co.uk', 'joe@bloggs.co.uk']
|
|
|
|
});
|
2016-04-08 10:07:41 +02:00
|
|
|
|
2016-04-08 23:33:13 +02:00
|
|
|
let choices7 = new Choices({
|
|
|
|
element: input7,
|
|
|
|
});
|
|
|
|
|
2016-04-08 10:07:41 +02:00
|
|
|
choices6.addItem('josh2@joshuajohnson.co.uk');
|
|
|
|
choices6.removeItem('josh@joshuajohnson.co.uk');
|
2016-04-08 23:33:13 +02:00
|
|
|
console.log(choices6.getItemByValue('josh2@joshuajohnson.co.uk'));
|
|
|
|
});
|