@ministryofjustice/hmpps-digital-prison-reporting-frontend
Version:
The Digital Prison Reporting Frontend contains templates and code to help display data effectively in UI applications.
149 lines (127 loc) • 4.81 kB
JavaScript
/* eslint-disable class-methods-use-this */
import { DprClientClass } from '../../../DprClientClass.mjs'
export default class Autocomplete extends DprClientClass {
static getModuleName() {
return 'autocomplete-text-input'
}
constructor(element) {
super(element)
const listId = this.getTextInput().getAttribute('aria-owns')
this.listItemsSelector = `#${listId} li`
this.listParentSelector = `#${listId} ul`
}
initialise() {
const textInput = this.getTextInput()
textInput.addEventListener('keyup', (event) => {
this.onTextInput(event, textInput)
})
textInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') {
e.stopPropagation()
e.preventDefault()
}
})
this.getElement()
.querySelectorAll('.autocomplete-text-input-list-button')
.forEach((button) => {
button.addEventListener('click', (event) => {
this.onOptionClick(event, textInput, this.getElement())
})
})
}
getTextInput() {
return this.getElement().querySelector('.autocomplete-text-input-box')
}
onTextInput(event, textInput) {
const minLength = Number(textInput.dataset.minimumLength)
const { resourceEndpoint } = textInput.dataset
const searchValue = event.target.value.toLowerCase()
if (resourceEndpoint) {
if (searchValue.length >= minLength) {
this.addItem(this.clearListAndRecreateTemplate(), '<i>Searching...</i>')
this.populateOptionsDynamically(resourceEndpoint, searchValue, textInput, () =>
this.clearListAndRecreateTemplate(),
)
} else {
this.clearListAndRecreateTemplate()
}
} else {
this.getElement()
.querySelectorAll(this.listItemsSelector)
.forEach((item) => {
if (
searchValue.length >= minLength &&
this.isMatchingStaticOptionNameOrDisplayPrefix(this.getInputListButton(item), searchValue, item)
) {
item.classList.remove('autocomplete-text-input-item-hide')
} else {
item.classList.add('autocomplete-text-input-item-hide')
}
})
}
if (searchValue.length === 0) {
const changeEvent = new Event('change')
textInput.dispatchEvent(changeEvent)
}
}
getInputListButton(item) {
return item.querySelector('.autocomplete-text-input-list-button')
}
isMatchingStaticOptionNameOrDisplayPrefix(inputListButton, searchValue, item) {
return (
this.isStaticOptionsNamePrefix(inputListButton.dataset.staticOptionNameValue, searchValue) ||
item.innerText.trim().toLowerCase().startsWith(searchValue)
)
}
isStaticOptionsNamePrefix(staticOptionNameValue, searchValue) {
return staticOptionNameValue && staticOptionNameValue.trim().toLowerCase().startsWith(searchValue)
}
async populateOptionsDynamically(resourceEndpoint, searchValue, textInput, templateProvider) {
try {
const response = await fetch(resourceEndpoint.replace('{prefix}', encodeURI(searchValue)))
const results = await response.json()
if (searchValue === textInput.value.toLowerCase()) {
const template = templateProvider()
results.forEach((r) => {
this.addItem(template, r, (event) => {
this.onOptionClick(event, textInput, this.getElement())
})
})
}
} catch (error) {
this.addItem(templateProvider(), `Failed to retrieve results: ${error}`)
}
}
onOptionClick(event, textInput, topLevelElement) {
event.preventDefault()
// eslint-disable-next-line no-param-reassign
textInput.value = event.target.innerText.trim()
textInput.staticOptionNameValue = event.target.dataset.staticOptionNameValue
textInput.focus()
const changeEvent = new Event('change')
textInput.dispatchEvent(changeEvent)
topLevelElement.querySelectorAll('li').forEach((item) => {
item.classList.add('autocomplete-text-input-item-hide')
})
}
addItem(template, content, clickEvent) {
const item = template.cloneNode(true)
item.querySelector('button').innerHTML = content
item.classList.remove('autocomplete-text-input-item-hide')
this.getElement().querySelector(this.listParentSelector).appendChild(item)
if (clickEvent) {
item.addEventListener('click', (event) => {
clickEvent(event)
})
}
}
clearListAndRecreateTemplate() {
const template = this.getElement().querySelector(this.listItemsSelector).cloneNode(true)
template.classList.add('autocomplete-text-input-item-hide')
this.getElement()
.querySelectorAll(this.listItemsSelector)
.forEach((e) => e.remove())
this.getElement().querySelector(this.listParentSelector).append(template)
return template
}
}