// Angular imports // import { Component, OnInit, Input, Output, EventEmitter, ElementRef, Inject, ViewChild, DoCheck } from '@angular/core'; // Components // // Interfaces // // Services // // Directives // // Utilities // import * as statics from '@fb/statics'; import { FbFormBase } from '../fbFormBase'; import { IFbAction } from '@fb/common'; /** * Visar en input för text. * * Syntax: * * * @param model Modell * @param label Label att visa. Visas inte om tight eller noLabel är satt * @param disabled Disabled om satt till true * @param disableReason Används som tooltip vid disabled * @param readonly Readonly om satt till true * @param noLabel Sätt till true för att dölja label * @param placeholder Visas då inget värde är ifyllt * @param autocomplete Om autocomplete ska vara aktiverat (default true) * @param minLength Minsta tillåtna längd * @param maxLength Maximalt tillåtna längd * @param fbType Typ av input. Utelämna för att hantera vanlig text. * @param noSpace Inga mellanrum om fbType är satt * @param allowNegative Negativa tal är tillåtna om fbType är satt * @param suffix Suffix som visas när textrutan inte editeras * @param kontorsValutaSuffix Visar valuta-suffix när textrutan inte editeras * @param kontorsValutaSuffixPerAar Visar valuta/år-suffix när textrutan inte editeras * @param action Action att visa i tooltip * @param tight Visar en mer kompakt input-ruta, utan label * @param icon Kan användas tillsammans med "tight" för att visa en ikon vid fältet * @param transparent Visar fältet som transparent * @param blur Händelse som ska utföras vid "blur" */ // Todo importera styles enligt wiki /vile @Component({ selector: 'fb-form-input', templateUrl: './fb-form-input.component.html', // styles: [require('./fb-form-input.component.less')] }) export class FbFormInputComponent extends FbFormBase implements OnInit, DoCheck { // public instances @Input() placeholder: string; @Input() autocomplete: boolean = true; @Input() minlength: number; @Input() maxlength: number; @Input() fbType: 'number' | 'float' | 'postnr' | 'OCR'; @Input() noSpace: boolean = false; @Input() allowNegative: boolean = false; @Input() decimalLimit: number; @Input() suffix: string; @Input() kontorsValutaSuffix: boolean = false; @Input() kontorsValutaSuffixPerAar: boolean = false; @Input() action: IFbAction = {}; @Input() tight: boolean = false; @Input() icon: string; @Input() transparent: boolean = false; // TODO frha: Readonly används endast på ett ställe (uppdragsinfo mäklarobjekt). Borde denna egenskap verkligen finnas på denna komponent? // Borde inte fb-text användas istället? Ska den vara kvar borde namnet ändras @Input() readonly: boolean = false; @Output() blur: EventEmitter = new EventEmitter(); readonly inputId: string = statics.Guid.new(); formattedModel: string; // private static private static readonly numberType: string = 'number'; private static readonly floatType: string = 'float'; private static readonly postnrType: string = 'postnr'; private static readonly ocrType: string = 'OCR'; // private instances private readonly backspaceKeyCode: number = 8; private readonly enterKeyCode: number = 13; private readonly rightArrowKeyCode: number = 39; private readonly printScreenKeyCode: number = 44; private readonly insertKeyCode: number = 45; private readonly deleteKeyCode: number = 46; @ViewChild('input') private inputEl: ElementRef; constructor( @Inject('CommonService') private readonly commonService: fb.ICommonService ) { super(); } ngOnInit(): void { super.ngOnInit(); if (this.allowNegative && this.fbType !== FbFormInputComponent.numberType && this.fbType !== FbFormInputComponent.floatType) { throw new Error(`allowNegative kan inte användas med ${this.fbType} för ${this.getStringFromModel()}`); } if (this.decimalLimit && this.fbType !== FbFormInputComponent.floatType) { throw new Error(`decimalLimit kan inte användas med ${this.fbType} för ${this.getStringFromModel()}`); } if ((!!this.suffix && this.kontorsValutaSuffix) || (!!this.suffix && this.kontorsValutaSuffixPerAar) || (this.kontorsValutaSuffix && this.kontorsValutaSuffixPerAar)) { throw new Error('Flera olika suffix får inte användas på elementet. Fel för ' + this.getStringFromModel()); } if (this.kontorsValutaSuffix) { this.suffix = this.commonService.getKontorsValutaSuffix(); } else if (this.kontorsValutaSuffixPerAar) { this.suffix = this.commonService.getKontorsValutaSuffix('GLOBALS.PER_AAR'); } this.formattedModel = this.format(this.model.value); if (this.suffix && this.formattedModel.length > 0) { this.formattedModel += ` ${this.suffix}`; } } ngDoCheck(): void { if (this.oldModelValue !== this.model.value) { this.updateModel(this.model.value !== null ? this.model.value.toString() : ''); this.oldModelValue = this.model.value; } } getMinLength(): number { if (this.minlength === 0 || !!this.minlength) { return this.minlength; } else { return this.model.minLength; } } getMaxLength(): number { if (this.maxlength === 0 || !!this.maxlength) { return this.maxlength; } else { return this.model.maxLength; } } private isNotDigit(key: number): boolean { const minCode: number = 48; const maxCode: number = 57; return key < minCode || key > maxCode; } private isNotControlKey(key: number): boolean { const controlKeys: number[] = [ 0, this.backspaceKeyCode, this.enterKeyCode, this.rightArrowKeyCode, this.printScreenKeyCode, this.deleteKeyCode ]; return controlKeys.indexOf(key) === -1; } private thereIsAComma(val: string): boolean { return val.indexOf(',') !== -1; } private isNotFirstComma(key: number, val: string): boolean { return (key === this.printScreenKeyCode || key === this.deleteKeyCode) && this.thereIsAComma(val); } private isNotInitialDash(key: number, start: number): boolean { return key !== this.insertKeyCode || start !== 0; } private decimalLimitReached(val: string): boolean { if (this.decimalLimit === undefined || !this.thereIsAComma(val)) { return false; } return val.split(',')[1].length >= this.decimalLimit; } onFocus(): void { if (this.suffix) { this.formattedModel = this.formattedModel.replace( ' ' + this.suffix, ''); } } onBlur(e: FocusEvent): void { // Då vi inte uppdaterar formattedModel i updateModel så verkar vi behöva göra när man lämnar fältet, annars får vi halvformaterat data som vi lägger på suffix på. this.formattedModel = this.format(this.model.value); if (this.suffix && this.formattedModel.length > 0) { this.formattedModel += ' ' + this.suffix; } this.blur.emit(e); } onKeypress(e: KeyboardEvent): void { const floatAble: boolean = this.fbType === FbFormInputComponent.floatType; if ((this.fbType && this.isNotDigit(e.which) && ((e.which === this.printScreenKeyCode && !floatAble) || (e.which === this.deleteKeyCode && !floatAble) || this.isNotControlKey(e.which) || this.isNotFirstComma(e.which, this.formattedModel)) && (!this.allowNegative || this.isNotInitialDash(e.which, this.inputEl.nativeElement.selectionStart))) || this.decimalLimitReached(this.formattedModel)) { event.preventDefault(); } } updateModel(data: string): void { const unformatted: string = this.unformat(data); super.updateModel(this.castToModel(unformatted)); const startBefore: any = this.inputEl.nativeElement.selectionStart; const lenBefore: number = this.formattedModel.length; // Vi uppdaterar inte formatted model direkt. Anledningen är att vi // vill se till att markören inte flyttar sig. // Uppdaterar vi modellen så kan markören flytta sig till slutet (t.ex. // när man lägger till space), vilket är svårt att hantera då renderingen // inte sker direkt. const newValue: string = this.format(unformatted); this.inputEl.nativeElement.value = newValue; this.inputEl.nativeElement.selectionStart = startBefore + (newValue.length - lenBefore); this.inputEl.nativeElement.selectionEnd = startBefore + (newValue.length - lenBefore); } private format(value: T): string { if (value === null || value === undefined) { return ''; } else { switch (this.fbType) { case FbFormInputComponent.numberType: case FbFormInputComponent.ocrType: return this.formatNumber(value.toString()); case FbFormInputComponent.floatType: return this.formatFloat(value.toString()); case FbFormInputComponent.postnrType: return this.formatPostnr(value.toString()); default: return value.toString(); } } } private formatNumber(value: string): string { return !this.noSpace ? this.addSpacesToInteger(value) : value; } private formatFloat(value: string): string { return this.formatNumber(value) .replace(/\./, ','); } private formatPostnr(value: string): string { return !this.noSpace ? this.addSpacesToPostnr(value) : value; } private unformat(value: string): string { if (!this.fbType) { // Hantera som vanlig sträng return value; } let result: string = value; result = result.replace(/ /g, ''); if (this.fbType === FbFormInputComponent.floatType) { result = this.clearAllCommasExceptFirst(result); result = result.replace(/,/, '.'); } else { result = result.replace(/\./g, ''); } if (this.allowNegative) { result = this.clearAllDashExceptInitial(result); } const regex: RegExp = this.allowNegative ? /[^0-9.-]/gi : /[^0-9.]/gi; result = result.replace(regex, ''); return result; } private castToModel(value: string): any { const model: string = this.model.value; if (typeof model === 'boolean') { return !!value; } else if (typeof model === 'number' || (this.fbType && this.fbType !== FbFormInputComponent.postnrType)) { const numericVal: number = parseFloat(value); return (isNaN(numericVal)) ? null : numericVal; } else if (value === '') { return null; } else { return value; } } private addSpacesToInteger(value: string): string { const decimalPos: number = value.indexOf('.'); const decimals: string = decimalPos === -1 ? '' : value.substr(decimalPos, value.length); const wholeNumbers: string = decimalPos === -1 ? value : value.substr(0, decimalPos); const spaces: string = wholeNumbers.replace(/(\d)(?=(\d{3})+(?!\d))/g, '$1 '); return '' + spaces + decimals; } private addSpacesToPostnr (value: string): string { const minimumLength: number = 2; if (value.length > minimumLength) { return value.substr(0, value.length - minimumLength) + ' ' + value.substr(value.length - minimumLength, value.length); } return value; } private clearAllCommasExceptFirst (value: string): string { const indexOfComma: number = value.indexOf(','); if (indexOfComma > -1) { value = value.replace(/,/g, ''); value = value.slice(0, indexOfComma) + ',' + value.slice(indexOfComma); } return value; } private clearAllDashExceptInitial (value: string): string { if (value[0] === '-') { return '-' + value.replace(/-/g, ''); } else { return value.replace(/-/g, ''); } } getParentElement(): HTMLElement { const div: any = this.inputEl.nativeElement.parentElement; const component: any = div.parentElement; return component; } }