Last active
June 21, 2016 19:23
-
-
Save m3g4p0p/cb71086785ff27c2c1f58037806629da to your computer and use it in GitHub Desktop.
A JS autocomplete module
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
var Autocomplete = function(element, suggestions) { | |
'use strict'; | |
/** | |
* Variables | |
*/ | |
var container = document.createElement('div'); | |
var list = document.createElement('ul'); | |
var listItems = []; | |
var current = []; | |
var input = ''; | |
var active = null; | |
var hovering = false; | |
/** | |
* Constants | |
*/ | |
var SHOW_CLASS = 'autocomplete-show'; | |
var ACTIVE_CLASS = 'autocomplete-active'; | |
var FIRST_CLASS = 'autocomplete-first'; | |
var LAST_CLASS = 'autocomplete-last'; | |
/** | |
* Method to create and return a list element based | |
* on a suggestion string | |
*/ | |
var _getListElement = function(value) { | |
var listElement = document.createElement('li'); | |
var listText = document.createTextNode(value); | |
listElement.appendChild(listText); | |
list.appendChild(listElement); | |
return listElement; | |
}; | |
/** | |
* Method to apply the active class | |
*/ | |
var _applyActive = function(item) { | |
if (item === active) { | |
item.classList.add(ACTIVE_CLASS); | |
} else { | |
item.classList.remove(ACTIVE_CLASS); | |
} | |
}; | |
/** | |
* Method to reset the list of current suggestions | |
*/ | |
var _resetCurrent = function() { | |
current.forEach(function(item) { | |
item.classList.remove(SHOW_CLASS); | |
}); | |
current = []; | |
active = null; | |
}; | |
/** | |
* Method to display suggestions based on the input value | |
*/ | |
var _checkSuggestions = function() { | |
// Cache the current input element's value to be able | |
// to cycle through the suggestions | |
input = element.value; | |
// Generate a list of currently available items | |
current = listItems.filter(function(item) { | |
var substring = item | |
.textContent | |
.toLowerCase() | |
.substring(0, input.length); | |
var isCurrent = substring === input.toLowerCase(); | |
// Show items which apply to the current input | |
if (isCurrent) { | |
item.classList.add(SHOW_CLASS); | |
} else { | |
item.classList.remove(SHOW_CLASS); | |
} | |
item.classList.remove(FIRST_CLASS, LAST_CLASS); | |
_applyActive(item); | |
return isCurrent; | |
}); | |
// Add classes to the first and last element for styling | |
// if desired | |
if (current.length) { | |
current[0].classList.add(FIRST_CLASS) | |
current[current.length - 1].classList.add(LAST_CLASS); | |
} | |
}; | |
/** | |
* Method to actually accept a given suggestion | |
*/ | |
var _accept = function(event) { | |
// If the method was called by an event listener, | |
// set the active item accordingly | |
if (event) { | |
active = event.target; | |
element.focus(); | |
} | |
element.value = input = active.textContent; | |
_resetCurrent(); | |
}; | |
/** | |
* Method to handle various key events | |
*/ | |
var _keyupHandler = function(event) { | |
var which = event.which; | |
var ignoreKeys = [9, 13, 16, 17, 18, 20, 37, 39]; | |
// Apply active class and set the input element's | |
// value according to the selected item | |
var selectionHandler = function(index) { | |
if (Number.isNaN(index)) { | |
_checkSuggestions(); | |
} | |
if (current.length) { | |
active = current[index || 0]; | |
element.value = active.textContent; | |
current.forEach(_applyActive); | |
} | |
}; | |
// Tab key | |
if (which === 9 && active) { | |
element.value = input = active.textContent; | |
_checkSuggestions(); | |
} | |
// Enter key | |
else if (which === 13 && active) { | |
_accept(); | |
} | |
// Esc key | |
else if (which === 27) { | |
element.value = input; | |
_resetCurrent(); | |
} | |
// Uparrow key | |
else if (which === 38) { | |
selectionHandler((current.indexOf(active) - 1 + | |
current.length) % current.length); | |
} | |
// Downarrow key | |
else if (which === 40) { | |
selectionHandler((current.indexOf(active) + 1) | |
% current.length); | |
} | |
// Check for the input element's value | |
else if (ignoreKeys.indexOf(which) === -1 && element.value) { | |
_checkSuggestions(); | |
} | |
// Remove list | |
else { | |
_resetCurrent(); | |
} | |
}; | |
/** | |
* Method to prevent defaults when pressing certain keys | |
*/ | |
var _keydownHandler = function(event) { | |
var which = event.which; | |
if ((which === 9 || which === 13) && active || | |
which === 38) { | |
event.preventDefault(); | |
} | |
}; | |
/** | |
* Method to handle mouse hover suggestions | |
*/ | |
var _hoverHandler = function(event) { | |
var target = event.target; | |
// If the hovered element is a suggestion item, | |
// set the input element's value accordingly | |
if (current.indexOf(target) > -1) { | |
element.value = target.textContent; | |
hovering = true; | |
// Don't highlight the active item as | |
// there's a hover effect anyway | |
if (active) { | |
active.classList.remove(ACTIVE_CLASS); | |
} | |
} | |
// If the list has been hovered and is left now, | |
// reset to the previously active item | |
else if (hovering) { | |
hovering = false; | |
element.value = active ? active.textContent : input; | |
if (active) { | |
active.classList.add(ACTIVE_CLASS); | |
} | |
} | |
}; | |
/** | |
* Method to change the suggestion list retrospectively | |
*/ | |
var _setSuggestions = function(suggestions) { | |
list.innerHTML = ''; | |
listItems = suggestions.map(_getListElement); | |
_resetCurrent(); | |
}; | |
/** | |
* Init method | |
*/ | |
var _init = function() { | |
var parent = element.parentNode; | |
var sibling = element.nextSibling; | |
// Wrap element in container | |
container.classList.add('autocomplete-container'); | |
container.appendChild(element); | |
container.appendChild(list); | |
if (sibling) { | |
parent.insertBefore(container, sibling); | |
} else { | |
parent.appendChild(container); | |
} | |
// Create suggestion list | |
listItems = suggestions.map(_getListElement); | |
// Add event listeners | |
element.addEventListener('keyup', _keyupHandler); | |
element.addEventListener('keydown', _keydownHandler); | |
element.addEventListener('blur', _resetCurrent); | |
list.addEventListener('mousedown', _accept); | |
document.body.addEventListener('mouseover', _hoverHandler); | |
}; | |
_init(); | |
/** | |
* Public API | |
*/ | |
return { | |
setSuggestions: _setSuggestions | |
}; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Usage:
Demo:
http://m3g4p0p.bplaced.net/autocomplete.html