Files
dc/client/src/app/shared/autocomplete/autocomplete.component.ts
T

310 lines
7.2 KiB
TypeScript

import {
AfterViewInit,
ChangeDetectorRef,
Component,
EventEmitter,
HostListener,
Input,
OnInit,
Output,
ViewChild,
ViewEncapsulation
} from '@angular/core'
export type OnLoadingMoreEvent = {
loadMoreFinished: (moreExists: boolean) => void
}
@Component({
selector: 'app-autocomplete',
templateUrl: './autocomplete.component.html',
styleUrls: ['./autocomplete.component.scss'],
encapsulation: ViewEncapsulation.None,
standalone: false
})
export class AutocompleteComponent implements OnInit, AfterViewInit {
@ViewChild('input') inputElement: any
@Input() inputId: string = ''
@Input() placeholder: string = ''
@Input() value: string = ''
@Input() disabled: boolean = false
@Input() type: string = 'text'
@Input() autocomplete: string = 'off'
@Input() tabindex: string = ''
@Input() emitOnlySelected: boolean = false
@Input() enableLoadMore: boolean = false
@Output() onInputKeydown: EventEmitter<any> = new EventEmitter()
@Output() onInputChange: EventEmitter<any> = new EventEmitter()
@Output() valueChange: EventEmitter<any> = new EventEmitter()
@Output() onLoadingMore: EventEmitter<OnLoadingMoreEvent> = new EventEmitter()
datalistOpen: boolean = false
afterOpenLockDelay: number = 150
afterOpenLockDelayTimeout: any
afterOpenLock: boolean = false
inputFocused: boolean = false
inputClientRect: {
width?: number
bottom?: number
} = {}
innerHeight: number | undefined
datalistCloseTimeout: any
preventDatalistClose: boolean = false
loadingMore: boolean = false
constructor(private cdr: ChangeDetectorRef) {}
ngOnInit(): void {}
ngAfterViewInit(): void {
setTimeout(() => {
this.calculateInputPosition()
}, 500)
}
onInputFired(event?: any, emitEvent?: boolean, initiator?: 'click') {
this.openDatalist(initiator !== 'click')
this.unselectAllOptions()
this.filterOptionsList()
if (!this.emitOnlySelected || emitEvent) {
this.onInputChange.emit({ target: { value: this.value } })
this.valueChange.emit(this.value)
}
}
onInputClick(event: any) {
this.openDatalist()
}
openDatalist(lockDelay: boolean = true) {
this.datalistOpen = true
if (lockDelay) {
this.afterOpenLock = true
this.afterOpenLockDelayTimeout = setTimeout(() => {
this.afterOpenLock = false
}, this.afterOpenLockDelay)
}
}
closeDatalist() {
clearTimeout(this.afterOpenLockDelayTimeout)
this.afterOpenLock = false
this.datalistOpen = false
}
resetDatalistFilter() {
this.filterOptionsList(true)
}
onInputFocusin(event: any) {
this.inputFocused = true
}
onInputFocusout(event: any) {
this.inputFocused = false
this.setToCloseDatalist()
}
onInputMouseenter(event: any) {
// If width not set it means calculation failed on init (input was hidden)
if (!this.inputClientRect.width) this.calculateInputPosition()
}
filterOptionsList(showAll?: boolean) {
const options = document.querySelectorAll(
`#datalist_${this.inputId} option`
)
for (let i = 0; i < options.length; i++) {
const option = options[i]
if (this.value.length === 0) {
option.classList.remove('hidden')
continue
}
if (showAll) {
option.classList.remove('hidden')
continue
}
const optionLabel = (option as any).innerText || ''
const optionValue = (option as any).value || ''
const combined = optionLabel + optionValue
if (combined?.toLowerCase().includes(this.value.toLowerCase())) {
option.classList.remove('hidden')
} else {
option.classList.add('hidden')
}
}
}
setToCloseDatalist() {
this.datalistCloseTimeout = setTimeout(() => {
if (this.preventDatalistClose || this.afterOpenLock) return
this.closeDatalist()
this.unselectAllOptions()
this.resetDatalistFilter()
}, 100)
}
calculateInputPosition() {
const inputElement = this.inputElement.nativeElement
const clientRect = inputElement?.getBoundingClientRect()
if (clientRect.width > 0 && clientRect.height > 0) {
this.inputClientRect = {
width: clientRect.width,
bottom: clientRect.bottom
}
}
this.innerHeight = window.innerHeight
}
onDataListMouseEnter(event: any) {
this.preventDatalistClose = true
}
onDataListMouseLeave(event: any) {
this.preventDatalistClose = false
this.inputElement.nativeElement.focus()
}
onDataListScroll(event: any) {
this.setToCloseDatalist()
}
onDataListClick(event: any) {
const clickedOption = event.target
const itemType = clickedOption.dataset?.type
if (itemType === 'load-more') {
this.loadMore()
return
}
if (clickedOption.tagName === 'OPTION') {
this.setOptionValue(clickedOption.innerText, clickedOption.value)
return
}
}
loadMore() {
this.onLoadingMore.emit({
loadMoreFinished: (moreExists: boolean) => {
this.loadingMore = false
this.enableLoadMore = moreExists
}
})
this.loadingMore = true
}
setOptionValue(label: string, value: string) {
if (label || value) {
if (!value) value = label
}
if (value) {
this.preventDatalistClose = false
this.value = value
this.onInputFired(null, true, 'click')
this.setToCloseDatalist()
}
}
onMainKeydown(event: KeyboardEvent) {
switch (event.key) {
case 'ArrowUp': {
this.getOptionsMoveFocus('up')
break
}
case 'ArrowDown': {
this.getOptionsMoveFocus('down')
break
}
case 'Enter': {
this.selectFocusedOption()
break
}
}
}
onInputKeyup(event: any) {
if (!this.datalistOpen) {
return true
}
// We prevent propagation of Escape event
// since it would close modal also if input is
// inside of it (or trigger any other actions that are listening on escape)
if (event.key === 'Escape') {
event.stopPropagation()
event.stopImmediatePropagation()
this.setToCloseDatalist()
return false
}
}
unselectAllOptions() {
const options = document.querySelectorAll(
`#datalist_${this.inputId} option`
)
for (let i = 0; i < options.length; i++) {
options[i].classList.remove('focused')
}
}
selectFocusedOption() {
const option = document.querySelector(
`#datalist_${this.inputId} option.focused`
)
if (option) this.onDataListClick({ target: option })
this.setToCloseDatalist()
}
getOptionsMoveFocus(direction: 'up' | 'down') {
const options = document.querySelectorAll(
`#datalist_${this.inputId} option:not(.hidden)`
)
let focusedIndex = 0
for (let i = 0; i < options.length; i++) {
const option = options[i]
if (option.classList.contains('focused')) {
focusedIndex = direction === 'down' ? i + 1 : i - 1
if (focusedIndex < 0) focusedIndex = options.length - 1
if (focusedIndex > options.length - 1) focusedIndex = 0
option.classList.remove('focused')
break
}
}
options[focusedIndex].classList.add('focused')
}
@HostListener('window:resize', ['$event'])
onResize(event: any) {
this.calculateInputPosition()
}
}