Skip to content

Instantly share code, notes, and snippets.

@DotNetNerd
Last active December 14, 2017 12:47
Show Gist options
  • Save DotNetNerd/20428a622d5195e08391a0a546666259 to your computer and use it in GitHub Desktop.
Save DotNetNerd/20428a622d5195e08391a0a546666259 to your computer and use it in GitHub Desktop.
Type(Script)Ahead
interface ITypeaheadArgs
{
minChars?: number,
onSelect?: () => void
}
async function typeahead(
field: HTMLInputElement,
search: (text: string) => Promise<string[]>,
args: ITypeaheadArgs) {
let handleKey = (event: KeyboardEvent, keyCode: number,
eventHandler: (typeahead: HTMLUListElement) => void) => {
if (event.keyCode === keyCode) {
event.preventDefault();
event.stopImmediatePropagation();
if (field.parentElement) {
let typeahead = <HTMLUListElement>field.parentElement.querySelector('.js-typeahead');
if (typeahead) {
eventHandler(typeahead);
}
}
}
};
field.addEventListener('keydown', event => {
if (event.keyCode === 38 || event.keyCode === 40) {
event.preventDefault();
}
});
field.addEventListener('keyup', event => {
if (!field.parentElement) { return; }
handleKey(event, 38, typeahead => {
let selected = typeahead.querySelector('.selected');
if (selected) {
let prev = selected.previousElementSibling;
while (prev !== null && prev.nodeType === Node.TEXT_NODE) {
prev = selected.previousElementSibling;
}
if (prev) {
selected.classList.remove('selected');
prev.classList.add('selected');
}
}
});
handleKey(event, 40, typeahead => {
let selected = typeahead.querySelector('.selected');
if (selected) {
let next = selected.nextElementSibling;
while (next !== null && next.nodeType === Node.TEXT_NODE) {
next = selected.nextElementSibling;
}
if (next) {
selected.classList.remove('selected');
next.classList.add('selected');
}
} else {
let first = typeahead.querySelector('li');
if (first) {
first.classList.add('selected');
}
}
});
handleKey(event, 13, typeahead => {
let selected = typeahead.querySelector('.selected');
if (selected) {
field.value = selected.innerHTML;
typeahead.innerHTML = '';
if (args.onSelect) {
args.onSelect();
}
}
});
});
field.addEventListener('keyup', debounce(async () => {
if (field.parentElement) {
let typeahead =
<HTMLUListElement>field.parentElement.querySelector('.js-typeahead');
if (!typeahead) {
typeahead = document.createElement('ul');
typeahead.classList.add('js-typeahead')
if (field.parentNode) {
field.parentNode.insertBefore(typeahead, field.nextSibling);
}
addChildClickListener(typeahead, 'li', e => {
field.value = e.innerHTML;
typeahead.innerHTML = '';
if (args.onSelect) {
args.onSelect();
}
});
document.body.addEventListener('click', () => typeahead.innerHTML = '');
}
if ((!args.minChars
|| field.value.trim().length >= args.minChars) && field.parentElement) {
let items = await search(field.value);
if (items) {
let liReducer =
(acc: string, val: string): string => acc += `<li>${val}</li>`;
typeahead.innerHTML = items.reduce(liReducer, '');
} else {
typeahead.innerHTML = '';
}
} else {
typeahead.innerHTML = '';
}
}
}, 500));
}
function addChildClickListener(node: HTMLElement,
selector: string, f: (e: HTMLElement) => any) {
node.addEventListener('click', (e: MouseEvent) => {
e.preventDefault();
if(e.srcElement && e.srcElement.matches(selector)) {
f(<HTMLElement>e.srcElement);
}
});
}
function debounce(func: () => void, wait = 100) {
let h: number;
return () => {
clearTimeout(h);
h = setTimeout(() => func(), wait);
};
}
/*
CSS:
.js-typeahead {
z-index: 99;
position: absolute;
background-color: white;
border: solid 1px lightgrey;
padding: 0;
list-style-type: none;
border-top: 0;
}
.js-typeahead li {
padding: 5px;
cursor: pointer;
}
.js-typeahead li.selected {
background-color: lightblue;
}
*/
// Use like:
// let element = document.getElementById('myBox');
// typeahead(element,
// searchText => await someClient.Startswith(searchText),
// {minChars: 5, onSelect: () => alert(`${element.value} selected`);})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment