// 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;
}
}