Last active
November 25, 2021 02:36
-
-
Save cagataycali/78b4b4972bb4d03096d82aaeb4a1591a to your computer and use it in GitHub Desktop.
[HTML + CSS + JS] Simple auto complete
This file contains 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
<!DOCTYPE html> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8" /> | |
<meta http-equiv="X-UA-Compatible" content="IE=edge" /> | |
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> | |
<title>Auto Complete</title> | |
<style> | |
#container { | |
width: 200px; | |
} | |
.input { | |
width: 200px; | |
} | |
.list { | |
padding-left: 0px; | |
margin-top: 0.2rem; | |
} | |
.item { | |
list-style: none; | |
} | |
.item:hover { | |
border-bottom: 1px dotted #333; | |
} | |
.item.active { | |
border-bottom: 1px dotted #333; | |
} | |
.empty { | |
display: none; | |
} | |
</style> | |
</head> | |
<body> | |
<div id="container"> | |
<input | |
type="text" | |
class="input" | |
value="" | |
placeholder="Search fruit" | |
aria-label="Search fruit" | |
/> | |
<ul class="list"></ul> | |
</div> | |
<script> | |
const fruits = [ | |
"apple", | |
"apricot", | |
"avocado", | |
"banana", | |
"bell pepper", | |
"bilberry", | |
"blackberry", | |
"blackcurrant", | |
"blood orange", | |
"blueberry", | |
"boysenberry", | |
"breadfruit", | |
"canary melon", | |
"cantaloupe", | |
"cherimoya", | |
"cherry", | |
"chili pepper", | |
"clementine", | |
"cloudberry", | |
"coconut", | |
"cranberry", | |
"cucumber", | |
"currant", | |
"damson", | |
"date", | |
"dragonfruit", | |
"durian", | |
"eggplant", | |
"elderberry", | |
"feijoa", | |
"fig", | |
"goji berry", | |
"gooseberry", | |
"grape", | |
"grapefruit", | |
"guava", | |
"honeydew", | |
"huckleberry", | |
"jackfruit", | |
"jambul", | |
"jujube", | |
"kiwi fruit", | |
"kumquat", | |
"lemon", | |
"lime", | |
"loquat", | |
"lychee", | |
"mandarine", | |
"mango", | |
"mulberry", | |
"nectarine", | |
"nut", | |
"olive", | |
"orange", | |
"papaya", | |
"passionfruit", | |
"peach", | |
"pear", | |
"persimmon", | |
"physalis", | |
"pineapple", | |
"plum", | |
"pomegranate", | |
"pomelo", | |
"purple mangosteen", | |
"quince", | |
"raisin", | |
"rambutan", | |
"raspberry", | |
"redcurrant", | |
"rock melon", | |
"salal berry", | |
"satsuma", | |
"star fruit", | |
"strawberry", | |
"tamarillo", | |
"tangerine", | |
"tomato", | |
"ugli fruit", | |
"watermelon", | |
]; | |
function debounce(func, wait) { | |
let timer = null; | |
return function () { | |
const later = () => { | |
timer = null; | |
func.apply(this, arguments); | |
}; | |
if (timer) { | |
clearTimeout(timer); | |
} | |
timer = setTimeout(later, wait); | |
}; | |
} | |
// Implement Trie (Prefix tree) | |
class Node { | |
constructor(char) { | |
this.char = char; | |
this.isEndWord = false; | |
this.children = new Map(); // Hashmap, | |
} | |
} | |
class Trie { | |
constructor() { | |
this.root = new Node(""); | |
} | |
insert = (...words) => { | |
for (const word of words) { | |
let node = this.root; | |
for (const char of word) { | |
if (!node.children.has(char)) | |
node.children.set(char, new Node(char)); | |
node = node.children.get(char); | |
} | |
node.isEndWord = true; | |
} | |
}; | |
autocomplete = (word) => { | |
const suggestions = []; | |
let node = this.root; | |
for (const char of word) { | |
if (!node.children.has(char)) return suggestions; | |
node = node.children.get(char); | |
} | |
this.helper(node, suggestions, word.substring(0, word.length - 1)); | |
return suggestions; | |
}; | |
helper = (node, suggestions, prefix) => { | |
if (node.isEndWord) suggestions.push(prefix + node.char); | |
for (const key of node.children.keys()) { | |
const childNode = node.children.get(key); | |
this.helper(childNode, suggestions, prefix + node.char); | |
} | |
}; | |
} | |
class Autocomplete { | |
constructor(input, list, search, wait = 2000) { | |
this.keys = { | |
ArrowUp: "ArrowUp", | |
ArrowDown: "ArrowDown", | |
Tab: "Tab", | |
Enter: "Enter", | |
Escape: "Escape", | |
}; | |
this.document = document; | |
this.input = input; | |
this.list = list; | |
this.search = search; | |
this.wait = wait; | |
this.selectedItemIndex = -1; // We'll update this after, for keyboard navigation. | |
this.input.addEventListener("keyup", this.handleInput); | |
} | |
handleInput = (e) => { | |
let previousItem; | |
let nextItem; | |
switch (e.key) { | |
case this.keys.ArrowDown: | |
previousItem = list.children[this.selectedItemIndex]; | |
if (previousItem) { | |
previousItem.classList.remove("active"); | |
} | |
this.selectedItemIndex++; | |
if (this.selectedItemIndex === list.children.length) { | |
this.selectedItemIndex = 0; | |
} | |
nextItem = list.children[this.selectedItemIndex]; | |
if (nextItem) { | |
nextItem.classList.add("active"); | |
} | |
break; | |
case this.keys.ArrowUp: | |
previousItem = list.children[this.selectedItemIndex]; | |
if (previousItem) { | |
previousItem.classList.remove("active"); | |
} | |
this.selectedItemIndex--; | |
if (this.selectedItemIndex === -1) { | |
input.focus(); | |
} | |
nextItem = list.children[this.selectedItemIndex]; | |
if (nextItem) { | |
nextItem.classList.add("active"); | |
} | |
break; | |
case this.keys.Tab: | |
console.log("tab"); | |
break; | |
case this.keys.Enter: | |
if (list.children[this.selectedItemIndex]) { | |
input.value = list.children[this.selectedItemIndex].innerText; | |
this.closeList(); | |
} | |
break; | |
case this.keys.Escape: | |
this.closeList(); | |
break; | |
default: | |
this.doSearch(e); | |
} | |
}; | |
doSearch = debounce((e) => { | |
if (e.target.value.length === 0) { | |
this.closeList(); | |
return; | |
} | |
const newItems = this.search(e.target.value); | |
this.renderItems(newItems); | |
}, this.wait); | |
closeList = () => { | |
this.list.textContent = ""; | |
}; | |
renderItems = (items) => { | |
const elementsToRender = []; | |
for (const item of items) { | |
const element = document.createElement("li"); | |
element.innerText = item; | |
element.classList.add("item"); | |
elementsToRender.push(element); | |
} | |
list.replaceChildren(...elementsToRender); | |
}; | |
} | |
const [input, list] = [ | |
document.querySelector(".input"), | |
document.querySelector(".list"), | |
]; | |
const prefixTree = new Trie(); | |
prefixTree.insert(...fruits); | |
const search = (text) => { | |
return prefixTree.autocomplete(text); | |
}; | |
const autocomplete = new Autocomplete(input, list, search); | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment