import {Component, HostListener, Input, OnChanges, OnInit, Output, EventEmitter, ExistingProvider, ViewChild, ViewEncapsulation, forwardRef, ElementRef, SimpleChange, SimpleChanges, ContentChild, TemplateRef} from '@angular/core'; import {NG_VALUE_ACCESSOR, ControlValueAccessor} from '@angular/forms'; import {SelectDropdownComponent} from './select-dropdown.component'; import {IOption} from './option.interface'; import {Option} from './option'; import {OptionList} from './option-list'; export const SELECT_VALUE_ACCESSOR: ExistingProvider = { provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => SelectComponent), multi: true }; @Component({ selector: 'ng-select', templateUrl: 'select.component.html', styleUrls: ['select.component.scss'], providers: [SELECT_VALUE_ACCESSOR], encapsulation: ViewEncapsulation.None }) export class SelectComponent implements ControlValueAccessor, OnChanges, OnInit { // Data input. @Input() options: Array = []; // Functionality settings. @Input() allowClear: boolean = false; @Input() disabled: boolean = false; @Input() multiple: boolean = false; @Input() multipleCloseDropdownAfterSelection: boolean = true; @Input() noFilter: number = 0; // Style settings. @Input() highlightColor: string; @Input() highlightTextColor: string; // Text settings. @Input() notFoundMsg: string = 'No results found'; @Input() placeholder: string = ''; @Input() filterPlaceholder: string = ''; @Input() label: string = ''; // Output events. @Output() opened = new EventEmitter(); @Output() closed = new EventEmitter(); @Output() selected = new EventEmitter(); @Output() deselected = new EventEmitter(); @Output() focus = new EventEmitter(); @Output() blur = new EventEmitter(); @Output() noOptionsFound = new EventEmitter(); @Output() filterInputChanged = new EventEmitter(); @ViewChild('selection') selectionSpan: ElementRef; @ViewChild('dropdown') dropdown: SelectDropdownComponent; @ViewChild('filterInput') filterInput: ElementRef; @ContentChild('optionTemplate') optionTemplate: TemplateRef; private _value: Array = []; private optionList: OptionList = new OptionList([]); // View state variables. hasFocus: boolean = false; isOpen: boolean = false; isBelow: boolean = true; private filterEnabled: boolean = true; private filterInputWidth: number = 1; private isDisabled: boolean = false; private placeholderView: string = ''; private clearClicked: boolean = false; private selectContainerClicked: boolean = false; private optionListClicked: boolean = false; private optionClicked: boolean = false; // Width and position for the dropdown container. private width: number; private top: number; private left: number; private onChange = (_: any) => {}; private onTouched = () => {}; constructor( private hostElement: ElementRef ) {} /** Event handlers. **/ ngOnInit() { this.placeholderView = this.placeholder; } ngOnChanges(changes: SimpleChanges) { this.handleInputChanges(changes); } ngAfterViewInit() { this.updateState(); } @HostListener('window:blur') onWindowBlur() { this._blur(); } @HostListener('window:click') onWindowClick() { if (!this.selectContainerClicked && (!this.optionListClicked || (this.optionListClicked && this.optionClicked))) { if (!(this.multiple && this.multipleCloseDropdownAfterSelection)) { // Don't close if using multi-selction and multipleCloseDropdownAfterSelection is false this.closeDropdown(this.optionClicked); } if (!this.optionClicked) { // Close in any case (if not already happened) this.closeDropdown(this.optionClicked); this._blur(); } } this.clearClicked = false; this.selectContainerClicked = false; this.optionListClicked = false; this.optionClicked = false; } @HostListener('window:resize') onWindowResize() { this.updateWidth(); } onSelectContainerClick(event: any) { this.selectContainerClicked = true; if (!this.clearClicked) { this.toggleDropdown(); } } onSelectContainerFocus() { this._focus(); } onSelectContainerKeydown(event: any) { this.handleSelectContainerKeydown(event); } onOptionsListClick() { this.optionListClicked = true; } onDropdownOptionClicked(option: Option) { this.optionClicked = true; this.multiple ? this.toggleSelectOption(option) : this.selectOption(option); } onSingleFilterClick() { this.selectContainerClicked = true; } onSingleFilterFocus() { this._focus(); } onFilterInput(term: string) { this.filterInputChanged.emit(term); this.filter(term); } onSingleFilterKeydown(event: any) { this.handleSingleFilterKeydown(event); } onMultipleFilterKeydown(event: any) { this.handleMultipleFilterKeydown(event); } onMultipleFilterFocus() { this._focus(); } onClearSelectionClick(event: any) { this.clearClicked = true; this.clearSelection(); this.closeDropdown(true); } onDeselectOptionClick(option: Option) { this.clearClicked = true; this.deselectOption(option); } /** API. **/ // TODO fix issues with global click/key handler that closes the dropdown. open() { this.openDropdown(); } close() { this.closeDropdown(false); } clear() { this.clearSelection(); } select(value: string | Array) { this.writeValue(value); } /** ControlValueAccessor interface methods. **/ writeValue(value: any) { this.value = value; } registerOnChange(fn: (_: any) => void) { this.onChange = fn; } registerOnTouched(fn: () => void) { this.onTouched = fn; } setDisabledState(isDisabled: boolean) { this.disabled = isDisabled; } /** Input change handling. **/ private handleInputChanges(changes: SimpleChanges) { let optionsChanged: boolean = changes.hasOwnProperty('options'); let noFilterChanged: boolean = changes.hasOwnProperty('noFilter'); let placeholderChanged: boolean = changes.hasOwnProperty('placeholder'); if (optionsChanged) { this.updateOptionList(changes.options.currentValue); this.updateState(); } if (optionsChanged || noFilterChanged) { this.updateFilterEnabled(); } if (placeholderChanged) { this.updateState(); } } private updateOptionList(options: Array) { this.optionList = new OptionList(options); this.optionList.value = this._value; } private updateFilterEnabled() { this.filterEnabled = this.optionList.options.length >= this.noFilter; } /** Value. **/ get value(): string | string[] { return this.multiple ? this._value : this._value[0]; } set value(v: string | string[]) { if (typeof v === 'undefined' || v === null || v === '') { v = []; } else if (typeof v === 'string') { v = [v]; } else if (!Array.isArray(v)) { throw new TypeError('Value must be a string or an array.'); } this.optionList.value = v; this._value = v; this.updateState(); } private valueChanged() { this._value = this.optionList.value; this.updateState(); this.onChange(this.value); } private updateState() { this.placeholderView = this.optionList.hasSelected ? '' : this.placeholder; setTimeout(() => { this.updateFilterWidth(); }); } /** Select. **/ private selectOption(option: Option) { if (!option.selected && !option.disabled) { this.optionList.select(option, this.multiple); this.valueChanged(); this.selected.emit(option.wrappedOption); } } private deselectOption(option: Option) { if (option.selected) { this.optionList.deselect(option); this.valueChanged(); this.deselected.emit(option.wrappedOption); setTimeout(() => { if (this.multiple) { this.updatePosition(); this.optionList.highlight(); if (this.isOpen) { this.dropdown.moveHighlightedIntoView(); } } }); } } private clearSelection() { let selection: Array