web/frontend/src/app/libs/deltix-ng-autocomplete/src/ts/components/autocomplete-base.ts (277 lines of code) (raw):

import { AfterViewChecked, AfterViewInit, Component, ElementRef, EventEmitter, OnChanges, OnDestroy, OnInit, SimpleChanges, } from '@angular/core'; import {ControlValueAccessor} from '@angular/forms'; @Component({ // tslint:disable-next-line:component-selector selector: 'deltix-ng-autocomplete-base', template: '', // tslint:disable-next-line:component-class-suffix }) export abstract class AutocompleteBase implements ControlValueAccessor, OnDestroy, OnChanges, AfterViewInit, AfterViewChecked, OnChanges, OnInit { public selectedText = ''; public disabled: boolean; public cssClass: string; protected inputElement: HTMLInputElement; protected onDocumentClick: () => any; protected skipDocumentClick = false; protected onChange: any = Function.prototype; protected onTouched: any = Function.prototype; protected changeInput: EventEmitter<string>; protected valueGetter: (value: any) => string; protected descriptionGetter: (value: any, highlightFunc: (str: string) => string) => string; protected highlight: boolean; protected stripTags = true; protected values: Array<any>; protected dropdownOuterContainer: HTMLElement; protected dropdownContainer: ElementRef; protected onDocumentEvent: () => void; protected showDropdownChange: EventEmitter<boolean>; protected tempDiv = document.createElement('DIV'); public constructor(protected element: ElementRef) { this.dropdownOuterContainer = document.createElement('div'); this.dropdownOuterContainer.classList.add( 'deltix-autocomplete', 'autocomplete-outer-container', ); document.body.appendChild(this.dropdownOuterContainer); this.onDocumentEvent = () => { this.updateDropdownPosition(); this.onChangePosition(); }; document.addEventListener('scroll', this.onDocumentEvent, true); window.addEventListener('resize', this.onDocumentEvent, true); } private _showDropdown = false; public get showDropdown(): boolean { return this._showDropdown; } public set showDropdown(value: boolean) { this.setShowDropdown(value); } public onAutocompleteClick(event: any) { this.skipDocumentClick = true; } public onInput(event: KeyboardEvent) { const element = <HTMLInputElement>event.target; this.inputEmit(element.value); } public onFocus(event: Event) { this.showDropdown = true; } public ngOnInit(): void { document.addEventListener( 'click', (this.onDocumentClick = () => { if (!this.skipDocumentClick) { this.showDropdown = false; } this.skipDocumentClick = false; this.onDocumentClickCallback.call(this); }), ); } public ngOnChanges(changes: SimpleChanges): void { this.initInputElement(); if (changes['dropdown'] != null) { if (this.isFalse(changes['dropdown'].currentValue)) { this.inputElement.removeAttribute('readonly'); } else { this.inputElement.setAttribute('readonly', ''); } } } public ngAfterViewInit() { this.dropdownOuterContainer.appendChild(this.dropdownContainer.nativeElement); if (this.cssClass != null) { this.dropdownOuterContainer.classList.add(this.cssClass); } } public ngAfterViewChecked() { if (this.disabled || !this.showDropdown) { return; } this.updateDropdownPosition(); } public registerOnChange(fn: any): void { this.onChange = fn; } public registerOnTouched(fn: any): void { this.onTouched = fn; } public setDisabledState(isDisabled: boolean): void { this.disabled = isDisabled; } public abstract writeValue(obj: any): void; public ngOnDestroy(): void { document.removeEventListener('click', this.onDocumentClick); document.removeEventListener('scroll', this.onDocumentEvent); window.removeEventListener('resize', this.onDocumentEvent); this.dropdownOuterContainer.remove(); } public isSelected(value: any): boolean { return false; } public onKeyUp(event: KeyboardEvent) { if (event.keyCode === 40) { // down const el = <HTMLElement>this.dropdownContainer.nativeElement; const items = el.querySelectorAll('.autocomplete-dropdown-item'); if (items.length > 0) { const first: HTMLElement = <HTMLElement>items.item(0).firstElementChild; first.focus(); } return; } } public onInputClick(event: Event) {} public onChangePosition() { } public getTitleAttrValueForItem(value: any): string { const title = this.getTitleForItem(value); return this.stripTagsFromString(title); } public abstract select(item: any, event: Event): void; public abstract onBlur(event: Event): void; protected setShowDropdown(value: boolean) { this._showDropdown = value; if (this.showDropdownChange != null) { this.showDropdownChange.emit(this._showDropdown); } } protected onElementKeyUp(event: KeyboardEvent) { if (event.keyCode === 40) { // down const next = (<HTMLElement>event.target).parentElement.nextElementSibling; if (next != null && next['tagName'] === 'LI') { const a = <HTMLElement>next.firstElementChild; a.focus(); } } else if (event.keyCode === 38) { // up const prev = (<HTMLElement>event.target).parentElement.previousElementSibling; if (prev != null && prev['tagName'] === 'LI') { const a = <HTMLElement>prev.firstElementChild; a.focus(); } else { this.inputElement.focus(); } } } protected inputEmit(str: string) { this.changeInput.emit(str); } protected toggleDropdown(event: Event) { this.showDropdown = !this.showDropdown; } protected highlightTitle(item: any): string { let text: string = this.getTitleForItem(item); if (this.isTrue(this.stripTags)) { text = this.stripTagsFromString(text); if (this.isTrue(this.highlight)) { return this.highlightText(text, this.selectedText); } } return text; } protected escapeRegExp(str: string): string { return str.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); } protected highlightText(text: string, highlightStr: string): string { const regexp = new RegExp(`(${this.escapeRegExp(highlightStr)})`, 'im'); return text.replace(regexp, '<span>$1</span>'); } protected getValueForItem(item: any, separator?: string, literal?: string): string { if (item == null) { return ''; } if (typeof item === 'string') { if (item.includes(separator)) { return `${literal}${item}${literal}`; } return item; } if (typeof item === 'number') { return item + ''; } if (typeof this.valueGetter === 'function') { return this.valueGetter.call(null, item); } return '[object]'; } protected getTitleForItem(item: any): string { if (item != null && typeof this.descriptionGetter === 'function') { return this.descriptionGetter.call(null, item, (str: string) => this.highlightText(str, this.selectedText), ); } return this.getValueForItem(item); } protected stripTagsFromString(str: string) { const tmp = this.tempDiv; tmp.innerHTML = str; return tmp.textContent || tmp.innerText || ''; } protected initInputElement() { if (this.inputElement == null) { this.inputElement = this.element.nativeElement.querySelector('input'); } } protected onDocumentClickCallback() { // empty method } protected isFalse(value: boolean | string): boolean { if (typeof value === 'string') { return value === 'false'; } return !value; } protected isTrue(value: boolean | string): boolean { if (typeof value === 'string') { return value === 'true'; } return value; } protected isShowDropdown(): boolean { return this.showDropdown && this.values.length > 0; } protected updateDropdownPosition() { const div: HTMLElement = this.element.nativeElement; const rect = div.getBoundingClientRect(); const dropdown: HTMLElement = this.dropdownContainer.nativeElement; const dropdownRect = dropdown.getBoundingClientRect(); const offset = rect.top + rect.height; let fullHeight = false; if (window.innerHeight < offset + dropdownRect.height || offset < 0) { if (rect.top - dropdownRect.height < 0) { dropdown.style.top = '0px'; if (dropdownRect.height >= window.innerHeight) { fullHeight = true; dropdown.style.maxHeight = dropdownRect.height + 'px'; } } else { dropdown.style.top = rect.top - dropdownRect.height + 'px'; } } else { dropdown.style.top = offset + 'px'; } dropdown.style.width = rect.width + 'px'; dropdown.style.left = rect.left + 'px'; if (fullHeight) { dropdown.classList.add('autocomplete-full-height'); } else { dropdown.classList.remove('autocomplete-full-height'); } } }