import {NgModule,Component,ViewChild,ElementRef,AfterViewChecked,AfterContentInit,OnDestroy,Input,Output,EventEmitter,ContentChildren,QueryList,TemplateRef,Renderer2,forwardRef,ChangeDetectorRef,IterableDiffers,ChangeDetectionStrategy, ViewEncapsulation} from '@angular/core';
import {CommonModule} from '@angular/common';
import {trigger,style,transition,animate,AnimationEvent} from '@angular/animations';
import {InputTextModule} from 'mirra-ui/inputtext';
import {ButtonModule} from 'mirra-ui/button';
import {RippleModule} from 'mirra-ui/ripple';
import {SharedModule,PrimeTemplate} from 'mirra-ui/api';
import {DomHandler} from 'mirra-ui/dom';
import {ObjectUtils, UniqueComponentId} from 'mirra-ui/utils';
import {NG_VALUE_ACCESSOR, ControlValueAccessor} from '@angular/forms';
export const AUTOCOMPLETE_VALUE_ACCESSOR: any = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AutoComplete),
multi: true
};
@Component({
selector: 'p-autoComplete',
template: `
-
{{resolveFieldData(option)}}
- {{emptyMessage}}
`,
animations: [
trigger('overlayAnimation', [
transition(':enter', [
style({opacity: 0, transform: 'scaleY(0.8)'}),
animate('{{showTransitionParams}}')
]),
transition(':leave', [
animate('{{hideTransitionParams}}', style({ opacity: 0 }))
])
])
],
host: {
'[class.p-inputwrapper-filled]': 'filled',
'[class.p-inputwrapper-focus]': 'focus && !disabled'
},
providers: [AUTOCOMPLETE_VALUE_ACCESSOR],
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
styleUrls: ['./autocomplete.scss']
})
export class AutoComplete implements AfterViewChecked,AfterContentInit,OnDestroy,ControlValueAccessor {
@Input() minLength: number = 1;
@Input() delay: number = 300;
@Input() style: any;
@Input() panelStyle: any;
@Input() styleClass: string;
@Input() panelStyleClass: string;
@Input() inputStyle: any;
@Input() inputId: string;
@Input() inputStyleClass: string;
@Input() placeholder: string;
@Input() readonly: boolean;
@Input() disabled: boolean;
@Input() maxlength: number;
@Input() name: string;
@Input() required: boolean;
@Input() size: number;
@Input() appendTo: any;
@Input() autoHighlight: boolean;
@Input() forceSelection: boolean;
@Input() type: string = 'text';
@Input() autoZIndex: boolean = true;
@Input() baseZIndex: number = 0;
@Input() ariaLabel: string;
@Input() ariaLabelledBy: string;
@Input() dropdownIcon: string = "far fa-chevron-down";
@Input() unique: boolean = true;
@Input() completeOnFocus: boolean = false;
@Output() completeMethod: EventEmitter = new EventEmitter();
@Output() onSelect: EventEmitter = new EventEmitter();
@Output() onUnselect: EventEmitter = new EventEmitter();
@Output() onFocus: EventEmitter = new EventEmitter();
@Output() onBlur: EventEmitter = new EventEmitter();
@Output() onDropdownClick: EventEmitter = new EventEmitter();
@Output() onClear: EventEmitter = new EventEmitter();
@Output() onKeyUp: EventEmitter = new EventEmitter();
@Output() onShow: EventEmitter = new EventEmitter();
@Output() onHide: EventEmitter = new EventEmitter();
@Input() field: string;
@Input() scrollHeight: string = '200px';
@Input() dropdown: boolean;
@Input() dropdownMode: string = 'blank';
@Input() multiple: boolean;
@Input() tabindex: number;
@Input() dataKey: string;
@Input() emptyMessage: string;
@Input() showTransitionOptions: string = '.12s cubic-bezier(0, 0, 0.2, 1)';
@Input() hideTransitionOptions: string = '.1s linear';
@Input() autofocus: boolean;
@Input() autocomplete: string = 'off';
@ViewChild('in') inputEL: ElementRef;
@ViewChild('multiIn') multiInputEL: ElementRef;
@ViewChild('multiContainer') multiContainerEL: ElementRef;
@ViewChild('ddBtn') dropdownButton: ElementRef;
@ContentChildren(PrimeTemplate) templates: QueryList;
overlay: HTMLDivElement;
itemTemplate: TemplateRef;
selectedItemTemplate: TemplateRef;
value: any;
_suggestions: any[];
onModelChange: Function = () => {};
onModelTouched: Function = () => {};
timeout: any;
overlayVisible: boolean = false;
documentClickListener: any;
suggestionsUpdated: boolean;
highlightOption: any;
highlightOptionChanged: boolean;
focus: boolean = false;
filled: boolean;
inputClick: boolean;
inputKeyDown: boolean;
noResults: boolean;
differ: any;
inputFieldValue: string = null;
loading: boolean;
documentResizeListener: any;
forceSelectionUpdateModelTimeout: any;
listId: string;
itemClicked: boolean;
constructor(public el: ElementRef, public renderer: Renderer2, public cd: ChangeDetectorRef, public differs: IterableDiffers) {
this.differ = differs.find([]).create(null);
this.listId = UniqueComponentId() + '_list';
}
@Input() get suggestions(): any[] {
return this._suggestions;
}
set suggestions(val:any[]) {
this._suggestions = val;
this.handleSuggestionsChange();
}
ngAfterViewChecked() {
//Use timeouts as since Angular 4.2, AfterViewChecked is broken and not called after panel is updated
if (this.suggestionsUpdated && this.overlay && this.overlay.offsetParent) {
setTimeout(() => {
if (this.overlay) {
this.alignOverlay();
}
}, 1);
this.suggestionsUpdated = false;
}
if (this.highlightOptionChanged) {
setTimeout(() => {
if (this.overlay) {
let listItem = DomHandler.findSingle(this.overlay, 'li.p-highlight');
if (listItem) {
DomHandler.scrollInView(this.overlay, listItem);
}
}
}, 1);
this.highlightOptionChanged = false;
}
}
handleSuggestionsChange() {
if (this._suggestions != null && this.loading) {
this.highlightOption = null;
if (this._suggestions.length) {
this.noResults = false;
this.show();
this.suggestionsUpdated = true;
if (this.autoHighlight) {
this.highlightOption = this._suggestions[0];
}
}
else {
this.noResults = true;
if (this.emptyMessage) {
this.show();
this.suggestionsUpdated = true;
}
else {
this.hide();
}
}
this.loading = false;
}
}
ngAfterContentInit() {
this.templates.forEach((item) => {
switch(item.getType()) {
case 'item':
this.itemTemplate = item.template;
break;
case 'selectedItem':
this.selectedItemTemplate = item.template;
break;
default:
this.itemTemplate = item.template;
break;
}
});
}
writeValue(value: any) : void {
this.value = value;
this.filled = this.value && this.value != '';
this.updateInputField();
}
registerOnChange(fn: Function): void {
this.onModelChange = fn;
}
registerOnTouched(fn: Function): void {
this.onModelTouched = fn;
}
setDisabledState(val: boolean): void {
this.disabled = val;
}
onInput(event: Event) {
// When an input element with a placeholder is clicked, the onInput event is invoked in IE.
if (!this.inputKeyDown && DomHandler.isIE()) {
return;
}
if (this.timeout) {
clearTimeout(this.timeout);
}
let value = ( event.target).value;
if (!this.multiple && !this.forceSelection) {
this.onModelChange(value);
}
if (value.length === 0 && !this.multiple) {
this.hide();
this.onClear.emit(event);
this.onModelChange(value);
}
if (value.length >= this.minLength) {
this.timeout = setTimeout(() => {
this.search(event, value);
}, this.delay);
}
else {
this.suggestions = null;
this.hide();
}
this.updateFilledState();
this.inputKeyDown = false;
}
onInputClick(event: MouseEvent) {
if (this.documentClickListener) {
this.inputClick = true;
}
}
search(event: any, query: string) {
//allow empty string but not undefined or null
if (query === undefined || query === null) {
return;
}
this.loading = true;
this.completeMethod.emit({
originalEvent: event,
query: query
});
}
selectItem(option: any, focus: boolean = true) {
if (this.forceSelectionUpdateModelTimeout) {
clearTimeout(this.forceSelectionUpdateModelTimeout);
this.forceSelectionUpdateModelTimeout = null;
}
if (this.multiple) {
this.multiInputEL.nativeElement.value = '';
this.value = this.value||[];
if (!this.isSelected(option) || !this.unique) {
this.value = [...this.value,option];
this.onModelChange(this.value);
}
}
else {
this.inputEL.nativeElement.value = this.field ? ObjectUtils.resolveFieldData(option, this.field)||'': option;
this.value = option;
this.onModelChange(this.value);
}
this.onSelect.emit(option);
this.updateFilledState();
if (focus) {
this.itemClicked = true;
this.focusInput();
}
}
show() {
if (this.multiInputEL || this.inputEL) {
let hasFocus = this.multiple ? document.activeElement == this.multiInputEL.nativeElement : document.activeElement == this.inputEL.nativeElement ;
if (!this.overlayVisible && hasFocus) {
this.overlayVisible = true;
}
}
}
onOverlayAnimationStart(event: AnimationEvent) {
switch (event.toState) {
case 'visible':
this.overlay = event.element;
this.appendOverlay();
if (this.autoZIndex) {
this.overlay.style.zIndex = String(this.baseZIndex + (++DomHandler.zindex));
}
this.alignOverlay();
this.bindDocumentClickListener();
this.bindDocumentResizeListener();
this.onShow.emit(event);
break;
case 'void':
this.onOverlayHide();
break;
}
}
onOverlayAnimationDone(event: AnimationEvent) {
if (event.toState === 'void') {
this._suggestions = null;
}
}
appendOverlay() {
if (this.appendTo) {
if (this.appendTo === 'body')
document.body.appendChild(this.overlay);
else
DomHandler.appendChild(this.overlay, this.appendTo);
if (!this.overlay.style.minWidth) {
this.overlay.style.minWidth = DomHandler.getWidth(this.el.nativeElement.children[0]) + 'px';
}
}
}
resolveFieldData(value) {
return this.field ? ObjectUtils.resolveFieldData(value, this.field): value;
}
restoreOverlayAppend() {
if (this.overlay && this.appendTo) {
this.el.nativeElement.appendChild(this.overlay);
}
}
alignOverlay() {
if (this.appendTo)
DomHandler.absolutePosition(this.overlay, (this.multiple ? this.multiContainerEL.nativeElement : this.inputEL.nativeElement));
else
DomHandler.relativePosition(this.overlay, (this.multiple ? this.multiContainerEL.nativeElement : this.inputEL.nativeElement));
}
hide() {
this.overlayVisible = false;
this.cd.markForCheck();
}
handleDropdownClick(event) {
if (!this.overlayVisible) {
this.focusInput();
let queryValue = this.multiple ? this.multiInputEL.nativeElement.value : this.inputEL.nativeElement.value;
if (this.dropdownMode === 'blank')
this.search(event, '');
else if (this.dropdownMode === 'current')
this.search(event, queryValue);
this.onDropdownClick.emit({
originalEvent: event,
query: queryValue
});
}
else {
this.hide();
}
}
focusInput() {
if (this.multiple)
this.multiInputEL.nativeElement.focus();
else
this.inputEL.nativeElement.focus();
}
removeItem(item: any) {
let itemIndex = DomHandler.index(item);
let removedValue = this.value[itemIndex];
this.value = this.value.filter((val, i) => i!=itemIndex);
this.onModelChange(this.value);
this.updateFilledState();
this.onUnselect.emit(removedValue);
}
onKeydown(event) {
if (this.overlayVisible) {
let highlightItemIndex = this.findOptionIndex(this.highlightOption);
switch(event.which) {
//down
case 40:
if (highlightItemIndex != -1) {
var nextItemIndex = highlightItemIndex + 1;
if (nextItemIndex != (this.suggestions.length)) {
this.highlightOption = this.suggestions[nextItemIndex];
this.highlightOptionChanged = true;
}
}
else {
this.highlightOption = this.suggestions[0];
}
event.preventDefault();
break;
//up
case 38:
if (highlightItemIndex > 0) {
let prevItemIndex = highlightItemIndex - 1;
this.highlightOption = this.suggestions[prevItemIndex];
this.highlightOptionChanged = true;
}
event.preventDefault();
break;
//enter
case 13:
if (this.highlightOption) {
this.selectItem(this.highlightOption);
this.hide();
}
event.preventDefault();
break;
//escape
case 27:
this.hide();
event.preventDefault();
break;
//tab
case 9:
if (this.highlightOption) {
this.selectItem(this.highlightOption);
}
this.hide();
break;
}
} else {
if (event.which === 40 && this.suggestions) {
this.search(event,event.target.value);
}
}
if (this.multiple) {
switch(event.which) {
//backspace
case 8:
if (this.value && this.value.length && !this.multiInputEL.nativeElement.value) {
this.value = [...this.value];
const removedValue = this.value.pop();
this.onModelChange(this.value);
this.updateFilledState();
this.onUnselect.emit(removedValue);
}
break;
}
}
this.inputKeyDown = true;
}
onKeyup(event) {
this.onKeyUp.emit(event);
}
onInputFocus(event) {
if (!this.itemClicked && this.completeOnFocus ) {
let queryValue = this.multiple ? this.multiInputEL.nativeElement.value : this.inputEL.nativeElement.value;
this.search(event, queryValue);
}
this.focus = true;
this.onFocus.emit(event);
this.itemClicked = false;
}
onInputBlur(event) {
this.focus = false;
this.onModelTouched();
this.onBlur.emit(event);
}
onInputChange(event) {
if (this.forceSelection) {
let valid = false;
let inputValue = event.target.value.trim();
if (this.suggestions) {
for (let suggestion of this.suggestions) {
let itemValue = this.field ? ObjectUtils.resolveFieldData(suggestion, this.field) : suggestion;
if (itemValue && inputValue === itemValue.trim()) {
valid = true;
this.forceSelectionUpdateModelTimeout = setTimeout(() => {
this.selectItem(suggestion, false);
}, 250);
break;
}
}
}
if (!valid) {
if (this.multiple) {
this.multiInputEL.nativeElement.value = '';
}
else {
this.value = null;
this.inputEL.nativeElement.value = '';
}
this.onClear.emit(event);
this.onModelChange(this.value);
}
}
}
onInputPaste(event: ClipboardEvent) {
this.onKeydown(event);
}
isSelected(val: any): boolean {
let selected: boolean = false;
if (this.value && this.value.length) {
for (let i = 0; i < this.value.length; i++) {
if (ObjectUtils.equals(this.value[i], val, this.dataKey)) {
selected = true;
break;
}
}
}
return selected;
}
findOptionIndex(option): number {
let index: number = -1;
if (this.suggestions) {
for (let i = 0; i < this.suggestions.length; i++) {
if (ObjectUtils.equals(option, this.suggestions[i])) {
index = i;
break;
}
}
}
return index;
}
updateFilledState() {
if (this.multiple)
this.filled = (this.value && this.value.length) || (this.multiInputEL && this.multiInputEL.nativeElement && this.multiInputEL.nativeElement.value != '');
else
this.filled = (this.inputFieldValue && this.inputFieldValue != '') || (this.inputEL && this.inputEL.nativeElement && this.inputEL.nativeElement.value != '');;
}
updateInputField() {
let formattedValue = this.value ? (this.field ? ObjectUtils.resolveFieldData(this.value, this.field)||'' : this.value) : '';
this.inputFieldValue = formattedValue;
if (this.inputEL && this.inputEL.nativeElement) {
this.inputEL.nativeElement.value = formattedValue;
}
this.updateFilledState();
}
bindDocumentClickListener() {
if (!this.documentClickListener) {
this.documentClickListener = this.renderer.listen('document', 'click', (event) => {
if (event.which === 3) {
return;
}
if (!this.inputClick && !this.isDropdownClick(event)) {
this.hide();
}
this.inputClick = false;
this.cd.markForCheck();
});
}
}
isDropdownClick(event) {
if (this.dropdown) {
let target = event.target;
return (target === this.dropdownButton.nativeElement || target.parentNode === this.dropdownButton.nativeElement);
}
else {
return false;
}
}
unbindDocumentClickListener() {
if (this.documentClickListener) {
this.documentClickListener();
this.documentClickListener = null;
}
}
bindDocumentResizeListener() {
this.documentResizeListener = this.onWindowResize.bind(this);
window.addEventListener('resize', this.documentResizeListener);
}
unbindDocumentResizeListener() {
if (this.documentResizeListener) {
window.removeEventListener('resize', this.documentResizeListener);
this.documentResizeListener = null;
}
}
onWindowResize() {
this.hide();
}
onOverlayHide() {
this.unbindDocumentClickListener();
this.unbindDocumentResizeListener();
this.overlay = null;
this.onHide.emit();
}
ngOnDestroy() {
if (this.forceSelectionUpdateModelTimeout) {
clearTimeout(this.forceSelectionUpdateModelTimeout);
this.forceSelectionUpdateModelTimeout = null;
}
this.restoreOverlayAppend();
this.onOverlayHide();
}
}
@NgModule({
imports: [CommonModule,InputTextModule,ButtonModule,SharedModule,RippleModule],
exports: [AutoComplete,SharedModule],
declarations: [AutoComplete]
})
export class AutoCompleteModule { }