Use DOM querying for highlighting options - less rendering

This commit is contained in:
Josh Johnson 2016-04-28 15:30:43 +01:00
parent 9dc23e86bf
commit cbfe38e937
4 changed files with 89 additions and 106 deletions

File diff suppressed because one or more lines are too long

View file

@ -24,33 +24,17 @@ export const selectItem = (id, selected) => {
} }
}; };
export const addOption = (value, label, id, groupId, highlighted, disabled) => { export const addOption = (value, label, id, groupId, disabled) => {
return { return {
type: 'ADD_OPTION', type: 'ADD_OPTION',
value, value,
label, label,
id, id,
groupId, groupId,
highlighted,
disabled, disabled,
} }
}; };
export const selectOption = (id, selected) => {
return {
type: 'SELECT_OPTION',
id,
selected,
}
};
export const highlightOption = (id) => {
return {
type: 'HIGHLIGHT_OPTION',
id,
}
};
export const filterOptions = (results) => { export const filterOptions = (results) => {
return { return {
type: 'FILTER_OPTIONS', type: 'FILTER_OPTIONS',

View file

@ -2,7 +2,7 @@
import { createStore } from 'redux'; import { createStore } from 'redux';
import rootReducer from './reducers/index.js'; import rootReducer from './reducers/index.js';
import { addItem, removeItem, selectItem, addOption, selectOption, highlightOption, filterOptions, activateOptions, addGroup } from './actions/index'; import { addItem, removeItem, selectItem, addOption, filterOptions, activateOptions, addGroup } from './actions/index';
import { hasClass, wrap, getSiblings, isType, strToEl, extend, getWidthOfInput, debounce } from './lib/utils.js'; import { hasClass, wrap, getSiblings, isType, strToEl, extend, getWidthOfInput, debounce } from './lib/utils.js';
import Sifter from 'sifter'; import Sifter from 'sifter';
@ -249,13 +249,14 @@ export class Choices {
const upKey = 38; const upKey = 38;
const downKey = 40; const downKey = 40;
const hasActiveDropDown = this.dropdown && this.dropdown.classList.contains(this.options.classNames.activeState); const hasActiveDropDown = this.dropdown && this.dropdown.classList.contains(this.options.classNames.activeState);
const hasItems = this.list && this.list.children;
// If we are typing in the input // If we are typing in the input
if(e.target === this.input) { if(e.target === this.input) {
// this.input.style.width = getWidthOfInput(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 CTRL + A or CMD + A have been pressed and there are items to select
if (ctrlDownKey && e.keyCode === aKey && this.list && this.list.children) { if (ctrlDownKey && e.keyCode === aKey && hasItems) {
this.handleSelectAll(); this.handleSelectAll();
} }
@ -267,22 +268,55 @@ export class Choices {
} }
if(this.passedElement.type === 'select-multiple' && hasActiveDropDown) { if(this.passedElement.type === 'select-multiple' && hasActiveDropDown) {
const highlighted = this.getOptionsFilteredByHighlighted(); const highlighted = this.dropdown.querySelector(`.${this.options.classNames.highlightedState}`);
const value = highlighted.getAttribute('data-choice-value');
const label = highlighted.innerHTML;
const id = highlighted.getAttribute('data-choice-id');
if(highlighted) { if(highlighted) {
this.addItem(highlighted.value, highlighted.label, highlighted.id); this.addItem(value, label, id);
this.input.value = ""; this.input.value = "";
// this.highlightPosition()
} }
} }
} }
if(e.keyCode === escapeKey && hasActiveDropDown) { if(e.keyCode === escapeKey && hasActiveDropDown) {
this.toggleDropdown(); if(this.passedElement.type === 'select-multiple' && hasActiveDropDown) {
this.toggleDropdown();
}
} }
if((e.keyCode === downKey || e.keyCode === upKey) && hasActiveDropDown) { if(e.keyCode === downKey || e.keyCode === upKey) {
let option = activeOptions[0]; if(this.passedElement.type === 'select-multiple' && hasActiveDropDown) {
this.highlightOption(option.id); const selectableOptions = activeOptions.filter((option) => {
return !option.selected;
});
let canHighlight = true;
if(e.keyCode === downKey) {
this.highlightPosition < (selectableOptions.length - 1) ? this.highlightPosition++ : canHighlight = false;
} else if(e.keyCode === upKey) {
this.highlightPosition > 0 ? this.highlightPosition-- : canHighlight = false;
}
if(canHighlight) {
const option = selectableOptions[this.highlightPosition];
const previousElement = this.dropdown.querySelector(`.${this.options.classNames.highlightedState}`);
const currentElement = this.dropdown.querySelector(`[data-choice-id="${option.id}"]`);
if(previousElement) {
previousElement.classList.remove(this.options.classNames.highlightedState);
}
if(currentElement) {
currentElement.classList.add(this.options.classNames.highlightedState);
}
}
}
} }
} }
if(inputIsFocussed) { if(inputIsFocussed) {
@ -297,26 +331,28 @@ export class Choices {
onKeyUp(e) { onKeyUp(e) {
if(e.target === this.input) { if(e.target === this.input) {
if(this.passedElement.type === 'select-multiple' && this.options.allowSearch) { if(this.passedElement.type === 'select-multiple' && this.options.allowSearch) {
if(this.input.value) { const charStr = String.fromCharCode(e.keyCode);
// If we have a value, filter options based on it if(this.input === document.activeElement && /[a-z0-9]/i.test(charStr)) {
const handleFilter = debounce(() => { if(this.input.value) {
const options = this.getOptionsFiltedBySelectable(); // If we have a value, filter options based on it
const sifter = new Sifter(options); const handleFilter = debounce(() => {
const results = sifter.search(this.input.value, { const options = this.getOptionsFiltedBySelectable();
fields: ['label', 'value'], const sifter = new Sifter(options);
sort: [{field: 'value', direction: 'asc'}], const results = sifter.search(this.input.value, {
limit: 10 fields: ['label', 'value'],
}); sort: [{field: 'value', direction: 'asc'}],
this.store.dispatch(filterOptions(results)); limit: 10
}, 100) });
this.store.dispatch(filterOptions(results));
handleFilter(); }, 100)
} else {
// Otherwise reset options to active handleFilter();
this.store.dispatch(activateOptions()); } else {
// Otherwise reset options to active
this.store.dispatch(activateOptions());
}
} }
} }
} }
} }
@ -355,7 +391,6 @@ export class Choices {
}); });
if(!option.selected) { if(!option.selected) {
this.selectOption(id, true);
this.addItem(option.value, option.label, option.id); this.addItem(option.value, option.label, option.id);
} }
} }
@ -471,16 +506,6 @@ export class Choices {
}); });
} }
selectOption(id, value = true) {
if(!id) return;
this.input.value = '';
this.store.dispatch(selectOption(id, value));
}
highlightOption(id) {
if(!id) return;
this.store.dispatch(highlightOption(id));
}
/** /**
* Add item to store with correct value * Add item to store with correct value
@ -621,7 +646,7 @@ export class Choices {
} }
} }
addOption(option, groupId = -1, highlighted = false, disabled = false) { addOption(option, groupId = -1, disabled = false) {
// Generate unique id // Generate unique id
const state = this.store.getState(); const state = this.store.getState();
const id = state.options.length + 1; const id = state.options.length + 1;
@ -629,10 +654,9 @@ export class Choices {
const label = option.innerHTML; const label = option.innerHTML;
const isSelected = option.selected; const isSelected = option.selected;
this.store.dispatch(addOption(value, label, id, groupId, highlighted, disabled)); this.store.dispatch(addOption(value, label, id, groupId, disabled));
if(isSelected) { if(isSelected) {
this.selectOption(id);
this.addItem(value, label, id); this.addItem(value, label, id);
} }
} }
@ -693,20 +717,12 @@ export class Choices {
getOptionsFilteredByActive() { getOptionsFilteredByActive() {
const options = this.getOptions(); const options = this.getOptions();
const valueArray = options.filter((option) => { const valueArray = options.filter((option) => {
return option.active === true && option.disabled === false; return option.active === true && option.disabled === false && option.selected !== true;
},[]); },[]);
return valueArray; return valueArray;
} }
getOptionsFilteredByHighlighted() {
const options = this.getOptions();
const value = options.find((option) => {
return option.highlighted === true;
});
return value;
}
getOptionsFiltedBySelectable() { getOptionsFiltedBySelectable() {
const options = this.getOptions(); const options = this.getOptions();
const valueArray = options.filter((option) => { const valueArray = options.filter((option) => {
@ -835,6 +851,7 @@ export class Choices {
this.input = input; this.input = input;
this.list = list; this.list = list;
this.dropdown = dropdown; this.dropdown = dropdown;
this.highlightPosition = 0;
const passedGroups = Array.from(this.passedElement.getElementsByTagName('OPTGROUP')); const passedGroups = Array.from(this.passedElement.getElementsByTagName('OPTGROUP'));
@ -851,9 +868,9 @@ export class Choices {
// If group is disabled, disable all of its children // If group is disabled, disable all of its children
if(group.disabled) { if(group.disabled) {
this.addOption(option, groupId, highlighted, true); this.addOption(option, groupId, true);
} else { } else {
this.addOption(option, groupId, highlighted); this.addOption(option, groupId);
} }
}); });
} else { } else {
@ -901,13 +918,13 @@ export class Choices {
* @return * @return
*/ */
render(callback = this.options.callbackOnRender) { render(callback = this.options.callbackOnRender) {
console.log('Rendering');
const classNames = this.options.classNames; const classNames = this.options.classNames;
const activeItems = this.getItemsFilteredByActive(); const activeItems = this.getItemsFilteredByActive();
// OPTIONS // OPTIONS
if(this.passedElement.type === 'select-multiple') { if(this.passedElement.type === 'select-multiple') {
const activeOptions = this.getOptionsFilteredByActive(); const activeOptions = this.getOptionsFilteredByActive();
const activeGroups = this.getGroupsFilteredByActive(); const activeGroups = this.getGroupsFilteredByActive();
@ -919,7 +936,6 @@ export class Choices {
// If we have grouped options // If we have grouped options
if(activeGroups.length >= 1) { if(activeGroups.length >= 1) {
activeGroups.forEach((group, index) => { activeGroups.forEach((group, index) => {
// Grab options that are children of this group // Grab options that are children of this group
const groupOptions = activeOptions.filter((option) => { const groupOptions = activeOptions.filter((option) => {
@ -934,7 +950,7 @@ export class Choices {
groupOptions.forEach((option) => { groupOptions.forEach((option) => {
const dropdownItem = strToEl(` const dropdownItem = strToEl(`
<div class="${ classNames.item } ${ classNames.itemOption } ${ option.selected ? classNames.selectedState + ' ' + classNames.itemDisabled : classNames.itemSelectable } ${ option.highlighted ? classNames.highlightedState : '' }" data-choice-option data-choice-id="${ option.id }" data-choice-value="${ option.value }"> <div class="${ classNames.item } ${ classNames.itemOption } ${ option.selected ? classNames.selectedState + ' ' + classNames.itemDisabled : classNames.itemSelectable }" data-choice-option data-choice-id="${ option.id }" data-choice-value="${ option.value }">
${ option.label } ${ option.label }
</div> </div>
`); `);
@ -948,7 +964,7 @@ export class Choices {
} else if(activeOptions.length >= 1) { } else if(activeOptions.length >= 1) {
activeOptions.forEach((option) => { activeOptions.forEach((option) => {
const dropdownItem = strToEl(` const dropdownItem = strToEl(`
<div class="${ classNames.item } ${ classNames.itemOption } ${ option.selected ? classNames.selectedState + ' ' + classNames.itemDisabled : classNames.itemSelectable } ${ option.highlighted ? classNames.highlightedState : '' }" data-choice-option data-choice-id="${ option.id }" data-choice-value="${ option.value }"> <div class="${ classNames.item } ${ classNames.itemOption } ${ option.selected ? classNames.selectedState + ' ' + classNames.itemDisabled : classNames.itemSelectable }" data-choice-option data-choice-id="${ option.id }" data-choice-value="${ option.value }">
${ option.label } ${ option.label }
</div> </div>
`); `);
@ -960,7 +976,6 @@ export class Choices {
optionListFragment.appendChild(dropdownItem); optionListFragment.appendChild(dropdownItem);
} }
this.dropdown.appendChild(optionListFragment); this.dropdown.appendChild(optionListFragment);
} }

View file

@ -6,31 +6,24 @@ const options = (state = [], action) => {
groupId: action.groupId, groupId: action.groupId,
value: action.value, value: action.value,
label: action.label, label: action.label,
highlighted: action.highlighted,
disabled: action.disabled, disabled: action.disabled,
selected: false, selected: false,
active: true, active: true,
}]; }];
case 'HIGHLIGHT_OPTION': case 'ADD_ITEM':
return state.map((option) => { // When an item is added and it has an associated option,
if(option.id === parseInt(action.id)) { // we want to disable it so it can't be chosen again
option.highlighted = true; if(action.optionId > -1) {
} else { return state.map((option) => {
option.highlighted = false; if(option.id === parseInt(action.optionId)) {
} option.selected = true;
}
return option; return option;
}); });
} else {
case 'SELECT_OPTION': return state;
return state.map((option) => { }
if(option.id === parseInt(action.id)) {
option.selected = action.selected;
}
return option;
});
case 'REMOVE_ITEM': case 'REMOVE_ITEM':
// When an item is removed and it has an associated option, // When an item is removed and it has an associated option,
@ -38,7 +31,7 @@ const options = (state = [], action) => {
if(action.optionId > -1) { if(action.optionId > -1) {
return state.map((option) => { return state.map((option) => {
if(option.id === parseInt(action.optionId)) { if(option.id === parseInt(action.optionId)) {
option.selected = action.selected; option.selected = false;
} }
return option; return option;
}); });
@ -56,15 +49,6 @@ const options = (state = [], action) => {
return result.id === index; return result.id === index;
}); });
// Highlight option if it is active and is the first
// active option in state
if(option.active && firstActive === false) {
option.highlighted = true;
firstActive = true;
} else {
option.highlighted = false;
}
return option; return option;
}); });