Choices/assets/scripts/src/choices.js

1132 lines
38 KiB
JavaScript
Raw Normal View History

'use strict';
2016-03-30 16:04:21 +02:00
import { createStore } from 'redux';
import rootReducer from './reducers/index.js';
import { addItem, removeItem, selectItem, addOption, filterOptions, activateOptions, addGroup } from './actions/index';
2016-05-02 16:29:05 +02:00
import { isScrolledIntoView, getAdjacentEl, findAncestor, wrap, isType, strToEl, extend, getWidthOfInput, debounce } from './lib/utils.js';
import Sifter from 'sifter';
/**
* Choices
*
* To do:
* - Remove item by clicking a target
* - Set input width based on the size of the contents
* - Single select input support
* - Populate options by function
*/
2016-03-18 13:26:38 +01:00
export class Choices {
2016-04-29 19:06:46 +02:00
constructor(element = '[data-choice]', userOptions = {}) {
2016-04-17 13:09:46 +02:00
// Cutting the mustard
2016-04-29 19:23:06 +02:00
const cuttingTheMustard = 'querySelector' in document && 'addEventListener' in document && 'classList' in document.createElement("div");
if (!cuttingTheMustard) console.error('init: Your browser doesn\'t support Choices');
2016-04-17 13:09:46 +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)) {
const elements = document.querySelectorAll(element);
if(elements.length > 1) {
for (let i = 1; i < elements.length; i++) {
let el = elements[i];
new Choices(el, options);
}
}
}
const defaultOptions = {
items: [],
addItems: true,
removeItems: true,
2016-03-21 19:53:26 +01:00
editItems: false,
maxItems: false,
delimiter: ',',
allowDuplicates: true,
allowPaste: true,
allowSearch: true,
regexFilter: false,
2016-04-15 10:19:02 +02:00
placeholder: true,
placeholderValue: '',
prependValue: false,
appendValue: false,
selectAll: true,
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',
listItems: 'choices__list--items',
listDropdown: 'choices__list--dropdown',
item: 'choices__item',
itemSelectable: 'choices__item--selectable',
itemDisabled: 'choices__item--disabled',
itemOption: 'choices__item--option',
group: 'choices__group',
groupHeading : 'choices__heading',
2016-04-11 15:13:50 +02:00
activeState: 'is-active',
disabledState: 'is-disabled',
highlightedState: 'is-highlighted',
2016-04-11 15:13:50 +02:00
hiddenState: 'is-hidden',
flippedState: 'is-flipped',
2016-04-11 15:13:50 +02:00
selectedState: 'is-selected'
2016-04-10 23:54:56 +02:00
},
2016-05-02 16:29:05 +02:00
templates: {},
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-15 23:42:10 +01:00
2016-04-04 22:44:32 +02:00
// Initial instance state
this.initialised = false;
// Merge options with user options
2016-04-29 19:06:46 +02:00
this.options = extend(defaultOptions, userOptions);
2016-04-04 22:44:32 +02:00
// Create data store
this.store = createStore(rootReducer);
2016-03-30 16:04:21 +02:00
// Retrieve triggering element (i.e. element with 'data-choice' trigger)
this.passedElement = isType('String', element) ? document.querySelector(element) : element;
// Set preset items - this looks out of place
this.presetItems = [];
if(this.options.items.length) {
this.presetItems = this.options.items;
} else if(this.passedElement.value !== '') {
this.presetItems = this.passedElement.value.split(this.options.delimiter);
}
// 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-04-29 19:23:06 +02:00
// Bind event handlers
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.onClick = this.onClick.bind(this);
this.onPaste = this.onPaste.bind(this);
this.onMouseOver = this.onMouseOver.bind(this);
2016-04-29 16:18:53 +02: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-04-29 19:06:46 +02:00
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-04-04 22:44:32 +02:00
*/
2016-04-29 19:06:46 +02:00
handleEnter(activeItems, value) {
let canUpdate = true;
2016-04-27 17:41:30 +02:00
if(this.passedElement.type === 'text') {
if(this.options.addItems) {
if (this.options.maxItems && this.options.maxItems <= this.list.children.length) {
// If there is a max entry limit and we have reached that limit
// don't update
canUpdate = false;
} else if(this.options.allowDuplicates === false && this.passedElement.value) {
// If no duplicates are allowed, and the value already exists
// in the array, don't update
canUpdate = !activeItems.some((item) => {
return item.value === value;
});
}
} else {
canUpdate = false;
2016-04-27 17:41:30 +02:00
}
2016-04-29 19:06:46 +02:00
if (canUpdate) {
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) {
this.addItem(value);
this.clearInput(this.passedElement);
}
}
}
};
2016-04-29 19:06:46 +02:00
/**
* Process back space event
* @param {Array} Active items
* @return
*/
handleBackspaceKey(activeItems) {
if(this.options.removeItems && activeItems) {
const lastItem = activeItems[activeItems.length - 1];
const hasSelectedItems = activeItems.some((item) => {
return item.selected === true;
});
// 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-04-17 13:09:46 +02:00
if(this.options.editItems && !hasSelectedItems && lastItem) {
this.input.value = lastItem.value;
this.removeItem(lastItem);
} else {
this.selectItem(lastItem);
this.removeAllSelectedItems();
}
}
};
2016-04-29 19:06:46 +02:00
/**
* Handle what happens on a click event
* @param {Array} activeItems Items that are active
* @param {HTMLElement} target What triggered the click
* @param {Boolean} hasShiftKey Whether shift key is active
* @return
*/
handleClick(activeItems, target, hasShiftKey) {
if(this.options.removeItems && target) {
2016-05-02 16:29:05 +02:00
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
2016-04-17 13:09:46 +02:00
// unless shift is being pressed
activeItems.forEach((item) => {
if(item.id === parseInt(passedId) && !item.selected) {
this.selectItem(item);
2016-04-29 19:06:46 +02:00
} else if(!hasShiftKey) {
this.deselectItem(item);
}
});
}
}
2016-04-29 19:06:46 +02:00
/**
* Key down event
2016-04-04 22:44:32 +02:00
* @param {Object} e Event
* @return
*/
onKeyDown(e) {
2016-04-29 19:23:06 +02:00
if(e.target !== this.input) return;
const ctrlDownKey = e.ctrlKey || e.metaKey;
2016-04-29 16:18:53 +02:00
const backKey = 46;
const deleteKey = 8;
const enterKey = 13;
const aKey = 65;
const escapeKey = 27;
const upKey = 38;
const downKey = 40;
2016-04-29 19:23:06 +02:00
const activeItems = this.getItemsFilteredByActive();
const activeOptions = this.getOptionsFilteredByActive();
2016-04-29 19:23:06 +02:00
const hasFocussedInput = this.input === document.activeElement;
const hasActiveDropdown = this.dropdown && this.dropdown.classList.contains(this.options.classNames.activeState);
const hasItems = this.list && this.list.children;
2016-04-29 19:23:06 +02:00
const keyString = String.fromCharCode(event.keyCode);
2016-04-29 19:23:06 +02:00
// If a user is typing and the dropdown is not active
if(/[a-zA-Z0-9-_ ]/.test(keyString) && this.dropdown && !hasActiveDropdown) {
this.toggleDropdown();
}
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) {
if(this.options.removeItems && !this.input.value && this.options.selectAll && this.input === document.activeElement) {
this.selectAll(this.list.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;
this.handleEnter(activeItems, value);
}
if(this.passedElement.type === 'select-multiple' && hasActiveDropdown) {
const highlighted = this.dropdown.querySelector(`.${this.options.classNames.highlightedState}`);
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-05-02 16:29:05 +02:00
const id = highlighted.getAttribute('data-id');
2016-04-29 19:23:06 +02:00
this.addItem(value, label, id);
2016-05-02 16:29:05 +02:00
this.clearInput(this.passedElement);
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 escapeKey:
if(this.passedElement.type === 'select-multiple' && hasActiveDropdown) {
this.toggleDropdown();
}
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
if(this.passedElement.type === 'select-multiple' && hasActiveDropdown) {
const currentEl = this.dropdown.querySelector(`.${this.options.classNames.highlightedState}`);
let nextEl;
if(currentEl) {
if(e.keyCode === downKey) {
2016-05-02 16:29:05 +02:00
nextEl = getAdjacentEl(currentEl, '[data-option-selectable]', 1);
} else if(e.keyCode === upKey) {
2016-05-02 16:29:05 +02:00
nextEl = getAdjacentEl(currentEl, '[data-option-selectable]', -1);
}
} else {
2016-05-02 16:29:05 +02:00
nextEl = this.dropdown.querySelector('[data-option-selectable]');
2016-04-29 19:23:06 +02:00
}
if(nextEl) {
if(currentEl) {
currentEl.classList.remove(this.options.classNames.highlightedState);
}
2016-05-02 16:29:05 +02:00
if(!isScrolledIntoView(nextEl)) {
nextEl.scrollIntoView();
}
nextEl.classList.add(this.options.classNames.highlightedState);
}
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
if(hasFocussedInput && !e.target.value) {
this.handleBackspaceKey(activeItems);
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-15 23:42:10 +01:00
2016-04-29 19:06:46 +02:00
/**
* Key up event
* @param {Object} e Event
* @return
*/
onKeyUp(e) {
2016-04-29 19:23:06 +02:00
if(e.target !== this.input) return;
if(this.passedElement.type === 'select-multiple' && this.options.allowSearch) {
const options = this.getOptions();
const hasUnactiveOptions = options.some((option) => {
return option.active !== true;
});
if(this.input === document.activeElement) {
if(this.input.value) {
// If we have a value, filter options based on it
const handleFilter = debounce(() => {
const options = this.getOptionsFiltedBySelectable();
const sifter = new Sifter(options);
const results = sifter.search(this.input.value, {
fields: ['label', 'value'],
sort: [{field: 'value', direction: 'asc'}],
limit: 10
});
2016-04-29 19:23:06 +02:00
this.store.dispatch(filterOptions(results));
}, 100)
handleFilter();
} else if(hasUnactiveOptions) {
// Otherwise reset options to active
this.store.dispatch(activateOptions());
}
2016-04-29 19:23:06 +02:00
}
}
}
2016-04-04 22:44:32 +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
*/
onClick(e) {
2016-04-29 19:06:46 +02:00
const activeItems = this.getItemsFilteredByActive();
const hasShiftKey = e.shiftKey ? true : false;
2016-04-17 13:09:46 +02:00
2016-05-02 16:29:05 +02:00
if(this.passedElement.type === 'select-multiple' && !this.dropdown.classList.contains(this.options.classNames.activeState)) {
this.toggleDropdown();
}
2016-04-11 15:13:50 +02:00
// If click is affecting a child node of our element
if(this.containerOuter.contains(e.target)) {
// If input is not in focus, it ought to be
if(this.input !== document.activeElement) {
this.input.focus();
}
2016-05-02 16:29:05 +02:00
if(e.target.hasAttribute('data-item')) {
// If we are clicking on an item
2016-04-29 19:06:46 +02:00
this.handleClick(activeItems, e.target, hasShiftKey);
2016-05-02 16:29:05 +02:00
} else if(e.target.hasAttribute('data-option')) {
// If we are clicking on an option
const options = this.getOptionsFilteredByActive();
2016-05-02 16:29:05 +02:00
const id = e.target.getAttribute('data-id');
const option = options.find((option) => {
return option.id === parseInt(id);
});
2016-05-02 16:29:05 +02:00
if(!option.selected && !option.disabled) {
this.addItem(option.value, option.label, option.id);
}
2016-04-11 15:13:50 +02:00
}
2016-04-12 15:31:07 +02:00
} else {
// Click is outside of our element so close dropdown and de-select items
2016-04-29 19:06:46 +02:00
const hasSelectedItems = activeItems.some((item) => {
return item.selected === true;
});
if(hasSelectedItems) {
this.deselectAll();
}
2016-04-29 19:06:46 +02:00
2016-05-02 16:29:05 +02:00
// Close all other dropodowns
2016-04-27 17:41:30 +02:00
if(this.passedElement.type === 'select-multiple' && this.dropdown.classList.contains(this.options.classNames.activeState)) {
this.toggleDropdown();
}
2016-04-09 12:29:56 +02:00
}
}
2016-04-29 19:06:46 +02:00
/**
* Paste event
* @param {Object} e Event
* @return
*/
onPaste(e) {
// Disable pasting into the input if option has been set
if(!this.options.allowPaste) {
e.preventDefault();
}
2016-04-09 12:29:56 +02:00
}
/**
* Mouse over (hover) event
* @param {Object} e Event
* @return
*/
onMouseOver(e) {
// If we have a dropdown and it is either the target or one of its children is the target
if(this.dropdown && (e.target === this.dropdown || findAncestor(e.target, this.options.classNames.listDropdown))) {
2016-05-02 16:29:05 +02:00
if(e.target.hasAttribute('data-option')) {
const highlightedOptions = this.dropdown.querySelectorAll(`.${this.options.classNames.highlightedState}`);
// Remove any highlighted options
Array.from(highlightedOptions).forEach((element) => {
element.classList.remove(this.options.classNames.highlightedState);
});
e.target.classList.add(this.options.classNames.highlightedState);
}
}
}
2016-04-29 19:06:46 +02:00
/**
* Focus event
* @param {Object} e Event
* @return
*/
onFocus(e) {
this.containerOuter.classList.add(this.options.classNames.activeState);
}
2016-04-29 19:06:46 +02:00
/**
* Blur event
* @param {Object} e Event
* @return
*/
onBlur(e) {
this.containerOuter.classList.remove(this.options.classNames.activeState);
}
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
*/
regexFilter(value) {
const expression = new RegExp(this.options.regexFilter, 'i');
const passesTest = expression.test(value);
return passesTest;
}
2016-04-04 22:44:32 +02:00
/**
* Select item (a selected item can be deleted)
* @param {Element} item Element to select
* @return
*/
selectItem(item) {
if(!item) return;
const id = item.id;
2016-04-12 15:31:07 +02:00
this.store.dispatch(selectItem(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) {
if(!item) return;
const id = item.id;
2016-04-12 15:31:07 +02:00
this.store.dispatch(selectItem(id, false));
}
2016-03-24 00:00:32 +01:00
2016-04-04 22:44:32 +02:00
/**
2016-05-02 16:29:05 +02:00
* Select items within store
2016-04-04 22:44:32 +02:00
* @return
*/
selectAll() {
const items = this.getItems();
items.forEach((item) => {
this.selectItem(item);
});
}
2016-05-02 16:29:05 +02:00
/**
* Deselect items within store
* @return
*/
deselectAll() {
const items = this.getItems();
items.forEach((item) => {
this.deselectItem(item);
});
}
2016-04-04 22:44:32 +02:00
/**
* Add item to store with correct value
* @param {String} value Value to add to store
*/
addItem(value, label, optionId = -1, callback = this.options.callbackOnAddItem) {
let passedValue = value.trim();
let passedLabel = label || passedValue;
let passedOptionId = optionId || -1;
// 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
const id = this.store.getState().items.length + 1;
this.store.dispatch(addItem(passedValue, passedLabel, id, passedOptionId));
2016-04-11 15:13:50 +02:00
// Run callback if it is a function
if(callback){
if(isType('Function', callback)) {
callback(id, value);
2016-03-24 00:00:32 +01:00
} else {
console.error('callbackOnAddItem: Callback is not a function');
2016-03-24 00:00:32 +01:00
}
}
}
2016-03-15 23:42:10 +01:00
2016-04-04 22:44:32 +02:00
/**
* Remove item from store
* @param
*/
removeItem(item, callback = this.options.callbackOnRemoveItem) {
if(!item || !isType('Object', item)) {
console.error('removeItem: No item object was passed to be removed');
2016-03-24 00:00:32 +01:00
return;
}
const id = item.id;
const value = item.value;
const optionId = item.optionId;
2016-04-08 10:07:41 +02:00
2016-04-29 16:18:53 +02:00
this.store.dispatch(removeItem(id, optionId));
// Run callback
if(callback){
if(isType('Function', callback)) {
callback(value);
} else {
console.error('callbackOnRemoveItem: Callback is not a function');
2016-04-08 10:07:41 +02:00
}
}
}
2016-04-29 19:06:46 +02:00
/**
* Remove an item from the store by its value
* @param {String} value Value to search for
* @return
*/
removeItemsByValue(value) {
if(!value || !isType('String', value)) {
console.error('removeItemsByValue: No value was passed to be removed');
}
const items = this.getItemsFilteredByActive();
items.forEach((item) => {
if(item.value === value) {
this.removeItem(item);
}
});
}
2016-04-04 22:44:32 +02:00
/**
2016-04-29 19:06:46 +02:00
* Remove all items from store array
* Note: removed items are soft deleted
2016-04-12 15:54:07 +02:00
* @param {Boolean} selectedOnly Optionally remove only selected items
2016-04-04 22:44:32 +02:00
* @return
*/
removeAllItems() {
const items = this.getItemsFilteredByActive();
2016-03-21 19:53:26 +01:00
2016-04-12 15:54:07 +02:00
items.forEach((item) => {
if(item.active) {
this.removeItem(item);
}
});
}
2016-04-29 19:06:46 +02:00
/**
* Remove all selected items from store
* Note: removed items are soft deleted
* @return
*/
removeAllSelectedItems() {
const items = this.getItemsFilteredByActive();
items.forEach((item) => {
if(item.selected && item.active) {
this.removeItem(item);
2016-03-21 19:53:26 +01:00
}
2016-04-12 15:54:07 +02:00
});
2016-03-21 19:53:26 +01:00
}
2016-04-09 12:29:56 +02:00
2016-04-29 19:06:46 +02:00
/**
* Show dropdown to user by adding active state class
* @return
*/
showDropdown() {
this.dropdown.classList.add(this.options.classNames.activeState);
const dimensions = this.dropdown.getBoundingClientRect();
2016-04-29 19:06:46 +02:00
const shouldFlip = dimensions.top + dimensions.height >= document.body.offsetHeight;
// Whether or not the dropdown should appear above or below input
if(shouldFlip) {
this.dropdown.classList.add(this.options.classNames.flippedState);
} else {
this.dropdown.classList.remove(this.options.classNames.flippedState);
}
}
2016-04-29 19:06:46 +02:00
/**
* Hide dropdown from user
* @return {[type]} [description]
*/
hideDropdown() {
// A dropdown flips if it does not have space below the input
const isFlipped = this.dropdown.classList.contains(this.options.classNames.flippedState);
this.dropdown.classList.remove(this.options.classNames.activeState);
if(isFlipped) {
this.dropdown.classList.remove(this.options.classNames.flippedState);
2016-04-09 12:29:56 +02:00
}
}
2016-04-29 19:06:46 +02:00
/**
* Determine whether to hide or show dropdown based on its current state
* @return
*/
toggleDropdown() {
if(!this.dropdown) return;
const isActive = this.dropdown.classList.contains(this.options.classNames.activeState);
2016-04-09 12:29:56 +02:00
if(isActive) {
this.hideDropdown();
} else {
this.showDropdown();
}
2016-04-09 12:29:56 +02:00
}
2016-04-29 19:06:46 +02:00
/**
* Add option to dropdown
* @param {Object} option Option to add
* @param {Number} groupId ID of the options group
* @return
*/
2016-05-02 16:29:05 +02:00
addOption(option, groupId = -1) {
2016-04-12 15:31:07 +02:00
// Generate unique id
2016-04-14 15:43:36 +02:00
const state = this.store.getState();
const id = state.options.length + 1;
const value = option.value;
const label = option.innerHTML;
2016-05-02 16:29:05 +02:00
const isDisabled = option.disabled || option.parentNode.disabled;
2016-04-14 15:43:36 +02:00
2016-05-02 16:29:05 +02:00
this.store.dispatch(addOption(value, label, id, groupId, isDisabled));
2016-04-14 15:43:36 +02:00
2016-05-02 16:29:05 +02:00
if(option.selected && !isDisabled) {
this.addItem(value, label, id);
2016-04-14 15:43:36 +02:00
}
2016-04-09 12:29:56 +02:00
}
2016-04-29 19:06:46 +02:00
/**
* Add group to dropdown
* @param {Object} group Group to add
* @param {Number} index Whether this is the first group to add
*/
addGroup(group, id, isFirst) {
const groupOptions = Array.from(group.getElementsByTagName('OPTION'));
const groupId = id;
if(groupOptions) {
this.store.dispatch(addGroup(group.label, groupId, true, group.disabled));
groupOptions.forEach((option, optionIndex) => {
// We want to pre-highlight the first option
const highlighted = isFirst && optionIndex === 0 ? true : false;
2016-05-02 16:29:05 +02:00
this.addOption(option, groupId);
2016-04-29 19:06:46 +02:00
});
} else {
this.store.dispatch(addGroup(group.label, group.id, false, group.disabled));
}
}
/**
2016-04-29 19:06:46 +02:00
* Get items from store
* @return {Array} Item objects
*/
getItems() {
const state = this.store.getState();
return state.items;
}
/**
2016-04-29 19:06:46 +02:00
* Get active items from store
* @return {Array} Item objects
*/
getItemsFilteredByActive() {
const items = this.getItems();
const valueArray = items.filter((item) => {
return item.active === true;
}, []);
return valueArray;
}
/**
2016-04-29 19:06:46 +02:00
* Get items from store reduced to just their values
* @return {Array} Item objects
*/
getItemsReducedToValues() {
const items = this.getItems();
const valueArray = items.reduce((prev, current) => {
prev.push(current.value);
return prev;
}, []);
return valueArray;
}
/**
2016-04-29 19:06:46 +02:00
* Get options from store
* @return {Array} Option objects
*/
getOptions() {
const state = this.store.getState();
return state.options;
}
2016-04-29 19:06:46 +02:00
/**
* Get active options from store
* @return {Array} Option objects
*/
getOptionsFilteredByActive() {
const options = this.getOptions();
const valueArray = options.filter((option) => {
2016-05-02 16:29:05 +02:00
return option.active === true && option.selected !== true;
},[]);
return valueArray;
}
2016-04-29 19:06:46 +02:00
/**
* Get selectable options from store
* @return {Array} Option objects
*/
getOptionsFiltedBySelectable() {
const options = this.getOptions();
const valueArray = options.filter((option) => {
return option.disabled === false;
},[]);
return valueArray;
}
2016-04-29 19:06:46 +02:00
/**
* Get groups from store
* @return {Array} Group objects
*/
getGroups() {
const state = this.store.getState();
return state.groups;
}
2016-04-29 19:06:46 +02:00
/**
* Get active groups from store
* @return {Array} Group objects
*/
getGroupsFilteredByActive() {
const groups = this.getGroups();
const options = this.getOptions();
const valueArray = groups.filter((group) => {
const isActive = group.active === true && group.disabled === false;
const hasActiveOptions = options.some((option) => {
return option.active === true && option.disabled === false;
});
return isActive && hasActiveOptions ? true : false;
},[]);
return valueArray;
}
2016-03-21 19:53:26 +01:00
/**
* Create DOM structure around passed select element
* @return
*/
2016-04-29 18:11:20 +02:00
generateInput() {
2016-05-02 16:29:05 +02:00
const containerOuter = this.getTemplate('containerOuter');
const containerInner = this.getTemplate('containerInner');
const list = this.getTemplate('list');
const input = this.getTemplate('input');
// Hide passed input
2016-04-11 15:13:50 +02:00
this.passedElement.classList.add(this.options.classNames.input, this.options.classNames.hiddenState);
this.passedElement.tabIndex = '-1';
this.passedElement.setAttribute('style', 'display:none;');
this.passedElement.setAttribute('aria-hidden', 'true');
2016-03-16 21:24:11 +01:00
// Wrap input in container preserving DOM ordering
wrap(this.passedElement, containerInner);
2016-03-18 13:26:38 +01:00
// Wrapper inner container with outer container
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
if (this.options.placeholder && this.options.placeholderValue) {
input.placeholder = this.options.placeholderValue;
input.style.width = getWidthOfInput(input);
}
2016-03-18 13:26:38 +01:00
if(!this.options.addItems) {
input.disabled = true;
2016-04-29 18:11:20 +02:00
containerOuter.classList.add(this.options.classNames.disabledState);
}
2016-04-04 23:52:49 +02:00
containerOuter.appendChild(containerInner);
containerInner.appendChild(list);
containerInner.appendChild(input);
2016-04-29 18:11:20 +02:00
if(this.passedElement.type === 'select-multiple') {
this.highlightPosition = 0;
2016-05-02 16:29:05 +02:00
const dropdown = this.getTemplate('dropdown');
2016-04-29 18:11:20 +02:00
const passedGroups = Array.from(this.passedElement.getElementsByTagName('OPTGROUP'));
containerOuter.appendChild(dropdown);
this.dropdown = dropdown;
if(passedGroups.length) {
passedGroups.forEach((group, index) => {
2016-04-29 19:06:46 +02:00
const isFirst = index === 0 ? true : false;
this.addGroup(group, index, isFirst);
2016-04-29 18:11:20 +02:00
});
} else {
const passedOptions = Array.from(this.passedElement.options);
passedOptions.forEach((option) => {
this.addOption(option);
});
}
} else if(this.passedElement.type === 'text') {
// Add any preset values seperated by delimiter
this.presetItems.forEach((value) => {
this.addItem(value);
});
}
2016-04-04 23:52:49 +02:00
this.containerOuter = containerOuter;
this.containerInner = containerInner;
this.input = input;
this.list = list;
}
2016-04-04 22:44:32 +02:00
/**
* Render DOM with values
* @return
*/
render(callback = this.options.callbackOnRender) {
const classNames = this.options.classNames;
const activeItems = this.getItemsFilteredByActive();
2016-04-04 15:43:22 +02:00
2016-04-12 15:31:07 +02:00
// OPTIONS
2016-04-27 17:41:30 +02:00
if(this.passedElement.type === 'select-multiple') {
const activeOptions = this.getOptionsFilteredByActive();
const activeGroups = this.getGroupsFilteredByActive();
2016-04-12 15:31:07 +02:00
// Clear options
this.dropdown.innerHTML = '';
2016-04-13 15:42:23 +02:00
// Create a fragment to store our list items (so we don't have to update the DOM for each item)
const optionListFragment = document.createDocumentFragment();
// If we have grouped options
if(activeGroups.length >= 1) {
activeGroups.forEach((group, i) => {
// Grab options that are children of this group
const groupOptions = activeOptions.filter((option) => {
return option.groupId === group.id;
});
if(groupOptions.length >= 1) {
2016-05-02 16:29:05 +02:00
const dropdownGroup = this.getTemplate('optgroup', group);
groupOptions.forEach((option, j) => {
2016-05-02 16:29:05 +02:00
const dropdownItem = this.getTemplate('option', option);
dropdownGroup.appendChild(dropdownItem);
});
optionListFragment.appendChild(dropdownGroup);
}
});
} else if(activeOptions.length >= 1) {
activeOptions.forEach((option, i) => {
2016-05-02 16:29:05 +02:00
const dropdownItem = this.getTemplate('option', option);
optionListFragment.appendChild(dropdownItem);
});
}
this.dropdown.appendChild(optionListFragment);
2016-05-02 16:29:05 +02:00
// If dropdown is empty, show a no content notice
if(this.dropdown.innerHTML === "") {
2016-05-02 16:29:05 +02:00
const dropdownItem = this.getTemplate('notice', 'No options to select');
2016-04-29 16:18:53 +02:00
optionListFragment.appendChild(dropdownItem);
this.dropdown.appendChild(optionListFragment);
} else {
// Highlight first element in dropdown
2016-05-02 16:29:05 +02:00
const firstEl = this.dropdown.querySelector('[data-option]');
if(firstEl) {
firstEl.classList.add(this.options.classNames.highlightedState);
}
}
2016-04-12 15:31:07 +02:00
}
// ITEMS
if(activeItems) {
// Simplify store data to just values
const itemsFiltered = this.getItemsReducedToValues();
// Assign hidden input array of values
this.passedElement.value = itemsFiltered.join(this.options.delimiter);
2016-04-04 22:44:32 +02:00
// Clear list
this.list.innerHTML = '';
// Create a fragment to store our list items (so we don't have to update the DOM for each item)
const itemListFragment = document.createDocumentFragment();
// Add each list item to list
activeItems.forEach((item) => {
2016-04-04 15:43:22 +02:00
// Create new list element
2016-05-02 16:29:05 +02:00
const listItem = this.getTemplate('item', item);
2016-04-04 15:43:22 +02:00
// Append it to list
itemListFragment.appendChild(listItem);
});
2016-04-04 15:43:22 +02:00
this.list.appendChild(itemListFragment);
}
// Run callback if it is a function
if(callback){
if(isType('Function', callback)) {
callback(activeItems);
} else {
console.error('callbackOnRender: Callback is not a function');
}
}
2016-04-04 15:43:22 +02:00
}
2016-05-02 16:29:05 +02:00
getTemplate(template, ...args) {
if(!template) return;
const templates = this.options.templates;
return templates[template](...args);
}
/**
* Create HTML element based on type and arguments
* @param {String} template Template to create
* @param {...} args Data
* @return {HTMLElement}
*/
2016-05-02 16:29:05 +02:00
createTemplates() {
const classNames = this.options.classNames;
const templates = {
containerOuter: () => {
return strToEl(`<div class="${ classNames.containerOuter }"></div>`);
},
containerInner: () => {
return strToEl(`<div class="${ classNames.containerInner }"></div>`);
},
list: () => {
return strToEl(`<ul class="${ classNames.list } ${ classNames.listItems }"></ul>`);
},
input: () => {
return strToEl(`<input type="text" class="${ classNames.input } ${ classNames.inputCloned }">`);
},
dropdown: () => {
return strToEl(`<div class="${ classNames.list } ${ classNames.listDropdown }"></div>`);
},
notice: (label) => {
2016-05-02 16:29:05 +02:00
return strToEl(`<div class="${ classNames.item } ${ classNames.itemOption }">${ label }</div>`);
},
option: (data) => {
return strToEl(`
2016-05-02 16:29:05 +02:00
<div class="${ classNames.item } ${ classNames.itemOption } ${ 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>
`);
},
optgroup: (data) => {
return strToEl(`
2016-05-02 16:29:05 +02:00
<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>
`);
},
item: (data) => {
return strToEl(`
2016-05-02 16:29:05 +02:00
<div class="${ classNames.item } ${ classNames.itemOption } ${ data.selected ? classNames.selectedState : classNames.itemSelectable }" data-item data-id="${ data.id }" data-value="${ data.value }">
${ data.label }
</div>
`);
},
};
2016-05-02 16:29:05 +02:00
this.options.templates = extend(this.options.templates, templates);
}
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-04-04 22:44:32 +02:00
*/
2016-04-29 18:11:20 +02:00
addEventListeners() {
document.addEventListener('keyup', this.onKeyUp);
document.addEventListener('keydown', this.onKeyDown);
document.addEventListener('click', this.onClick);
document.addEventListener('paste', this.onPaste);
document.addEventListener('mouseover', this.onMouseOver);
2016-04-29 18:11:20 +02:00
this.input.addEventListener('focus', this.onFocus);
this.input.addEventListener('blur', this.onBlur);
}
/**
* Destroy event listeners
2016-04-29 19:06:46 +02:00
* @return
2016-04-29 18:11:20 +02:00
*/
removeEventListeners() {
document.removeEventListener('keyup', this.onKeyUp);
document.removeEventListener('keydown', this.onKeyDown);
document.removeEventListener('click', this.onClick);
document.removeEventListener('paste', this.onPaste);
document.removeEventListener('mouseover', this.onMouseOver);
2016-04-29 18:11:20 +02:00
this.input.removeEventListener('focus', this.onFocus);
this.input.removeEventListener('blur', this.onBlur);
}
2016-04-04 22:44:32 +02:00
/**
* Initialise Choices
* @return
*/
init(callback = this.options.callbackOnInit) {
2016-04-04 22:44:32 +02:00
this.initialised = true;
2016-05-02 16:29:05 +02:00
// Create required elements
this.createTemplates();
2016-04-29 18:11:20 +02:00
// Generate input markup
this.generateInput();
// Subscribe to store
this.store.subscribe(this.render);
// Render any items
this.render();
// Trigger event listeners
this.addEventListeners();
// Run callback if it is a function
if(callback){
if(isType('Function', callback)) {
callback();
} else {
console.error('callbackOnInit: Callback is not a function');
}
}
2016-04-04 22:44:32 +02:00
}
/**
* Destroy Choices and nullify values
* @return
*/
destroy() {
this.passedElement = null;
this.userOptions = null;
this.options = null;
this.initialised = null;
this.store = null;
}
};
2016-03-15 23:42:10 +01:00
2016-04-25 19:00:30 +02:00
window.Choices = module.exports = Choices;