novo-elements
Version:
Bullhorn's NOVO Element Repository for Angular 2
316 lines (285 loc) • 10.8 kB
text/typescript
// NG2
import { Component, EventEmitter, ElementRef, ViewContainerRef, forwardRef, ViewChild, Input, Output, OnInit, DoCheck, Renderer, HostListener, ChangeDetectorRef } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
// Vendor
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/fromEvent';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/operator/distinctUntilChanged';
// APP
import { OutsideClick } from '../../utils/outside-click/OutsideClick';
import { KeyCodes } from '../../utils/key-codes/KeyCodes';
import { PickerResults } from './extras/picker-results/PickerResults';
import { ComponentUtils } from '../../utils/component-utils/ComponentUtils';
import { Helpers } from '../../utils/Helpers';
import { NovoPickerContainer } from './extras/picker-container/PickerContainer';
import { NovoOverlayTemplate } from '../overlay/Overlay';
// Value accessor for the component (supports ngModel)
const PICKER_VALUE_ACCESSOR = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => NovoPickerElement),
multi: true
};
/**
* @name Picker
*
* @description This class is the directive definition of the Picker. If you add and attribute of `picker` to an input,
* it will create an instance of the picker which wraps the input in all of the picker HTML elements and functionality.
* Picker should be added as a two-way bound ngModel instance `[(picker)]=""` in order to have the picker options
* dynamically populate.
*/
export class NovoPickerElement implements OnInit {
// Container for the results
results: ViewContainerRef;
config: any;
placeholder: string;
clearValueOnSelect: boolean;
closeOnSelect: boolean = true;
selected: Array<any> = [];
// Deprecated
appendToBody: boolean = false;
// Deprecated
parentScrollSelector: string;
// Deprecated
parentScrollAction: string = 'close';
// Custom class for the dropdown container
containerClass: string;
// Side the dropdown will open
side: string = 'left';
// Autoselects the first option in the results
autoSelectFirstOption: boolean = true;
overrideElement: ElementRef;
// Emitter for selects
select: EventEmitter<any> = new EventEmitter();
focus: EventEmitter<any> = new EventEmitter();
blur: EventEmitter<any> = new EventEmitter();
typing: EventEmitter<any> = new EventEmitter();
public container: NovoOverlayTemplate;
private input: ElementRef;
closeHandler: any;
isStatic: boolean = true;
term: string = '';
resultsComponent: any;
popup: any;
_value: any;
onModelChange: Function = () => { };
onModelTouched: Function = () => { };
constructor(public element: ElementRef, private componentUtils: ComponentUtils, private _changeDetectorRef: ChangeDetectorRef) {
// Setup handlers
//this.closeHandler = this.toggleActive.bind(this);
}
ngOnInit() {
if (this.overrideElement) {
this.element = this.overrideElement;
}
if (this.appendToBody) {
console.warn(`'appendToBody' has been deprecated. Please remove this attribute.`);
}
// Custom results template
this.resultsComponent = this.config.resultsTemplate || PickerResults;
// Get all distinct key up events from the input and only fire if long enough and distinct
//let input = this.element.nativeElement.querySelector('input');
const pasteObserver = Observable.fromEvent(this.input.nativeElement, 'paste')
.debounceTime(250)
.distinctUntilChanged();
pasteObserver.subscribe(
(event: ClipboardEvent) => this.onDebouncedKeyup(event),
err => this.hideResults(err));
const keyboardObserver = Observable.fromEvent(this.input.nativeElement, 'keyup')
.debounceTime(250)
.distinctUntilChanged();
keyboardObserver.subscribe(
(event: KeyboardEvent) => this.onDebouncedKeyup(event),
err => this.hideResults(err));
}
private onDebouncedKeyup(event: Event) {
if ([KeyCodes.ESC, KeyCodes.UP, KeyCodes.DOWN, KeyCodes.ENTER, KeyCodes.TAB].includes(event['keyCode'])) {
return;
}
this.show((event.target as any).value);
}
/** BEGIN: Convienient Panel Methods. */
public openPanel(): void {
this.container.openPanel();
}
public closePanel(): void {
this.container.closePanel();
}
public get panelOpen(): boolean {
return this.container && this.container.panelOpen;
}
/** END: Convienient Panel Methods. */
private show(term?: string): void {
this.openPanel();
// Show the results inside
this.showResults(term);
}
private hide(): void {
this.closePanel();
}
onKeyDown(event: KeyboardEvent) {
if (this.panelOpen) {
if (event.keyCode === KeyCodes.ESC || event.keyCode === KeyCodes.TAB) {
this.hideResults();
return;
}
if (event.keyCode === KeyCodes.UP) {
this.popup.instance.prevActiveMatch();
return;
}
if (event.keyCode === KeyCodes.DOWN) {
this.popup.instance.nextActiveMatch();
return;
}
if (event.keyCode === KeyCodes.ENTER) {
this.popup.instance.selectActiveMatch();
return;
}
if (event.keyCode === KeyCodes.BACKSPACE && !Helpers.isBlank(this._value)) {
this.clearValue(false);
this.closePanel();
}
}
}
clearValue(wipeTerm) {
this._value = null;
this.select.emit(this._value);
this.onModelChange(this._value);
if (wipeTerm) {
this.term = null;
this.hideResults();
}
}
/**
* @name onFocus
* @description When the input's focus event is called this method calls the debounced function that displays the
* results.
*/
onFocus(event) {
this.show();
this.focus.emit(event);
}
/**
* @name showResults
*
* @description This method creates an instance of the results (called popup) and adds all the bindings to that
* instance.
*/
showResults(term?: any) {
// Update Matches
if (this.popup) {
// Update existing list or create the DOM element
this.popup.instance.config = this.config;
this.popup.instance.term = this.term;
this.popup.instance.selected = this.selected;
this.popup.instance.autoSelectFirstOption = this.autoSelectFirstOption;
} else {
this.popup = this.componentUtils.appendNextToLocation(this.resultsComponent, this.results);
this.popup.instance.parent = this;
this.popup.instance.config = this.config;
this.popup.instance.term = this.term;
this.popup.instance.selected = this.selected;
this.popup.instance.autoSelectFirstOption = this.autoSelectFirstOption;
this.popup.instance.overlay = this.container._overlayRef;
}
}
/**
* @name hideResults
*
* @description - This method deletes the picker results from the DOM.
*/
hideResults(err?: any) {
if (this.popup) {
this.popup.destroy();
this.popup = null;
}
this.hide();
}
// get accessor
get value() {
return this._value;
}
// set accessor including call the onchange callback
set value(selected) {
if (!selected) {
this.term = '';
this._value = null;
this.onModelChange(this._value);
} else if (selected.value !== this._value) {
this.term = this.clearValueOnSelect ? '' : selected.label;
this._value = selected.value;
this.select.emit(selected);
this.onModelChange(selected.value);
} else {
this.select.emit(selected);
}
}
// Makes sure to clear the model if the user clears the text box
checkTerm(event) {
this.typing.emit(event);
if (!event || !event.length) {
this._value = null;
this.onModelChange(this._value);
}
}
// Set touched on blur
onTouched(event?: Event) {
this.onModelTouched();
this.blur.emit(event);
}
// From ControlValueAccessor interface
writeValue(value) {
if (this.clearValueOnSelect) {
this.term = '';
} else {
if (typeof value === 'string') {
this.term = value;
} else if (value && value.label) {
this.term = value.label;
} else if (value && value.firstName) {
this.term = `${value.firstName} ${value.lastName}`;
} else if (value && value.name) {
this.term = value.name;
} else if (this.config.getLabels && typeof this.config.getLabels === 'function') {
this.config.getLabels(value).then(result => {
if (result) {
this.term = result.label || '';
} else {
this.term = value;
}
});
} else {
this.term = value;
}
}
this._value = value;
}
registerOnChange(fn: Function): void {
this.onModelChange = fn;
}
registerOnTouched(fn: Function): void {
this.onModelTouched = fn;
}
}