'use strict'; import { hasClass, wrap, getSiblings, isType } from './lib/utils.js'; /** TODO: - Dynamically set input width to contents - Handle select input - Handle multiple select input ? */ export class Choices { constructor(options) { const FAKE_EL = document.createElement("FAKE_ELement"); const USER_OPTIONS = options || {}; const DEFAULT_OPTIONS = { element: document.querySelector('[data-choice]'), disabled: false, create: true, removeItems: true, editItems: false, maxItems: false, delimiter: ',', allowDuplicates: true, debug: false, placeholder: false, callbackOnInit: function() {}, callbackOnRender: function() {}, callbackOnKeyUp: function() {}, callbackOnKeyDown: function() {}, callbackOnEntry: function() {}, callbackOnRemove: function() {} }; // Merge options with user options this.options = this.extend(DEFAULT_OPTIONS, USER_OPTIONS || {}); this.initialised = false; this.supports = 'querySelector' in document && 'addEventListener' in document && 'classList' in FAKE_EL; // Retrieve elements this.element = this.options.element; // If input already has values, parse the array, otherwise create a blank array this.valueArray = this.element.value !== '' ? this.cleanInputValue(this.element.value) : []; // How many values in array this.valueCount = this.valueArray.length; // Bind methods this.onClick = this.onClick.bind(this); this.onKeyDown = this.onKeyDown.bind(this); this.onChange = this.onChange.bind(this); this.onFocus = this.onFocus.bind(this); this.onBlur = this.onChange.bind(this); this.init(); } cleanInputValue(value) { // Remove spaces and split with delimiter return value.replace(/\s/g, '').split(this.options.delimiter); } /** * Merges unspecified amount of objects into new object * @private * @return {Object} Merged object of arguments */ extend() { let extended = {}; let length = arguments.length; /** * Merge one object into another * @param {Object} obj Object to merge into extended object */ let merge = function(obj) { for (let prop in obj) { extended[prop] = obj[prop]; } }; // Loop through each passed argument for (let i = 0; i < length; i++) { // Store argument at position i let obj = arguments[i]; // If we are in fact dealing with an object, merge it. Otherwise throw error if (isType('Object', obj)) { merge(obj); } else { console.error('Custom options must be an object'); } } return extended; }; /* State */ isOpen() { } isDisabled() { } isEmpty() { return (this.valueCount.length === 0) ? true : false; } clearInput() { if (this.input.value) this.input.value = ''; } /* Event handling */ onKeyUp(e) { } onKeyDown(e) { const CTRLDOWN_KEY = e.ctrlKey || e.metaKey; const DELETE_KEY = 8 || 46; const ENTER_KEY = 13; const A_KEY = 65; // If CTRL + A or CMD + A have been pressed and there are items to select if (CTRLDOWN_KEY && e.keyCode === A_KEY && this.list && this.list.children) { let handleSelectAll = () => { if(this.options.removeItems) { for (let i = 0; i < this.list.children.length; i++) { let listItem = this.list.children[i]; // Select any items that have not already been selected if(!listItem.classList.contains('is-selected')) { listItem.classList.add('is-selected'); } } } }; handleSelectAll(); } // If enter key is pressed and the input has a value if (e.keyCode === ENTER_KEY && e.target.value) { let value = this.input.value; let handleENTER_KEY = () => { let canUpdate = true; // 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) { canUpdate = false; } // If no duplicates are allowed, and the value already exists // in the array, don't update if (this.options.allowDuplicates === false && this.element.value) { if (this.valueArray.indexOf(value) > -1) { canUpdate = false; } } // All is good, update if (canUpdate) { if(this.element.type === 'text') { this.addItem(this.list, value); this.updateInputValue(value); this.clearInput(this.element); this.unselectAll(this.list.children); } else { } } }; handleENTER_KEY(); } // If backspace or delete key is pressed and the input has no value if (e.keyCode === DELETE_KEY && !e.target.value) { let handleBackspaceKey = () => { if(this.options.removeItems) { let currentListItems = this.list.querySelectorAll('.choices__item'); let selectedItems = this.list.querySelectorAll('.is-selected'); let lastItem = currentListItems[currentListItems.length - 1]; if(lastItem) { lastItem.classList.add('is-selected'); } // 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 if(this.options.editItems && lastItem && selectedItems.length <= 1) { this.input.value = lastItem.innerHTML; this.removeItem(lastItem); } else { this.removeAll(currentListItems); } } }; handleBackspaceKey(); e.preventDefault(); } } onFocus(e) { } onClick(e) { } onChange(e) { } /* Event listeners */ addEventListeners(el) { el.addEventListener('click', this.onClick); el.addEventListener('keyup', this.onKeyUp); el.addEventListener('keydown', this.onKeyDown); el.addEventListener('change', this.onChange); el.addEventListener('focus', this.onFocus); el.addEventListener('blur', this.onBlur); } removeEventListeners(el) { el.removeEventListener('click', this.onClick); el.removeEventListener('keyup', this.onKeyUp); el.removeEventListener('keydown', this.onKeyDown); el.removeEventListener('change', this.onChange); el.removeEventListener('focus', this.onFocus); el.removeEventListener('blur', this.onBlur); } /* Methods */ setValue() {} getValue() {} getValues() {} getPlaceholder() {} updateInputValue(value) { if (this.options.debug) console.debug('Update input value'); // Push new value to array this.valueArray.push(value); // Caste array to string and set it as the hidden inputs value this.element.value = this.valueArray.join(this.options.delimiter); } removeInputValue(value) { if (this.options.debug) console.debug('Remove input value'); let index = this.valueArray.indexOf(value); this.valueArray.splice(index, 1); this.element.value = this.valueArray.join(this.options.delimiter); } addItem(parent, value) { if (this.options.debug) console.debug('Add item'); // // Create new list element let item = document.createElement('li'); item.classList.add('choices__item'); item.textContent = value; // Append it to list parent.appendChild(item); } unselectAll(items) { for (let i = 0; i < items.length; i++) { let item = items[i]; if (item.classList.contains('is-selected')) { item.classList.remove('is-selected'); } }; } removeAll(items) { for (let i = 0; i < items.length; i++) { let item = items[i]; if (item.classList.contains('is-selected')) { this.removeItem(item); this.removeInputValue(item.textContent); } }; } removeItem(item) { if (item) item.parentNode.removeChild(item); } init() { if (!this.supports) console.error('Your browser doesn\'nt support shit'); this.initialised = true; this.render(this.element); } renderTextInput() { // Template: // //