Skip to content

Instantly share code, notes, and snippets.

@m3g4p0p
Last active June 21, 2016 19:23
Show Gist options
  • Save m3g4p0p/cb71086785ff27c2c1f58037806629da to your computer and use it in GitHub Desktop.
Save m3g4p0p/cb71086785ff27c2c1f58037806629da to your computer and use it in GitHub Desktop.
A JS autocomplete module
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
};
};
@m3g4p0p
Copy link
Author

m3g4p0p commented Jun 21, 2016

Usage:

var autocomplete = new Autocomplete(textInputElement, suggestionArray);

Demo:
http://m3g4p0p.bplaced.net/autocomplete.html

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment