Handling option groups + label/value differentation

This commit is contained in:
Josh Johnson 2016-04-16 17:06:27 +01:00
parent fc0a72d2a2
commit f63652471c
12 changed files with 229 additions and 82 deletions

File diff suppressed because one or more lines are too long

View file

@ -1,7 +1,8 @@
export const addItem = (value, id, optionId) => {
export const addItem = (value, label, id, optionId) => {
return {
type: 'ADD_ITEM',
value: value,
label: label,
id: parseInt(id),
optionId: parseInt(optionId)
}
@ -23,11 +24,13 @@ export const selectItem = (id, selected) => {
}
};
export const addOption = (value, id) => {
export const addOption = (value, label, id, groupId) => {
return {
type: 'ADD_OPTION',
value: value,
label: label,
id: parseInt(id),
groupId: parseInt(groupId)
}
};
@ -37,4 +40,12 @@ export const selectOption = (id, selected) => {
id: parseInt(id),
selected: selected,
}
};
export const addGroup = (value, id) => {
return {
type: 'ADD_GROUP',
value: value,
id: parseInt(id)
}
};

View file

@ -2,8 +2,8 @@
import { createStore } from 'redux';
import rootReducer from './reducers/index.js';
import { addItem, removeItem, selectItem, addOption, selectOption } from './actions/index';
import { hasClass, wrap, getSiblings, isType, strToEl, extend } from './lib/utils.js';
import { addItem, removeItem, selectItem, addOption, selectOption, addGroup } from './actions/index';
import { hasClass, wrap, getSiblings, isType, strToEl, extend, getWidthOfInput } from './lib/utils.js';
/**
@ -61,6 +61,8 @@ export class Choices {
itemSelectable: 'choices__item--selectable',
itemDisabled: 'choices__item--disabled',
itemOption: 'choices__item--option',
group: 'choices__group',
groupHeading : 'choices__heading',
activeState: 'is-active',
disabledState: 'is-disabled',
hiddenState: 'is-hidden',
@ -125,6 +127,14 @@ export class Choices {
return (this.store.getState().items.length === 0) ? true : false;
}
hasSelectedItems() {
const items = this.getItems();
return items.some((item) => {
return item.selected === true;
});
}
/* Event handling */
/**
@ -143,6 +153,8 @@ export class Choices {
// If we are typing in the input
if(e.target === this.input) {
// this.input.style.width = getWidthOfInput(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) {
const handleSelectAll = () => {
@ -161,22 +173,24 @@ export class Choices {
const 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) {
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;
}
// If no duplicates are allowed, and the value already exists
// in the array, don't update
if (this.options.allowDuplicates === false && this.passedElement.value) {
canUpdate = !items.some((item) => {
return item.value === value;
});
}
// All is good, update
if (canUpdate && this.options.addItems) {
if (canUpdate) {
if(this.passedElement.type === 'text') {
let canAddItem = true;
@ -280,13 +294,16 @@ export class Choices {
if(!option.selected) {
this.selectOption(id, true);
this.addItem(option.value, option.id);
this.addItem(option.value, option.label, option.id);
}
}
} else {
// Click is outside of our element so close dropdown and de-select items
this.deselectAll();
if(this.hasSelectedItems()) {
this.deselectAll();
}
// If there is a dropdown and it is active
if(this.dropdown && this.dropdown.classList.contains(this.options.classNames.activeState)) {
this.toggleDropdown();
}
@ -392,10 +409,11 @@ export class Choices {
* Add item to store with correct value
* @param {String} value Value to add to store
*/
addItem(value, optionId = -1, callback = this.options.callbackOnAddItem) {
addItem(value, label, optionId = -1, callback = this.options.callbackOnAddItem) {
if (this.options.debug) console.debug('Add item');
let passedValue = value;
let passedValue = value.trim();
let passedLabel = label || passedValue;
// If a prepended value has been passed, prepend it
if(this.options.prependValue) {
@ -424,7 +442,7 @@ export class Choices {
}
}
this.store.dispatch(addItem(passedValue, id, optionId));
this.store.dispatch(addItem(passedValue, passedLabel, id, optionId));
}
/**
@ -485,21 +503,26 @@ export class Choices {
this.dropdown.classList[isActive ? 'remove' : 'add']('is-active');
}
addOptionToDropdown(option) {
addOption(option, groupId = -1) {
// Generate unique id
const state = this.store.getState();
const id = state.options.length + 1;
const value = option.value;
const label = option.innerHTML;
const isSelected = option.selected;
this.store.dispatch(addOption(value, id));
this.store.dispatch(addOption(value, label, id, groupId));
if(isSelected) {
this.selectOption(id);
this.addItem(option.value, id);
this.addItem(value, label, id);
}
}
addGroup(value, id) {
this.store.dispatch(addGroup(value, id));
}
/* Getters */
/**
@ -548,6 +571,11 @@ export class Choices {
const state = this.store.getState();
return state.options;
}
getGroups() {
const state = this.store.getState();
return state.groups;
}
/* Rendering */
@ -556,18 +584,6 @@ export class Choices {
* @return
*/
generateTextInput() {
/*
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>
*/
let containerOuter = strToEl(`<div class="${ this.options.classNames.containerOuter }"></div>`);
let containerInner = strToEl(`<div class="${ this.options.classNames.containerInner }"></div>`);
@ -591,7 +607,8 @@ export class Choices {
// ...and we have a value to set
const placeholderValue = this.options.placeholderValue || this.passedElement.placeholder;
if(placeholderValue) {
input.placeholder = placeholderValue;
input.placeholder = placeholderValue;
input.style.width = getWidthOfInput(input);
}
}
@ -650,7 +667,8 @@ export class Choices {
// If placeholder has been enabled and we have a value
if (this.options.placeholder && this.options.placeholderValue) {
input.placeholder = this.options.placeholderValue;
input.placeholder = this.options.placeholderValue;
input.style.width = getWidthOfInput(input);
}
if(!this.options.addItems) {
@ -668,10 +686,29 @@ export class Choices {
this.list = list;
this.dropdown = dropdown;
const passedOptions = Array.prototype.slice.call(this.passedElement.options);
passedOptions.forEach((option) => {
this.addOptionToDropdown(option);
});
// const passedGroups;
const passedGroups = Array.from(this.passedElement.getElementsByTagName('OPTGROUP'));
if(passedGroups.length) {
passedGroups.forEach((group, index) => {
const groupOptions = Array.from(group.getElementsByTagName('OPTION'));
const groupId = index;
this.addGroup(group.label, groupId);
groupOptions.forEach((option) => {
this.addOption(option, groupId);
});
});
} else {
const passedOptions = Array.from(this.passedElement.options);
passedOptions.forEach((option) => {
this.addOption(option);
});
}
// Subscribe to store
this.store.subscribe(this.render);
@ -725,6 +762,7 @@ export class Choices {
const classNames = this.options.classNames;
const items = this.getItems();
const options = this.getOptions();
const groups = this.getGroups();
// OPTIONS
if(this.dropdown) {
@ -734,16 +772,54 @@ export class Choices {
// Create a fragment to store our list items (so we don't have to update the DOM for each item)
const optionListFragment = document.createDocumentFragment();
// Add each option to dropdown
if(options) {
options.forEach((option) => {
const dropdownItem = strToEl(`<li class="${ classNames.item } ${ classNames.itemOption } ${ option.selected ? classNames.selectedState + ' ' + classNames.itemDisabled : classNames.itemSelectable }" data-choice-selectable data-choice-id="${ option.id }" data-choice-value="${ option.value }">${ option.value }</li>`);
optionListFragment.appendChild(dropdownItem);
// If we have grouped options
if(groups.length) {
groups.forEach((group) => {
const dropdownGroup = strToEl(`
<div class="${ classNames.group } ${ group.disabled ? classNames.itemDisabled : '' }" data-choice-value="${ group.value }" data-choice-group-id="${ group.id }">
<div class="${ classNames.groupHeading }">${ group.value }</div>
</div>
`);
const childOptions = options.filter((option) => {
return option.groupId === group.id;
});
if(childOptions) {
childOptions.forEach((option) => {
const dropdownItem = strToEl(`
<div class="${ classNames.item } ${ classNames.itemOption } ${ option.selected ? classNames.selectedState + ' ' + classNames.itemDisabled : classNames.itemSelectable }" data-choice-selectable data-choice-id="${ option.id }" data-choice-value="${ option.value }">
${ option.label }
</div>
`);
dropdownGroup.appendChild(dropdownItem);
});
} else {
const dropdownItem = strToEl(`<div class="${ classNames.item }">No options to select</div>`);
dropdownGroup.appendChild(dropdownItem);
}
optionListFragment.appendChild(dropdownGroup);
});
} else {
const dropdownItem = strToEl(`<li class="${ classNames.item }">No options to select</li>`);
optionListFragment.appendChild(dropdownItem);
if(options) {
options.forEach((option) => {
const dropdownItem = strToEl(`
<div class="${ classNames.item } ${ classNames.itemOption } ${ option.selected ? classNames.selectedState + ' ' + classNames.itemDisabled : classNames.itemSelectable }" data-choice-selectable data-choice-id="${ option.id }" data-choice-value="${ option.value }">
${ option.label }
</div>
`);
optionListFragment.appendChild(dropdownItem);
});
} else {
const dropdownItem = strToEl(`<div class="${ classNames.item }">No options to select</div>`);
optionListFragment.appendChild(dropdownItem);
}
}
this.dropdown.appendChild(optionListFragment);
}
@ -765,7 +841,11 @@ export class Choices {
items.forEach((item) => {
if(item.active) {
// Create new list element
const listItem = strToEl(`<li class="${ classNames.item } ${ this.options.removeItems ? classNames.itemSelectable : '' } ${ item.selected ? classNames.selectedState : '' }" data-choice-item data-choice-id="${ item.id }" data-choice-selected="${ item.selected }">${ item.value }</li>`);
const listItem = strToEl(`
<li class="${ classNames.item } ${ this.options.removeItems ? classNames.itemSelectable : '' } ${ item.selected ? classNames.selectedState : '' }" data-choice-item data-choice-id="${ item.id }" data-choice-selected="${ item.selected }">
${ item.label }
</li>
`);
// Append it to list
itemListFragment.appendChild(listItem);
@ -777,7 +857,7 @@ export class Choices {
// Run callback if it is a function
if(callback){
if(isType('Function', callback)) {
callback(items, options);
callback(items, options, groups);
} else {
console.error('callbackOnRender: Callback is not a function');
}
@ -889,8 +969,8 @@ document.addEventListener('DOMContentLoaded', () => {
const choicesMultiple = new Choices('[data-choice]', {
placeholderValue: 'This is a placeholder set in the config',
callbackOnRender: function(items, options) {
console.log(items);
callbackOnRender: function(items, options, groups) {
console.log(options);
},
});

View file

@ -371,15 +371,29 @@ export const strToEl = (function() {
}());
/**
* Calculates the width of a passed input based on its value
* Sets the width of a passed input based on its value
* @return {Number} Width of input
*/
export const getWidthOfInput = () => {
let tmp = document.createElement('span');
tmp.className = "tmp-element";
tmp.innerHTML = inputEl.value.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
document.body.appendChild(tmp);
let theWidth = tmp.getBoundingClientRect().width;
document.body.removeChild(tmp);
return theWidth;
export const getWidthOfInput = (input, initialWidth = 20) => {
const value = input.value || input.placeholder;
let width = input.offsetWidth;
if(value) {
const testEl = strToEl(`<span class="offscreen">${value}</span>`);
testEl.style.position = 'absolute';
testEl.style.top = '-9999px';
testEl.style.left = '-9999px';
testEl.style.padding = '0';
testEl.style.width = 'auto';
document.body.appendChild(testEl);
if(testEl.offsetWidth > initialWidth && testEl.offsetWidth != input.offsetWidth) {
width = testEl.offsetWidth + initialWidth;
}
document.body.removeChild(testEl);
}
return `${width}px`;
}

View file

@ -0,0 +1,15 @@
const groups = (state = [], action) => {
switch (action.type) {
case 'ADD_GROUP':
return [...state, {
id: parseInt(action.id),
value: action.value,
disabled: false,
}];
default:
return state;
}
}
export default groups;

View file

@ -1,9 +1,11 @@
import { combineReducers } from 'redux';
import items from './items';
import groups from './groups';
import options from './options';
const rootReducer = combineReducers({
items,
groups,
options
})

View file

@ -6,6 +6,7 @@ const items = (state = [], action) => {
id: action.id,
optionId: action.optionId,
value: action.value,
label: action.label,
active: true,
selected: false
}];

View file

@ -3,9 +3,11 @@ const options = (state = [], action) => {
case 'ADD_OPTION':
return [...state, {
id: parseInt(action.id),
groupId: action.groupId,
value: action.value,
label: action.label,
disabled: false,
selected: false
selected: false,
}];;
case 'SELECT_OPTION':

View file

@ -46,7 +46,8 @@ h1, h2, h3, h4, h5, h6 {
padding: .75rem .75rem .375rem;
border: 1px solid #DDDDDD;
border-radius: .25rem;
font-size: 1.4rem; }
font-size: 1.4rem;
cursor: text; }
.choices__inner:focus {
outline: 1px solid #00BCD4;
outline-offset: -1px; }
@ -67,7 +68,8 @@ h1, h2, h3, h4, h5, h6 {
margin-bottom: .375rem;
background-color: #00BCD4;
border: 1px solid #00b1c7;
color: #FFFFFF; }
color: #FFFFFF;
word-break: break-all; }
.choices__list--items .choices__item.is-selected {
background-color: #00a5bb; }
@ -85,12 +87,12 @@ h1, h2, h3, h4, h5, h6 {
.choices__list--dropdown .choices__item {
padding: 1rem;
font-size: 1.4rem; }
.choices__list--dropdown .choices__item:hover {
background-color: #f9f9f9; }
.choices__list--dropdown .choices__item.is-selected {
opacity: .5; }
.choices__list--dropdown .choices__item.is-selected:hover {
background-color: #FFFFFF; }
.choices__list--dropdown .choices__item--selectable:hover {
background-color: #f9f9f9; }
.choices__list--dropdown.is-active {
display: block; }
.choices__list--dropdown.is-flipped {
@ -116,6 +118,12 @@ h1, h2, h3, h4, h5, h6 {
-ms-user-select: none;
user-select: none; }
.choices__group .choices__heading {
font-size: 1.2rem;
padding: 1rem;
border-bottom: 1px solid #EAEAEA;
color: gray; }
.choices__input {
background-color: #f9f9f9;
font-size: 1.4rem;

View file

@ -1 +1 @@
*,:after,:before{box-sizing:border-box}body,html{margin:0;height:100%;widows:100%}html{font-size:62.5%}body{background-color:#333;font-family:"Helvetica Neue",Helvetica,Arial,"Lucida Grande",sans-serif;font-size:1.6rem;color:#fff}label{display:block;margin-bottom:.8rem;font-size:1.4rem;font-weight:500}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:1.2rem;font-weight:500}.container{display:block;margin:auto;max-width:35em;padding:2.4rem}.section{background-color:#fff;padding:2.4rem;color:#333}.choices{margin-bottom:2.4rem;position:relative}.choices__inner{background-color:#f9f9f9;padding:.75rem .75rem .375rem;border:1px solid #ddd;border-radius:.25rem;font-size:1.4rem}.choices__inner:focus{outline:1px solid #00bcd4;outline-offset:-1px}.choices__list{margin:0;padding-left:0;list-style-type:none}.choices__list--items{display:inline}.choices__list--items .choices__item{display:inline-block;border-radius:2rem;padding:.4rem 1rem;font-size:1.2rem;margin-right:.375rem;margin-bottom:.375rem;background-color:#00bcd4;border:1px solid #00b1c7;color:#fff}.choices__list--items .choices__item.is-selected{background-color:#00a5bb}.choices__list--dropdown{z-index:1;position:absolute;width:100%;background-color:#fff;border:1px solid #ddd;top:100%;margin-top:-1px;display:none;border-bottom-left-radius:.25rem;border-bottom-right-radius:.25rem}.choices__list--dropdown .choices__item{padding:1rem;font-size:1.4rem}.choices__list--dropdown .choices__item:hover{background-color:#f9f9f9}.choices__list--dropdown .choices__item.is-selected{opacity:.5}.choices__list--dropdown .choices__item.is-selected:hover{background-color:#fff}.choices__list--dropdown.is-active{display:block}.choices__list--dropdown.is-flipped{top:auto;bottom:100%;margin-top:0;margin-bottom:-1px;border-bottom-left-radius:0;border-bottom-right-radius:0;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.choices__item{cursor:default}.choices__item--selectable{cursor:pointer}.choices__item--disabled{cursor:not-allowed;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.choices__input{background-color:#f9f9f9;font-size:1.4rem;padding:0;margin-bottom:.5rem;display:inline-block;vertical-align:baseline;border:0;border-radius:0;max-width:100%;padding:.4rem 0 .4rem .2rem}.choices__input:focus{outline:0}
*,:after,:before{box-sizing:border-box}body,html{margin:0;height:100%;widows:100%}html{font-size:62.5%}body{background-color:#333;font-family:"Helvetica Neue",Helvetica,Arial,"Lucida Grande",sans-serif;font-size:1.6rem;color:#fff}label{display:block;margin-bottom:.8rem;font-size:1.4rem;font-weight:500}h1,h2,h3,h4,h5,h6{margin-top:0;margin-bottom:1.2rem;font-weight:500}.container{display:block;margin:auto;max-width:35em;padding:2.4rem}.section{background-color:#fff;padding:2.4rem;color:#333}.choices{margin-bottom:2.4rem;position:relative}.choices__inner{background-color:#f9f9f9;padding:.75rem .75rem .375rem;border:1px solid #ddd;border-radius:.25rem;font-size:1.4rem;cursor:text}.choices__inner:focus{outline:1px solid #00bcd4;outline-offset:-1px}.choices__list{margin:0;padding-left:0;list-style-type:none}.choices__list--items{display:inline}.choices__list--items .choices__item{display:inline-block;border-radius:2rem;padding:.4rem 1rem;font-size:1.2rem;margin-right:.375rem;margin-bottom:.375rem;background-color:#00bcd4;border:1px solid #00b1c7;color:#fff;word-break:break-all}.choices__list--items .choices__item.is-selected{background-color:#00a5bb}.choices__list--dropdown{z-index:1;position:absolute;width:100%;background-color:#fff;border:1px solid #ddd;top:100%;margin-top:-1px;display:none;border-bottom-left-radius:.25rem;border-bottom-right-radius:.25rem}.choices__list--dropdown .choices__item{padding:1rem;font-size:1.4rem}.choices__list--dropdown .choices__item.is-selected{opacity:.5}.choices__list--dropdown .choices__item.is-selected:hover{background-color:#fff}.choices__list--dropdown .choices__item--selectable:hover{background-color:#f9f9f9}.choices__list--dropdown.is-active{display:block}.choices__list--dropdown.is-flipped{top:auto;bottom:100%;margin-top:0;margin-bottom:-1px;border-bottom-left-radius:0;border-bottom-right-radius:0;border-top-left-radius:.25rem;border-top-right-radius:.25rem}.choices__item{cursor:default}.choices__item--selectable{cursor:pointer}.choices__item--disabled{cursor:not-allowed;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.choices__group .choices__heading{font-size:1.2rem;padding:1rem;border-bottom:1px solid #eaeaea;color:gray}.choices__input{background-color:#f9f9f9;font-size:1.4rem;padding:0;margin-bottom:.5rem;display:inline-block;vertical-align:baseline;border:0;border-radius:0;max-width:100%;padding:.4rem 0 .4rem .2rem}.choices__input:focus{outline:0}

View file

@ -58,6 +58,7 @@ h1, h2, h3, h4, h5, h6 {
border: 1px solid #DDDDDD;
border-radius: .25rem;
font-size: 1.4rem;
cursor: text;
&:focus {
outline: 1px solid #00BCD4;
outline-offset: -1px;
@ -84,6 +85,7 @@ h1, h2, h3, h4, h5, h6 {
background-color: #00BCD4;
border: 1px solid darken(#00BCD4, 2.5%);
color: #FFFFFF;
word-break: break-all;
&.is-selected { background-color: darken(#00BCD4, 5%); }
}
}
@ -102,12 +104,15 @@ h1, h2, h3, h4, h5, h6 {
.choices__item {
padding: 1rem;
font-size: 1.4rem;
&:hover { background-color: mix(#000000, #FFFFFF, 2.5%); }
&.is-selected {
opacity: .5;
&:hover { background-color: #FFFFFF; }
}
}
.choices__item--selectable {
&:hover { background-color: mix(#000000, #FFFFFF, 2.5%); }
}
&.is-active { display: block; }
&.is-flipped {
top: auto;
@ -128,6 +133,15 @@ h1, h2, h3, h4, h5, h6 {
user-select: none;
}
.choices__group {
.choices__heading {
font-size: 1.2rem;
padding: 1rem;
border-bottom: 1px solid #EAEAEA;
color: lighten(#333, 30%);
}
}
.choices__input {
background-color: mix(#000000, #FFFFFF, 2.5%);
font-size: 1.4rem;

View file

@ -36,7 +36,7 @@
<option value="Dropdown item 4" disabled="disabled">Dropdown item 4</option>
</select>
<label for="choices-8">Select box</label>
<!-- <label for="choices-8">Select box</label>
<select id="choices-8" name="choices-8" data-choice placeholder="This is a placeholder" multiple>
<option value="Dropdown item 1">Dropdown item 1</option>
<option value="Dropdown item 2">Dropdown item 2</option>
@ -48,24 +48,24 @@
<option value="Dropdown item 1">Dropdown item 1</option>
<option value="Dropdown item 2" selected>Dropdown item 2</option>
<option value="Dropdown item 3">Dropdown item 3</option>
</select>
</select> -->
<label for="choices-10">Select box with pre-selected option</label>
<label for="choices-10">Select box with option groups</label>
<select id="choices-10" name="choices-10" data-choice placeholder="This is a placeholder" multiple>
<optgroup label="Group 1">
<option value="Dropdown item 1">Dropdown item 1</option>
<option value="Dropdown item 2">Dropdown item 2</option>
<option value="Dropdown item 3">Dropdown item 3</option>
<option value="Value 1">Dropdown item 1</option>
<option value="Value 2">Dropdown item 2</option>
<option value="Value 3">Dropdown item 3</option>
</optgroup>
<optgroup label="Group 2">
<option value="Dropdown item 4">Dropdown item 4</option>
<option value="Dropdown item 5">Dropdown item 5</option>
<option value="Dropdown item 6">Dropdown item 6</option>
<option value="Value 4">Dropdown item 4</option>
<option value="Value 5">Dropdown item 5</option>
<option value="Value 6">Dropdown item 6</option>
</optgroup>
<optgroup label="Group 3" disabled>
<option value="Dropdown item 4">Dropdown item 7</option>
<option value="Dropdown item 5">Dropdown item 8</option>
<option value="Dropdown item 6">Dropdown item 9</option>
<option value="Value 7">Dropdown item 7</option>
<option value="Value 8">Dropdown item 8</option>
<option value="Value 9">Dropdown item 9</option>
</optgroup>
</select>
</div>