|
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`);}) |