First commit.
This commit is contained in:
137
static/autocomp.js
Normal file
137
static/autocomp.js
Normal file
@@ -0,0 +1,137 @@
|
||||
function autocomp(el, options = {}) {
|
||||
const opt = {
|
||||
onQuery: null, onNavigate: null, onSelect: null, onRender: null, debounce: 100, autoSelect: true,...options
|
||||
};
|
||||
|
||||
let box, cur = opt.autoSelect ? 0 : -1, items = [], val, req;
|
||||
|
||||
// Disable browser's default autocomplete behaviour on the input.
|
||||
el.autocomplete = "off";
|
||||
|
||||
// Attach all the events required for the interactions in one go.
|
||||
["input", "keydown", "blur"].forEach(k => el.addEventListener(k, handleEvent));
|
||||
|
||||
function handleEvent(e) {
|
||||
if (e.type === "keydown" && !handleKeydown(e)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.type === "blur") {
|
||||
return destroy();
|
||||
}
|
||||
|
||||
const newVal = e.target.value;
|
||||
if (!newVal) {
|
||||
destroy();
|
||||
val = null;
|
||||
return;
|
||||
}
|
||||
|
||||
if (newVal === val && box) {
|
||||
return;
|
||||
}
|
||||
val = newVal;
|
||||
|
||||
// Clear (debounce) any existing pending requests and queue
|
||||
// the next search request.
|
||||
clearTimeout(req);
|
||||
req = setTimeout(query, opt.debounce);
|
||||
}
|
||||
|
||||
function handleKeydown(e) {
|
||||
if (!box) {
|
||||
return e.keyCode === 38 || e.keyCode === 40
|
||||
}
|
||||
|
||||
switch (e.keyCode) {
|
||||
case 38: return navigate(-1, e); // Up arrow.
|
||||
case 40: return navigate(1, e); // Down arrow
|
||||
case 9: // Tab
|
||||
case 13: // Enter
|
||||
e.preventDefault();
|
||||
select(cur);
|
||||
destroy();
|
||||
return;
|
||||
case 27: // Escape.
|
||||
destroy();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
async function query() {
|
||||
if (!val) {
|
||||
return;
|
||||
}
|
||||
|
||||
items = await opt.onQuery(val);
|
||||
if (!items.length) {
|
||||
return destroy();
|
||||
}
|
||||
|
||||
if (!box) {
|
||||
createBox();
|
||||
}
|
||||
|
||||
renderResults();
|
||||
}
|
||||
|
||||
function createBox() {
|
||||
box = document.createElement("div");
|
||||
Object.assign(box.style, {
|
||||
width: window.getComputedStyle(el).width,
|
||||
position: "absolute",
|
||||
left: `${el.offsetLeft}px`,
|
||||
top: `${el.offsetTop + el.offsetHeight}px`
|
||||
});
|
||||
|
||||
box.classList.add("autocomp");
|
||||
el.parentNode.insertBefore(box, el.nextSibling);
|
||||
}
|
||||
|
||||
function renderResults() {
|
||||
box.innerHTML = "";
|
||||
items.forEach((item, idx) => {
|
||||
const div = document.createElement("div");
|
||||
div.classList.add("autocomp-item");
|
||||
|
||||
// If there's a custom renderer callback, use it, else, simply insert the value/text as-is.
|
||||
opt.onRender ? div.appendChild(opt.onRender(item)) : div.innerText = item;
|
||||
if (idx === cur) {
|
||||
div.classList.add("autocomp-sel");
|
||||
}
|
||||
|
||||
div.addEventListener("mousedown", () => select(idx));
|
||||
box.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
function navigate(direction, e) {
|
||||
e.preventDefault();
|
||||
|
||||
// Remove the previous item's highlight;
|
||||
const prev = box.querySelector(`:nth-child(${cur + 1})`);
|
||||
prev?.classList.remove("autocomp-sel");
|
||||
|
||||
// Increment the cursor and highlight the next item, cycled between [0, n].
|
||||
cur = (cur + direction + items.length) % items.length;
|
||||
box.querySelector(`:nth-child(${cur + 1})`).classList.add("autocomp-sel");
|
||||
}
|
||||
|
||||
function select(idx) {
|
||||
if (!opt.onSelect) {
|
||||
return;
|
||||
}
|
||||
|
||||
val = opt.onSelect(items[idx]);
|
||||
el.value = val || items[idx];
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
items = [];
|
||||
cur = opt.autoSelect ? 0 : -1;
|
||||
if (box) {
|
||||
box.remove();
|
||||
box = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user