: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
: str_replace(): Passing null to parameter #2 ($replace) of type array|string is deprecated in
* https://github.com/kraaden/autocomplete
* Copyright (c) 2016 Denys Krasnoshchok
export const enum EventTrigger {
export interface AutocompleteItem {
export interface AutocompleteSettings<T extends AutocompleteItem> {
render?: (item: T, currentValue: string) => HTMLDivElement | undefined;
renderGroup?: (name: string, currentValue: string) => HTMLDivElement | undefined;
onSelect: (item: T, input: HTMLInputElement) => void;
* Show autocomplete on focus event. Focus event will ignore the `minLength` property and will always call `fetch`.
fetch: (text: string, update: (items: T[] | false) => void, trigger: EventTrigger) => void;
* Callback for additional autocomplete customization
* @param {HTMLInputElement} input - input box associated with autocomplete
* @param {ClientRect | DOMRect} inputRect - size of the input box and its position relative to the viewport
* @param {HTMLDivElement} container - container with suggestions
* @param {number} maxHeight - max height that can be used by autocomplete
customize?: (input: HTMLInputElement, inputRect: ClientRect | DOMRect, container: HTMLDivElement, maxHeight: number) => void;
* Prevents automatic form submit when ENTER is pressed
export interface AutocompleteResult {
export default function autocomplete<T extends AutocompleteItem>(settings: AutocompleteSettings<T>): AutocompleteResult {
// just an alias to minimize JS file size
const container: HTMLDivElement = doc.createElement("div");
const containerStyle = container.style;
const userAgent = navigator.userAgent;
const mobileFirefox = userAgent.indexOf("Firefox") !== -1 && userAgent.indexOf("Mobile") !== -1;
const debounceWaitMs = settings.debounceWaitMs || 0;
const preventSubmit = settings.preventSubmit || false;
// 'keyup' event will not be fired on Mobile Firefox, so we have to use 'input' event instead
const keyUpEventName = mobileFirefox ? "input" : "keyup";
const showOnFocus = settings.showOnFocus;
let selected: T | undefined;
let debounceTimer : number | undefined;
if (settings.minLength !== undefined) {
minLen = settings.minLength;
throw new Error("input undefined");
const input: HTMLInputElement = settings.input;
container.className = "autocomplete " + (settings.className || "");
containerStyle.position = "fixed";
* Detach the container from DOM
function detach(): void {
const parent = container.parentNode;
parent.removeChild(container);
* Clear debouncing timer if assigned
function clearDebounceTimer(): void {
window.clearTimeout(debounceTimer);
* Attach the container to DOM
function attach(): void {
if (!container.parentNode) {
doc.body.appendChild(container);
* Check if container for autocomplete is displayed
function containerDisplayed(): boolean {
return !!container.parentNode;
* Clear autocomplete state and hide container
* Update autocomplete position
function updatePosition(): void {
if (!containerDisplayed()) {
containerStyle.height = "auto";
containerStyle.width = input.offsetWidth + "px";
const inputRect = input.getBoundingClientRect();
const top = inputRect.top + input.offsetHeight;
let maxHeight = window.innerHeight - top;
containerStyle.top = top + "px";
containerStyle.bottom = "";
containerStyle.left = inputRect.left + "px";
containerStyle.maxHeight = maxHeight + "px";
if (settings.customize) {
settings.customize(input, inputRect, container, maxHeight);
* Redraw the autocomplete div element with suggestions
function update(): void {
// delete all children from autocomplete DOM container
while (container.firstChild) {
container.removeChild(container.firstChild);
// function for rendering autocomplete suggestions
let render = function(item: T, currentValue: string): HTMLDivElement | undefined {
const itemElement = doc.createElement("div");
itemElement.textContent = item.label || "";
render = settings.render;
// function to render autocomplete groups
let renderGroup = function(groupName: string, currentValue: string): HTMLDivElement | undefined {
const groupDiv = doc.createElement("div");
groupDiv.textContent = groupName;
if (settings.renderGroup) {
renderGroup = settings.renderGroup;
const fragment = doc.createDocumentFragment();
items.forEach(function(item: T): void {
if (item.group && item.group !== prevGroup) {
const groupDiv = renderGroup(item.group, inputValue);
groupDiv.className += " group";
fragment.appendChild(groupDiv);
const div = render(item, inputValue);
div.addEventListener("click", function(ev: MouseEvent): void {
settings.onSelect(item, input);
div.className += " selected";
fragment.appendChild(div);
container.appendChild(fragment);
const empty = doc.createElement("div");
empty.className = "empty";
empty.textContent = settings.emptyMsg;
container.appendChild(empty);
function updateIfDisplayed(): void {
if (containerDisplayed()) {
function resizeEventHandler(): void {
function scrollEventHandler(e: Event): void {
if (e.target !== container) {
function keyupEventHandler(ev: KeyboardEvent): void {
const keyCode = ev.which || ev.keyCode || 0;
const ignore = [Keys.Up, Keys.Enter, Keys.Esc, Keys.Right, Keys.Left, Keys.Shift, Keys.Ctrl, Keys.Alt, Keys.CapsLock, Keys.WindowsKey, Keys.Tab];
for (const key of ignore) {
// the down key is used to open autocomplete
if (keyCode === Keys.Down && containerDisplayed()) {
startFetch(EventTrigger.Keyboard);
* Automatically move scroll bar if selected item is not visible
function updateScroll(): void {
const elements = container.getElementsByClassName("selected");
if (elements.length > 0) {
let element = elements[0] as HTMLDivElement;
const previous = element.previousElementSibling as HTMLDivElement;
if (previous && previous.className.indexOf("group") !== -1 && !previous.previousElementSibling) {
if (element.offsetTop < container.scrollTop) {
container.scrollTop = element.offsetTop;
const selectBottom = element.offsetTop + element.offsetHeight;
const containerBottom = container.scrollTop + container.offsetHeight;
if (selectBottom > containerBottom) {
container.scrollTop += selectBottom - containerBottom;
* Select the previous item in suggestions
function selectPrev(): void {
if (selected === items[0]) {
selected = items[items.length - 1];
for (let i = items.length - 1; i > 0; i--) {
if (selected === items[i] || i === 1) {
* Select the next item in suggestions
function selectNext(): void {
if (!selected || selected === items[items.length - 1]) {
for (let i = 0; i < (items.length - 1); i++) {
if (selected === items[i]) {
function keydownEventHandler(ev: KeyboardEvent): void {
const keyCode = ev.which || ev.keyCode || 0;
if (keyCode === Keys.Up || keyCode === Keys.Down || keyCode === Keys.Esc) {
const containerIsDisplayed = containerDisplayed();
if (keyCode === Keys.Esc) {
if (!containerDisplayed || items.length < 1) {
if (containerIsDisplayed) {
if (keyCode === Keys.Enter) {
settings.onSelect(selected, input);
function focusEventHandler(): void {
startFetch(EventTrigger.Focus);
function startFetch(trigger: EventTrigger) {
// if multiple keys were pressed, before we get update from server,
// this may cause redrawing our autocomplete multiple times after the last key press.
// to avoid this, the number of times keyboard was pressed will be
// saved and checked before redraw our autocomplete box.
const savedKeypressCounter = ++keypressCounter;
if (val.length >= minLen || trigger === EventTrigger.Focus) {
debounceTimer = window.setTimeout(function(): void {
settings.fetch(val, function(elements: T[] | false): void {
if (keypressCounter === savedKeypressCounter && elements) {
selected = items.length > 0 ? items[0] : undefined;
}, EventTrigger.Keyboard);
}, trigger === EventTrigger.Keyboard ? debounceWaitMs : 0);
function blurEventHandler(): void {
// we need to delay clear, because when we click on an item, blur will be called before click and remove items from DOM
if (doc.activeElement !== input) {
* Fixes #26: on long clicks focus will be lost and onSelect method will not be called
container.addEventListener("mousedown", function(evt: Event) {
* This function will remove DOM elements and clear event handlers
function destroy(): void {
input.removeEventListener("focus", focusEventHandler);
input.removeEventListener("keydown", keydownEventHandler);
input.removeEventListener(keyUpEventName, keyupEventHandler as EventListenerOrEventListenerObject);
input.removeEventListener("blur", blurEventHandler);
window.removeEventListener("resize", resizeEventHandler);
doc.removeEventListener("scroll", scrollEventHandler, true);
// prevent the update call if there are pending AJAX requests
input.addEventListener("keydown", keydownEventHandler);
input.addEventListener(keyUpEventName, keyupEventHandler as EventListenerOrEventListenerObject);
input.addEventListener("blur", blurEventHandler);
input.addEventListener("focus", focusEventHandler);
window.addEventListener("resize", resizeEventHandler);
doc.addEventListener("scroll", scrollEventHandler, true);