// Angular // import { Component, Input, OnInit, ElementRef, Output, EventEmitter, HostListener, AfterViewInit } from '@angular/core'; // Interface // import { IUpdateModel } from '../forms.interface'; // RxJs // import { Subject } from 'rxjs/Subject'; // Other // import * as _ from 'underscore'; import * as statics from '@fb/statics'; const styles: any[] = [require('./fb-form-input-multiselect.component.less')]; // Todo's /Viktor L // Implementera funktionalitet för disabled // Se över om man behöver implementera optionsFn, om ja implementera // Implementera disabledReason // Kolla todo's i component och html // implementera addNewItem /** * @ngdoc fbFormMultiSelect * @name fasit.directives.#fbFormMultiSelect * @fbFormInputMultiselect * * @description * Ersätter fbFormMultiSelect * Directive som skapar en div med class-namnet "form-group" och en label med tillhörande select under den. * * @params * @param model Input: any[]. Modell, en lista av gjorda val från options * @param options Input: Object[]. Lista med objekt över de val som finns * @param disabled Input: boolean. Disabled om satt till true. Gamla namnet var disable // Todo ej implementerad * @param disableReason Input: string. Används som tooltip vid disabled // Todo ej implementerad * @param noLabel Input: boolean. Sätt till true för att dölja label * @param label Input: string. Label att visa. Visas inte om tight eller noLabel är satt * @param showAttr Input: string. Namn på vad propertyn som ska visas för objekten i listan heter. * (Om detta är ett changetrack, skriv ex "mittShowAttr.value"). Gammalt namn: showAttrArray * @param valueAttr Input: string. Namn på vad propertyn som är värdet för objekten i listan heter, detta denna model.value matchar mot. Gammalt namn: keyAttrArray. * @param allowAddOption Input: boolean. Vid true kan användaren lägga till val i options listan * @param useExternalOptionFn Input: boolean. vid true ändrar inte komponenten modellen, utan räknar med att det sker manuellt * @param onValueChange Output: IUpdateModel. Emittar när modellen uppdaterats */ // Todo se över om dessa parameterar behövs vid fortsatt implementation // onAdd: '&', // optionsFn: '&?', // optionsFnConditionToRun: '&?', @Component({ selector: 'fb-form-input-multiselect', templateUrl: './fb-form-input-multiselect.component.html', styles: styles }) export class FbFormInputMultiselectComponent implements OnInit, AfterViewInit { @Input() model: any[]; @Input() options: Object[]; @Input() label: string; @Input() valueAttr: string; @Input() showAttr: string; @Input() // Todo implementera allowAddOption: boolean = false; @Input() // Todo implementera disabled: boolean = false; @Input() // Todo implementera disableReason: string; @Input() noLabel: boolean = false; @Input() useExternalOptionFn: boolean = false; @Output() onValueChange: EventEmitter = new EventEmitter(); optionDropdownActive: boolean; pureOptions: any[]; // Todo rename to filteredOptionList? newItem: string = ''; keyEventSubject: Subject; private selfElm: Element; private hostElm: Node; private inputElement: HTMLInputElement; constructor( private readonly elementRef: ElementRef ) { } ngOnInit(): void { this.checkAttributesBeforeInit(); this.initObservables(); } ngAfterViewInit(): void { this.setupUiVariables(); this.awaitOptionPromise(); this.resizeInput(); } @HostListener('window:click', ['$event']) clickOutside(event): void { if (!this.optionDropdownActive) { return; } statics.clickOutside(event, this.selfElm, this.hostElm, () => { this.hideOptionDropdown(); }); } keypressHandler(keyboardEvent: KeyboardEvent): void { this.keyEventSubject.next(keyboardEvent); switch (keyboardEvent.keyCode) { case fb.FasITDomain.KeyCode.backspaceKeyCode: this.onBackspacePress(); break; case fb.FasITDomain.KeyCode.tabKeyCode: this.hideOptionDropdown(); break; default: setTimeout(() => { this.createPureOptions(); }); this.showOptionDropdown(); break; } setTimeout(() => { this.resizeInput(); }); } modelHasValue(): boolean { return this.model.length !== 0 || this.newItem.length >= 1 || this.optionDropdownActive; } removeTagItem(item: any): void { setTimeout(() => { const index: number = this.model.findIndex(x => x === item); this.model.splice(index, 1); this.onModelUpdate(); }); } selectOption(pureOption: any): void { this.newItem = ''; const value: any = this.getKeyValue(pureOption); this.model.push(value); this.onModelUpdate(); } getDisplayTextForModel(modelItem: any): string { const selectedOption: any = _.filter(this.options, (option: any) => { const comparer: any = this.getKeyValue(option); return comparer === modelItem; }); return this.getDisplayValue(selectedOption[0]); } addNewItem(): void { // Todo throw(new Error('Not implemented yet')); // $scope.addNewItem = function () { // var data = { value: $scope.newItem }; // $scope.onAdd({ input: data }); // $scope.model.push(data.value); // $scope.options.push(data.value); // createPureOptions(); //Denna behövs inte köras nu, men om nån ändrar något någon gång kanske den behövs // showDroppis(); // $scope.newItem = ''; // }; } focusInput(): void { this.inputElement.select(); this.showOptionDropdown(); } onInputFocus(): void { this.showOptionDropdown(); } checkFocus(event: any): void { if (this.useExternalOptionFn) { event.preventDefault(); } } getDisplayValue(option: Object): string { // Todo ska showAttrFilter (pipe) implementeras? // if ($scope.showAttrFilter) { // return $filter($scope.showAttrFilter)(option); // } return (statics.getObjectValueFromPropName(option, this.showAttr) || '') as string; } private checkAttributesBeforeInit(): void { let errorString: string = ''; if (statics.isUndefined(this.options)) { // Todo för optionsFn: && statics.isUndefined(this.optionsFn) errorString += 'Missing required parameter: options or optionsFn are required \n'; } if (statics.isUndefined(this.model)) { errorString += 'Missing required parameter: model are requiered'; } if (statics.isUndefined(this.label)) { errorString += 'Missing required parameter: label are requiered'; } if (this.disabled && statics.isUndefined(this.disableReason)) { errorString += 'Missing required parameter: disableReason are requiered when disabled'; } if (statics.isUndefined(this.valueAttr)) { errorString += 'Missing required parameter: valueAttr are requiered'; } if (statics.isUndefined(this.showAttr)) { errorString += 'Missing required parameter: showAttr are requiered'; } if (errorString !== '') { const labelString: string = this.label ? ` with label ${this.label}` : ''; errorString = `Error for fbForm-component multiselect${labelString}: ${errorString}`; throw new Error(errorString); } } private initObservables(): void { this.keyEventSubject = new Subject(); } private onModelUpdate(): void { const emitVal: IUpdateModel = { newVal: this.model, oldVal: undefined }; this.onValueChange.emit(emitVal); this.createPureOptions(); } private setupUiVariables(): void { if (statics.isUndefined(this.options.$promise)) { this.options.$promise = new Promise((resolve) => { resolve(); }) as any; } this.selfElm = this.elementRef.nativeElement; this.hostElm = this.selfElm.parentNode; this.inputElement = this.selfElm.querySelector('input'); } private awaitOptionPromise(): void { if (this.options) { if (this.options.$promise) { this.options.$promise.then(() => { this.createPureOptions(); }); } else { this.createPureOptions(); } } } private showOptionDropdown(): void { this.optionDropdownActive = !this.useExternalOptionFn && !this.disabled && this.pureOptions.length !== 0; } private hideOptionDropdown(): void { if (!this.optionDropdownActive) { return; } this.newItem = ''; setTimeout(() => { this.resizeInput(); }); this.optionDropdownActive = false; this.createPureOptions(); } private createPureOptions(): void { this.pureOptions = _(this.options).filter((option: any) => { const optionSelected: boolean = this.isOptionSelected(option); const searchStringMatch: boolean = this.isSearchStringMatch(option); const showMe: boolean = !optionSelected && searchStringMatch; return showMe; }); this.pureOptions = _.sortBy(this.pureOptions, item => this.getDisplayValue(item)); } private isOptionSelected(option: Object): boolean { const optionKeyValue: string = this.getKeyValue(option); const optionInModel: any = this.model.find((item: any) => { return item === optionKeyValue; }); return statics.isDefined(optionInModel); } private isSearchStringMatch(option: Object): boolean { const displayText: string = this.getDisplayValue(option); const matchSearch: boolean = displayText.toLowerCase().indexOf(this.newItem.toLowerCase()) !== -1; return matchSearch; } private getKeyValue(object: any): any { return statics.getObjectValueFromPropName(object, this.valueAttr); } private resizeInput(): void { const caluclatedWidth: number = this.inputElement.value.length * 9 + 3; this.inputElement.setAttribute('style', `width: ${caluclatedWidth}px`); } private onBackspacePress(): void { if (this.newItem === '') { this.model.pop(); this.onModelUpdate(); this.showOptionDropdown(); } setTimeout(() => { this.createPureOptions(); }); } } // Todo search when having an external search function. // var timeout: ng.IPromise; // $scope.loading = { $resolved: true }; // borde köra på $scope.searching, men funkar inte av nån anledning // $scope.searching = null; // $scope.$watch('newItem', function (newVal: string, oldVal: string) { // if (newVal && newVal !== oldVal && $scope.externalOptionFn) { // $scope.options = []; // if (newVal.length > 2 // && (!$scope.optionsFnConditionToRun || $scope.optionsFnConditionToRun({ input: newVal })) // ) { // $scope.loading.$resolved = false; // if (timeout) { // $timeout.cancel(timeout); // } // timeout = $timeout(function () { // $scope.searching = $scope.optionsFn({ input: newVal }); // $scope.searching.then(function (res) { // _.each(res, function (option) { // $scope.options.push(option); // }); // createPureOptions(); // $scope.loading.$resolved = true; // }); // }, 300); // } // showDroppis(); // } else { // createPureOptions(); // } // }); // Todo update options when options is updated from outside (option added or removed) // $scope.originalOptions = []; // var setOriginalOptions = $scope.$watch('options.length', function (newVal, oldVal) { // if (newVal) { // if ($scope.options && $scope.options.length > 0) { // _.each($scope.options, function (option) { // $scope.originalOptions.push($scope.options); // }); // setOriginalOptions(); // clearar watchern // //createPureOptions(); // } // } // });