Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fixed autocomplete control behaviour #521

Merged
merged 1 commit into from
Jun 7, 2017
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
141 changes: 113 additions & 28 deletions src/js/control/autocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,40 +14,39 @@ export default class controlAutocomplete extends control {
let {values, type, ...data} = this.config;
const keyboardNav = (e) => {
const list = e.target.nextSibling.nextSibling;
let activeOption = list.getElementsByClassName('active-option')[0];
const hiddenField = e.target.nextSibling;
let activeOption = this.getActiveOption(list);
const keyCodeMapVals = [
// up
[38, () => {
if (activeOption) {
if (activeOption.previousSibling) {
activeOption.classList.remove('active-option');
activeOption = activeOption.previousSibling;
activeOption.classList.add('active-option');
}
const previous = this.getPreviousOption(activeOption);
if (previous) {
this.selectOption(list, previous);
}
}],
// down
[40, () => {
if (activeOption) {
if (activeOption.nextSibling) {
activeOption.classList.remove('active-option');
activeOption = activeOption.nextSibling;
activeOption.classList.add('active-option');
}
} else {
activeOption = list.firstChild;
activeOption.classList.add('active-option');
const next = this.getNextOption(activeOption);
if (next) {
this.selectOption(list, next);
}
}],
// enter
[13, () => {
if (activeOption) {
e.target.value = activeOption.innerHTML;
e.target.value = activeOption.innerHTML;
hiddenField.value = activeOption.getAttribute('value');
if (list.style.display === 'none') {
list.style.display = 'block';
this.showList(list, activeOption);
} else {
list.style.display = 'none';
this.hideList(list);
}
}
e.preventDefault();
}],
// escape
[27, () => {
this.hideList(list);
}]
];
let keyCodeMap = new Map(keyCodeMapVals);
Expand All @@ -61,10 +60,13 @@ export default class controlAutocomplete extends control {
};
const fauxEvents = {
focus: evt => {
let list = evt.target.nextSibling.nextSibling;
const list = evt.target.nextSibling.nextSibling;
const filteredOptions = filter(list.querySelectorAll('li'), evt.target.value);
evt.target.addEventListener('keydown', keyboardNav);
list.style.display = 'block';
list.style.width = list.parentElement.offsetWidth + 'px';
if ( evt.target.value.length > 0 ) {
const selectedOption = filteredOptions.length > 0 ? filteredOptions[filteredOptions.length-1] : null;
this.showList(list, selectedOption);
}
},
blur: evt => {
evt.target.removeEventListener('keydown', keyboardNav);
Expand All @@ -74,17 +76,24 @@ export default class controlAutocomplete extends control {
},
input: (evt) => {
const list = evt.target.nextSibling.nextSibling;
filter(list.querySelectorAll('li'), evt.target.value);
if (!evt.target.value) {
list.style.display = 'none';
const hiddenField = evt.target.nextSibling;
hiddenField.value = evt.target.value;
const filteredOptions = filter(list.querySelectorAll('li'), evt.target.value);
if (filteredOptions.length == 0) {
this.hideList(list);
} else {
list.style.display = 'block';
let activeOption = this.getActiveOption(list);
if (!activeOption) {
activeOption = filteredOptions[filteredOptions.length - 1];
}
this.showList(list, activeOption);
}
}
};
let fauxAttrs = Object.assign({}, data,
{
id: `${data.id}-input`,
autocomplete: 'off',
events: fauxEvents
});
let hiddenAttrs = Object.assign({}, data, {type: 'hidden'});
Expand All @@ -102,8 +111,8 @@ export default class controlAutocomplete extends control {
const list = evt.target.parentElement;
const field = list.previousSibling.previousSibling;
field.value = optionData.label;
field.previousSibling.value = optionData.value;
list.style.display = 'none';
field.nextSibling.value = optionData.value;
this.hideList(list);
}
},
value: optionData.value
Expand All @@ -116,6 +125,82 @@ export default class controlAutocomplete extends control {
return field;
}


/**
* Hides autocomplete list and deselects all the options
* @param {Object} list - list of autocomplete options
*/
hideList(list) {
this.selectOption(list, null);
list.style.display = 'none';
}

/**
* Shows autocomplete list. Automatically selects 'selectedOption'
* @param {Object} list - list of autocomplete options
* @param {Object} selectedOption - option to be selected
*/
showList(list, selectedOption) {
this.selectOption(list, selectedOption);
list.style.display = 'block';
list.style.width = list.parentElement.offsetWidth + 'px';
}

/**
* Returns first option from autocomplete list with 'active-option' class
* @param {Object} list - list of autocomplete options
* @return {Object} first list option with 'active-option' class
*/
getActiveOption(list) {
const activeOption = list.getElementsByClassName('active-option')[0];
if (activeOption && activeOption.style.display !== 'none') {
return activeOption;
}
return null;
}

/**
* Previous next option to the current option
* @param {Object} current - currently selected option
* @return {Object} previous option to the current option or null if previous doesn't exist
*/
getPreviousOption(current) {
let previous = current;
do {
previous = previous ? previous.previousSibling : null;
} while (previous != null && previous.style.display === 'none');
return previous;
}

/**
* Returns next option to the current option
* @param {Object} current - currently selected option
* @return {Object} next option to the current option or null if next doesn't exist
*/
getNextOption(current) {
let next = current;
do {
next = next ? next.nextSibling: null;
} while (next != null && next.style.display === 'none');
return next;
}

/**
* Selects option in autocomplete list. Removes class 'active-option' from all options
* and then adds that class to 'selected' option. If 'selected' is null then no option is selected
* @param {Object} list - list of autocomplete options
* @param {Object} selectedOption - option - 'li' element - to be selected in autocomplete list
*/
selectOption(list, selectedOption) {
const options = list.querySelectorAll('li');
options.forEach((option)=>{
option.classList.remove('active-option');
});
if (selectedOption) {
selectedOption.classList.add('active-option');
}
}

/**
* When the element is rendered into the DOM, execute the following code to initialise it
* @param {Object} evt - event
Expand Down