Handle click events on list items + set active status in store + set select status in store

This commit is contained in:
Josh Johnson 2016-04-03 23:07:10 +01:00
parent c4598aeccb
commit 174aa399c2
6 changed files with 213 additions and 258 deletions

File diff suppressed because one or more lines are too long

View file

@ -5,18 +5,19 @@ export const addItemToStore = (value, element, id) => {
element: element,
id: id
}
}
};
export const removeItemFromStore = (id) => {
return {
type: 'REMOVE_ITEM',
id: id,
id: id
}
}
};
export const updateItemInStore = (value) => {
export const selectItemFromStore = (id, value) => {
return {
type: 'UPDATE_ITEM',
type: 'SELECT_ITEM',
id: id,
value: value
}
}
};

View file

@ -2,8 +2,8 @@
import { createStore } from 'redux';
import choices from './reducers/index.js';
import { addItemToStore, removeItemFromStore } from './actions/index';
import { hasClass, wrap, getSiblings, isType } from './lib/utils.js';
import { addItemToStore, removeItemFromStore, selectItemFromStore } from './actions/index';
import { hasClass, wrap, getSiblings, isType, strToEl } from './lib/utils.js';
/**
@ -18,10 +18,10 @@ import { hasClass, wrap, getSiblings, isType } from './lib/utils.js';
export class Choices {
constructor(options) {
const FAKE_EL = document.createElement("fakeel");
const USER_OPTIONS = options || {};
const STORE = createStore(choices);
const DEFAULT_OPTIONS = {
const fakeEl = document.createElement("fakeel");
const userOptions = options || {};
const store = createStore(choices);
const defaultOptions = {
element: document.querySelector('[data-choice]'),
disabled: false,
addItems: true,
@ -35,6 +35,7 @@ export class Choices {
placeholder: false,
prependValue: false,
appendValue: false,
selectAll: true,
callbackOnInit: function() {},
callbackOnRender: function() {},
callbackOnRemoveItem: function() {},
@ -42,27 +43,25 @@ export class Choices {
};
// Merge options with user options
this.options = this.extend(DEFAULT_OPTIONS, USER_OPTIONS || {});
this.store = STORE;
this.options = this.extend(defaultOptions, userOptions || {});
this.store = store;
this.initialised = false;
this.supports = 'querySelector' in document && 'addEventListener' in document && 'classList' in FAKE_EL;
this.supports = 'querySelector' in document && 'addEventListener' in document && 'classList' in fakeEl;
// Retrieve elements
// Retrieve triggering element (i.e. element with 'data-choice' trigger)
this.element = this.options.element;
// If input already has values, parse the array, otherwise create a blank array
// Hmm, this should really map this.store
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.onClick = this.onClick.bind(this);
this.init();
}
@ -93,7 +92,7 @@ export class Choices {
// Loop through each passed argument
for (let i = 0; i < length; i++) {
// Store argument at position i
// store argument at position i
let obj = arguments[i];
// If we are in fact dealing with an object, merge it. Otherwise throw error
@ -131,74 +130,74 @@ export class Choices {
}
onKeyDown(e) {
const CTRLDOWN_KEY = e.ctrlKey || e.metaKey;
const DELETE_KEY = 8 || 46;
const ENTER_KEY = 13;
const A_KEY = 65;
const ctrlDownKey = e.ctrlKey || e.metaKey;
const deleteKey = 8 || 46;
const enterKey = 13;
const aKey = 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 && !this.input.value) {
this.selectAll(this.list.children);
}
};
// If we are typing in the input
if(e.target === this.input) {
// 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 = () => {
if(this.options.removeItems && !this.input.value && this.options.selectAll) {
this.selectAll(this.list.children);
}
};
handleSelectAll();
}
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;
// If enter key is pressed and the input has a value
if (e.keyCode === enterKey && e.target.value) {
let value = this.input.value;
let handleEnter = () => {
let canUpdate = true;
let handleEnter = () => {
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) {
// 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;
}
}
// All is good, update
if (canUpdate) {
if(this.element.type === 'text') {
let canAddItem = true;
// 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;
}
}
// 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, update
if (canUpdate) {
if(this.element.type === 'text') {
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(this.list, value);
this.updateInputValue(value);
this.clearInput(this.element);
}
}
// All is good, add
if(canAddItem) {
this.addItem(this.list, value);
this.updateInputValue(value);
this.clearInput(this.element);
this.unselectAll(this.list.children);
}
} else {
}
}
};
};
handleEnter();
handleEnter();
}
}
// If backspace or delete key is pressed and the input has no value
if (e.keyCode === DELETE_KEY && !e.target.value) {
if (e.keyCode === deleteKey && !e.target.value) {
let handleBackspaceKey = () => {
if(this.options.removeItems) {
@ -206,14 +205,14 @@ export class Choices {
let selectedItems = this.list.querySelectorAll('.is-selected');
let lastItem = currentListItems[currentListItems.length - 1];
if(lastItem) {
lastItem.classList.add('is-selected');
if(lastItem && !this.options.editItems) {
this.selectItem(lastItem);
}
// 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) {
if(this.options.editItems && lastItem && selectedItems.length === 1) {
this.input.value = lastItem.innerHTML;
this.removeItem(lastItem);
} else {
@ -228,36 +227,30 @@ export class Choices {
}
}
onFocus(e) {
}
onClick(e) {
if(e.target.tagName === 'LI') {
let item = e.target;
}
let handleClick = (item) => {
let passedId = item.getAttribute('data-choice-id');
let items = this.list.children;
onChange(e) {
// We only want to select one item with a click
// so we unselect any items that aren't the target
for (var i = 0; i < items.length; i++) {
let singleItem = items[i];
let id = singleItem.getAttribute('data-choice-id');;
}
/* 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);
if(id === passedId && !singleItem.classList.contains('is-selected')) {
this.selectItem(singleItem);
} else {
this.unselectItem(singleItem);
}
}
}
handleClick(item);
}
}
/* Methods */
@ -274,26 +267,24 @@ export class Choices {
return passesTest;
}
getPlaceholder() {}
selectItem(item) {
let id = item.getAttribute('data-choice-id');
item.classList.add('is-selected');
this.store.dispatch(selectItemFromStore(id, true));
console.log(this.store.getState());
}
unselectItem(item) {
let id = item.getAttribute('data-choice-id');
item.classList.remove('is-selected');
this.store.dispatch(selectItemFromStore(id, false));
console.log(this.store.getState());
}
selectAll(items) {
for (let i = 0; i < items.length; i++) {
let item = items[i];
if (!item.classList.contains('is-selected')) {
item.classList.add('is-selected');
}
};
}
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');
}
this.selectItem(item);
};
}
@ -333,11 +324,8 @@ export class Choices {
let id = this.store.getState().length + 1;
// Create new list element
let item = document.createElement('li');
item.classList.add('choices__item');
item.textContent = passedValue;
item.id = id;
// Create new list element
let item = strToEl(`<li class="choices__item" data-choice-id=${id}>${passedValue}</li>`);
// Append it to list
parent.appendChild(item);
@ -361,7 +349,7 @@ export class Choices {
return;
}
let id = item.id;
let id = item.getAttribute('data-choice-id');
let value = item.innerHTML;
item.parentNode.removeChild(item);
@ -396,21 +384,20 @@ export class Choices {
}
renderTextInput() {
// 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>
/*
Template:
let containerOuter = document.createElement('div');
containerOuter.className = 'choices choices--active';
<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>
*/
let containerInner = document.createElement('div');
containerInner.className = 'choices__inner';
let containerOuter = strToEl('<div class="choices choices--active"></div>');
let containerInner = strToEl('<div class="choices__inner"></div>');
// Hide passed input
this.element.classList.add('choices__input', 'choices__input--hidden');
@ -424,12 +411,8 @@ export class Choices {
// Wrapper inner container with outer container
wrap(containerInner, containerOuter);
let list = document.createElement('ul');
list.className = 'choices__list choices__list--items';
let input = document.createElement('input');
input.type = 'text';
input.className = 'choices__input choices__input--cloned';
let list = strToEl('<ul class="choices__list choices__list--items"></ul>');
let input = strToEl('<input type="text" class="choices__input choices__input--cloned">');
if (input.placeholder) {
input.placeholder = this.element.placeholder;
@ -456,70 +439,10 @@ export class Choices {
}
// Trigger event listeners
this.addEventListeners(this.input);
document.addEventListener('keydown', this.onKeyDown);
this.list.addEventListener('click', this.onClick);
}
renderSelectInput() {
let containerOuter = document.createElement('div');
containerOuter.className = 'choices choices--active';
let containerInner = document.createElement('div');
containerInner.className = 'choices__inner';
// Hide passed input
this.element.classList.add('choices__input', 'choices__input--hidden');
this.element.tabIndex = '-1';
this.element.setAttribute('style', 'display:none;');
this.element.setAttribute('aria-hidden', 'true');
// Wrap input in container preserving DOM ordering
wrap(this.element, containerInner);
// Wrapper inner container with outer container
wrap(containerInner, containerOuter);
let options = document.createElement('ul');
options.className = 'choices__list choices__list--options';
let input = document.createElement('input');
input.type = 'text';
input.className = 'choices__input choices__input--cloned';
containerInner.appendChild(input);
containerInner.appendChild(options);
containerOuter.appendChild(containerInner);
this.containerOuter = containerOuter;
this.containerInner = containerInner;
this.input = input;
this.list = null;
this.options = options;
let initialOptions = this.element.children;
if (initialOptions) {
for (let i = 0; i < initialOptions.length; i++) {
let parentOption = initialOptions[i];
if(parentOption.tagName === 'OPTGROUP') {
this.addItem(this.options, parentOption.label);
for (let j = 0; j < parentOption.children.length; j++) {
let childOption = parentOption.children[j];
this.addItem(this.options, childOption.innerHTML);
}
} else if(parentOption.tagName === 'OPTION') {
this.addItem(this.options, parentOption.innerHTML);
}
}
}
// Trigger event listeners
this.addEventListeners(this.input);
}
renderMultipleSelectInput() {
}
render() {
if (this.options.debug) console.debug('Render');
@ -529,13 +452,13 @@ export class Choices {
this.renderTextInput();
break;
case "select-one":
this.renderSelectInput();
// this.renderSelectInput();
break;
case "select-multiple":
this.renderMultipleSelectInput();
// this.renderMultipleSelectInput();
break;
default:
rthis.renderTextInput();
this.renderTextInput();
break;
}
@ -558,8 +481,8 @@ export class Choices {
let choices1 = new Choices({
element : input1,
delimiter: ' ',
maxItems: 5,
// delimiter: ' ',
// maxItems: 5,
// callbackOnRemoveItem: function(value) {
// console.log(value);
// },
@ -568,27 +491,27 @@ export class Choices {
// }
});
let choices2 = new Choices({
element : input2,
allowDuplicates: false,
editItems: true,
});
// 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 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
});
// let choices4 = new Choices({
// element : input4,
// addItems: false
// });
let choices5 = new Choices({
element: input5,
prependValue: 'item-',
appendValue: `-${Date.now()}`
});
// let choices5 = new Choices({
// element: input5,
// prependValue: 'item-',
// appendValue: `-${Date.now()}`
// });
})();

View file

@ -1,4 +1,4 @@
export let hasClass = (elem, className) => {
export const hasClass = (elem, className) => {
return new RegExp(' ' + className + ' ').test(' ' + elem.className + ' ');
}
@ -7,7 +7,7 @@ export let hasClass = (elem, className) => {
* @param {String} str String to capitalise
* @return {String} Capitalised string
*/
export let capitalise = function(str) {
export const capitalise = function(str) {
return str.replace(/\w\S*/g, function(txt){
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
});
@ -19,7 +19,7 @@ export let capitalise = function(str) {
* @param {Object} obj Object to be tested
* @return {Boolean}
*/
export let isType = function(type, obj) {
export const isType = function(type, obj) {
var clas = Object.prototype.toString.call(obj).slice(8, -1);
return obj !== undefined && obj !== null && clas === type;
};
@ -29,7 +29,7 @@ export let isType = function(type, obj) {
* CSS transition end event listener
* @return
*/
export let whichTransitionEvent = function(){
export const whichTransitionEvent = function(){
var t,
el = document.createElement("fakeelement");
@ -51,7 +51,7 @@ export let whichTransitionEvent = function(){
* CSS animation end event listener
* @return
*/
export let whichAnimationEvent = function() {
export const whichAnimationEvent = function() {
var t,
el = document.createElement('fakeelement');
@ -77,7 +77,7 @@ export let whichAnimationEvent = function() {
* @param {String} selector Class to find
* @return {Array} Array of parent elements
*/
export let getParentsUntil = function(elem, parent, selector) {
export const getParentsUntil = function(elem, parent, selector) {
var parents = [];
// Get matches
for (; elem && elem !== document; elem = elem.parentNode) {
@ -156,7 +156,7 @@ export let getParentsUntil = function(elem, parent, selector) {
}
};
export let wrap = function (element, wrapper) {
export const wrap = function (element, wrapper) {
wrapper = wrapper || document.createElement('div');
if (element.nextSibling) {
element.parentNode.insertBefore(wrapper, element.nextSibling);
@ -166,7 +166,7 @@ export let wrap = function (element, wrapper) {
return wrapper.appendChild(element);
};
export let getSiblings = function (elem) {
export const getSiblings = function (elem) {
var siblings = [];
var sibling = elem.parentNode.firstChild;
for ( ; sibling; sibling = sibling.nextSibling ) {
@ -183,7 +183,7 @@ export let getSiblings = function (elem) {
* @param {[type]} cls Class of parent
* @return {NodeElement} Found parent element
*/
export let findAncestor = function(el, cls) {
export const findAncestor = function(el, cls) {
while ((el = el.parentElement) && !el.classList.contains(cls));
return el;
};
@ -195,7 +195,7 @@ export let findAncestor = function(el, cls) {
* @param {Boolean} immediate If passed, trigger the function on the leading edge, instead of the trailing.
* @return {Function} A function will be called after it stops being called for a given delay
*/
export let debounce = function(func, wait, immediate) {
export const debounce = function(func, wait, immediate) {
var timeout;
return function() {
var context = this,
@ -217,7 +217,7 @@ export let debounce = function(func, wait, immediate) {
* @param {NodeElement} el Element to test for
* @return {Number} Elements Distance from top of page
*/
export let getElemDistance = function(el) {
export const getElemDistance = function(el) {
var location = 0;
if (el.offsetParent) {
do {
@ -234,7 +234,7 @@ export let getElemDistance = function(el) {
* @param {Node} el Element to test for
* @return {Number} Height of element
*/
export let getElementOffset = function(el, offset) {
export const getElementOffset = function(el, offset) {
var elOffset = offset;
if(elOffset > 1) elOffset = 1;
if(elOffset > 0) elOffset = 0;
@ -247,7 +247,7 @@ export let getElementOffset = function(el, offset) {
* @private
* @return {String} Position of scroll
*/
export let getScrollPosition = function(position) {
export const getScrollPosition = function(position) {
if(position === 'bottom') {
// Scroll position from the bottom of the viewport
return Math.max((window.scrollY || window.pageYOffset) + (window.innerHeight || document.documentElement.clientHeight));
@ -263,7 +263,7 @@ export let getScrollPosition = function(position) {
* @return {String} Position of scroll
* @return {Boolean}
*/
export let isInView = function(el, position, offset) {
export const isInView = function(el, position, offset) {
// If the user has scrolled further than the distance from the element to the top of its parent
return this.getScrollPosition(position) > (this.getElemDistance(el) + this.getElementOffset(el, offset)) ? true : false;
};
@ -273,7 +273,7 @@ export let isInView = function(el, position, offset) {
* @param {String} Initial string/html
* @return {String} Sanitised string
*/
export let stripHTML = function(html) {
export const stripHTML = function(html) {
let el = document.createElement("DIV");
el.innerHTML = html;
return el.textContent || el.innerText || "";
@ -285,7 +285,7 @@ export let stripHTML = function(html) {
* @param {String} animation Animation class to add to element
* @return
*/
export let addAnimation = (el, animation) => {
export const addAnimation = (el, animation) => {
let animationEvent = whichAnimationEvent();
let removeAnimation = () => {
@ -304,6 +304,26 @@ export let addAnimation = (el, animation) => {
* @param {Number} max Maximum range
* @return {Number} Random number
*/
export let getRandomNumber = function(min, max) {
export const getRandomNumber = function(min, max) {
return Math.floor(Math.random() * (max - min) + min);
}
}
/**
* Turn a string into a node
* @param {String} String to convert
* @return {Node} Converted node element
*/
export const strToEl = (function() {
var tmpEl = document.createElement('div');
return function(str) {
var r;
tmpEl.innerHTML = str;
r = tmpEl.children[0];
while (tmpEl.firstChild) {
tmpEl.removeChild(tmpEl.firstChild);
}
return r;
};
}());

View file

@ -1,4 +1,3 @@
// Array of choices
const choices = (state = [], action) => {
switch (action.type) {
case 'ADD_ITEM':
@ -7,17 +6,29 @@ const choices = (state = [], action) => {
id: parseInt(action.id),
value: action.value,
element: action.element,
active: true
active: true,
selected: false
}];
case 'REMOVE_ITEM':
// Remove item from items array
return state.filter(function(item) {
if(item.id !== parseInt(action.id)) {
return item;
// Set item to inactive
return state.map((item) => {
if(item.id === parseInt(action.id)) {
item.active = false;
}
return item;
});
case 'SELECT_ITEM':
return state.map((item) => {
if(item.id === parseInt(action.id)) {
item.selected = action.value;
}
return item;
});
default:
return state;
}

View file

@ -7,8 +7,8 @@
</head>
<body>
<label for="1">Text input with no values and a limit of 5 items</label>
<input id="1" type="text" data-choice>
<input id="1" type="text" data-choice value="preset-1, preset-2">
<!--
<label for="2">Text input with preset values, custom classes and a placeholder. No duplicate values allowed</label>
<input id="2" type="text" data-choice value="preset-1, preset-2" placeholder="This is a placeholder" class="custom class">
@ -19,7 +19,7 @@
<input id="4" type="text" data-choice value="josh@joshuajohnson.co.uk, joe@bloggs.co.uk" placeholder="This is a placeholder">
<label for="5">Text input that prepends and appends a value to each item</label>
<input id="5" type="text" data-choice value="preset-1, preset-2" placeholder="This is a placeholder">
<input id="5" type="text" data-choice value="preset-1, preset-2" placeholder="This is a placeholder"> -->
<!-- <label for="3">Select input with two options</label>
<select id="3" name="3" data-choice>