From 34528949c004136a8cde1de69d01c3054d59b278 Mon Sep 17 00:00:00 2001 From: christophercr Date: Wed, 13 Feb 2019 15:12:14 +0100 Subject: [PATCH] feat(stark-ui): implement directives for email, number and timestamp masks ISSUES CLOSED: #681, #682, #683 BREAKING CHANGE: new `typings` folder in the package containing typings for some libraries used by Stark-UI components/directives. This must be included in the `typeRoots` of your app `tsconfig.json`: `"typeRoots": ["./node_modules/@nationalbankbelgium/stark-ui/typings", ...]` --- packages/rollup.config.common-data.js | 1 + .../input-mask-directives/directives.ts | 6 + .../directives/email-mask.directive.spec.ts | 357 +++++++++++++++ .../directives/email-mask.directive.ts | 107 +++++ .../directives/number-mask-config.intf.ts | 62 +++ .../directives/number-mask.directive.spec.ts | 379 ++++++++++++++++ .../directives/number-mask.directive.ts | 127 ++++++ .../directives/text-mask-config.intf.ts | 4 +- .../directives/text-mask.directive.ts | 26 +- .../directives/timestamp-mask-config.intf.ts | 9 + .../timestamp-mask.directive.spec.ts | 413 ++++++++++++++++++ .../directives/timestamp-mask.directive.ts | 147 +++++++ .../directives/timestamp-pipe.fn.spec.ts | 122 ++++++ .../directives/timestamp-pipe.fn.ts | 88 ++++ .../input-mask-directives.module.ts | 6 +- showcase/src/app/demo-ui/demo-ui.module.ts | 6 +- ...-input-mask-directives-page.component.html | 171 +++++++- ...mo-input-mask-directives-page.component.ts | 103 ++++- .../email-mask-directive.html | 14 + .../email-mask-directive.ts | 17 + .../number-mask-directive.html | 38 ++ .../number-mask-directive.ts | 78 ++++ .../text-mask-directive.html | 12 +- .../text-mask-directive.ts | 14 +- .../timestamp-mask-directive.html | 75 ++++ .../timestamp-mask-directive.ts | 58 +++ showcase/src/assets/translations/en.json | 32 +- showcase/src/assets/translations/fr.json | 28 +- showcase/src/assets/translations/nl.json | 28 +- 29 files changed, 2475 insertions(+), 53 deletions(-) create mode 100644 packages/stark-ui/src/modules/input-mask-directives/directives/email-mask.directive.spec.ts create mode 100644 packages/stark-ui/src/modules/input-mask-directives/directives/email-mask.directive.ts create mode 100644 packages/stark-ui/src/modules/input-mask-directives/directives/number-mask-config.intf.ts create mode 100644 packages/stark-ui/src/modules/input-mask-directives/directives/number-mask.directive.spec.ts create mode 100644 packages/stark-ui/src/modules/input-mask-directives/directives/number-mask.directive.ts create mode 100644 packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-mask-config.intf.ts create mode 100644 packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-mask.directive.spec.ts create mode 100644 packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-mask.directive.ts create mode 100644 packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-pipe.fn.spec.ts create mode 100644 packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-pipe.fn.ts create mode 100644 showcase/src/assets/examples/input-mask-directives/email-mask-directive.html create mode 100644 showcase/src/assets/examples/input-mask-directives/email-mask-directive.ts create mode 100644 showcase/src/assets/examples/input-mask-directives/number-mask-directive.html create mode 100644 showcase/src/assets/examples/input-mask-directives/number-mask-directive.ts create mode 100644 showcase/src/assets/examples/input-mask-directives/timestamp-mask-directive.html create mode 100644 showcase/src/assets/examples/input-mask-directives/timestamp-mask-directive.ts diff --git a/packages/rollup.config.common-data.js b/packages/rollup.config.common-data.js index 15ac38fd79..b6ab47c007 100644 --- a/packages/rollup.config.common-data.js +++ b/packages/rollup.config.common-data.js @@ -69,6 +69,7 @@ const globals = { "prismjs/components/prism-css-extras.min.js": "Prism.languages.css.selector", "prismjs/components/prism-scss.min.js": "Prism.languages.scss", "text-mask-core": "textMaskCore", + "text-mask-addons": "textMaskAddons", uuid: "uuid", rxjs: "rxjs", diff --git a/packages/stark-ui/src/modules/input-mask-directives/directives.ts b/packages/stark-ui/src/modules/input-mask-directives/directives.ts index d6e7741585..05fb37aa05 100644 --- a/packages/stark-ui/src/modules/input-mask-directives/directives.ts +++ b/packages/stark-ui/src/modules/input-mask-directives/directives.ts @@ -1,3 +1,9 @@ +export * from "./directives/email-mask.directive"; +export * from "./directives/number-mask-config.intf"; +export * from "./directives/number-mask.directive"; export * from "./directives/text-mask.constants"; export * from "./directives/text-mask.directive"; export * from "./directives/text-mask-config.intf"; +export * from "./directives/timestamp-mask-config.intf"; +export * from "./directives/timestamp-mask.directive"; +export * from "./directives/timestamp-pipe.fn"; diff --git a/packages/stark-ui/src/modules/input-mask-directives/directives/email-mask.directive.spec.ts b/packages/stark-ui/src/modules/input-mask-directives/directives/email-mask.directive.spec.ts new file mode 100644 index 0000000000..4de294b131 --- /dev/null +++ b/packages/stark-ui/src/modules/input-mask-directives/directives/email-mask.directive.spec.ts @@ -0,0 +1,357 @@ +/* tslint:disable:completed-docs no-duplicate-string no-identical-functions no-big-function */ +import { Component, DebugElement } from "@angular/core"; +import { FormControl, FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { By } from "@angular/platform-browser"; +import { ComponentFixture, fakeAsync, TestBed } from "@angular/core/testing"; +import { Observer } from "rxjs"; +import { StarkEmailMaskDirective } from "./email-mask.directive"; + +describe("EmailMaskDirective", () => { + let fixture: ComponentFixture; + let hostComponent: TestComponent; + let inputElement: DebugElement; + + @Component({ + selector: "test-component", + template: getTemplate("[starkEmailMask]='emailMaskConfig'") + }) + class TestComponent { + public emailMaskConfig: boolean; + public ngModelValue: string = ""; + public formControl: FormControl = new FormControl(""); + } + + function getTemplate(emailMaskDirective: string): string { + return ""; + } + + function initializeComponentFixture(): void { + fixture = TestBed.createComponent(TestComponent); + hostComponent = fixture.componentInstance; + inputElement = fixture.debugElement.query(By.css("input")); + // trigger initial data binding + fixture.detectChanges(); + } + + function changeInputValue(inputDebugElement: DebugElement, value: string, eventType: string = "input"): void { + (inputDebugElement.nativeElement).value = value; + + // more verbose way to create and trigger an event (the only way it works in IE) + // https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events + const ev: Event = document.createEvent("Event"); + ev.initEvent(eventType, true, true); + (inputDebugElement.nativeElement).dispatchEvent(ev); + } + + // Inject module dependencies + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [StarkEmailMaskDirective, TestComponent], + imports: [FormsModule, ReactiveFormsModule], + providers: [] + }); + }); + + describe("uncontrolled", () => { + beforeEach(fakeAsync(() => { + // compile template and css + return TestBed.compileComponents(); + })); + + beforeEach(() => { + initializeComponentFixture(); + }); + + it("should render the appropriate content", () => { + expect(inputElement.attributes["ng-reflect-mask-config"]).toBeDefined(); // starkEmailMask directive + }); + + it("should update the input value and show the mask only when a valid event is triggered in the input field", () => { + // Angular2 text-mask directive handles only the "input" event + const validEvents: string[] = ["input"]; + + for (const eventType of validEvents) { + changeInputValue(inputElement, ""); + fixture.detectChanges(); + expect(inputElement.nativeElement.value).toBe(""); + + changeInputValue(inputElement, "my-email@", eventType); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("my-email@ ."); + } + + const invalidEvents: string[] = ["blur", "keyup", "change", "focus", "keydown", "keypress", "click"]; + + for (const eventType of invalidEvents) { + changeInputValue(inputElement, ""); + fixture.detectChanges(); + expect(inputElement.nativeElement.value).toBe(""); + + changeInputValue(inputElement, "my-email", eventType); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("my-email"); // no mask shown + } + }); + + it("should prevent invalid values to be entered in the input field when the value is changed manually", () => { + const invalidValues: string[] = ["@@", "@.a.", " @ .", "what@.ever@."]; + + for (const value of invalidValues) { + changeInputValue(inputElement, value); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe(""); + } + }); + + it("should remove the mask only when the config is set to false", () => { + changeInputValue(inputElement, "my-email@"); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("my-email@ ."); + + hostComponent.emailMaskConfig = undefined; + fixture.detectChanges(); + + changeInputValue(inputElement, "what@.ever@."); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("my-email@ ."); // the mask is enabled by default + + hostComponent.emailMaskConfig = ""; // use case when the directive is used with no inputs: + fixture.detectChanges(); + + changeInputValue(inputElement, "what@.ever@."); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("my-email@ ."); // the mask is enabled by default + + hostComponent.emailMaskConfig = false; + fixture.detectChanges(); + + changeInputValue(inputElement, "what@@.ever@."); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("what@@.ever@."); // no mask at all + }); + }); + + describe("with ngModel", () => { + beforeEach(fakeAsync(() => { + const newTemplate: string = getTemplate("[(ngModel)]='ngModelValue' [starkEmailMask]='emailMaskConfig'"); + + TestBed.overrideTemplate(TestComponent, newTemplate); + + // compile template and css + return TestBed.compileComponents(); + })); + + beforeEach(() => { + initializeComponentFixture(); + }); + + it("should render the appropriate content", () => { + expect(inputElement.attributes["ng-reflect-mask-config"]).toBeDefined(); // starkEmailMask directive + }); + + it("should update the input value and show the mask only when a valid event is triggered in the input field", () => { + // Angular2 text-mask directive handles only the "input" event + const validEvents: string[] = ["input"]; + + for (const eventType of validEvents) { + changeInputValue(inputElement, ""); + fixture.detectChanges(); + expect(hostComponent.ngModelValue).toBe(""); + + changeInputValue(inputElement, "my-email@", eventType); + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe("my-email@ ."); + } + + const invalidEvents: string[] = ["blur", "keyup", "change", "focus", "keydown", "keypress", "click"]; + + for (const eventType of invalidEvents) { + changeInputValue(inputElement, ""); + fixture.detectChanges(); + expect(hostComponent.ngModelValue).toBe(""); + + changeInputValue(inputElement, "my-email@", eventType); + fixture.detectChanges(); + + // IMPORTANT: the ngModel is not changed with invalid events, just with "input" events + expect(hostComponent.ngModelValue).toBe(""); // no mask shown + } + }); + + it("should prevent invalid values to be entered in the input field when the value is changed manually", () => { + const invalidValues: string[] = ["@@", "@.a.", " @ .", "what@.ever@."]; + + for (const value of invalidValues) { + changeInputValue(inputElement, value); + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe(""); + } + }); + + it("should remove the mask only when the config is set to false", () => { + changeInputValue(inputElement, "my-email@"); + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe("my-email@ ."); + + hostComponent.emailMaskConfig = undefined; + fixture.detectChanges(); + + changeInputValue(inputElement, "what@.ever@."); + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe("my-email@ ."); // the mask is enabled by default + + hostComponent.emailMaskConfig = ""; // use case when the directive is used with no inputs: + fixture.detectChanges(); + + changeInputValue(inputElement, "what@.ever@."); + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe("my-email@ ."); // the mask is enabled by default + + hostComponent.emailMaskConfig = false; + fixture.detectChanges(); + + changeInputValue(inputElement, "what@@.ever@."); + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe("what@@.ever@."); // no mask at all + }); + }); + + describe("with FormControl", () => { + let mockValueChangeObserver: jasmine.SpyObj>; + + beforeEach(fakeAsync(() => { + const newTemplate: string = getTemplate("[formControl]='formControl' [starkEmailMask]='emailMaskConfig'"); + + TestBed.overrideTemplate(TestComponent, newTemplate); + + // compile template and css + return TestBed.compileComponents(); + })); + + beforeEach(() => { + initializeComponentFixture(); + + mockValueChangeObserver = jasmine.createSpyObj>("observerSpy", ["next", "error", "complete"]); + hostComponent.formControl.valueChanges.subscribe(mockValueChangeObserver); + }); + + it("should render the appropriate content", () => { + expect(inputElement.attributes["ng-reflect-mask-config"]).toBeDefined(); // starkEmailMask directive + }); + + it("should update the input value and show the mask only when a valid event is triggered in the input field", () => { + // Angular2 text-mask directive handles only the "input" event + const validEvents: string[] = ["input"]; + + for (const eventType of validEvents) { + changeInputValue(inputElement, ""); + fixture.detectChanges(); + expect(hostComponent.formControl.value).toBe(""); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + + mockValueChangeObserver.next.calls.reset(); + changeInputValue(inputElement, "my-email@", eventType); + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe("my-email@ ."); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + expect(mockValueChangeObserver.error).not.toHaveBeenCalled(); + expect(mockValueChangeObserver.complete).not.toHaveBeenCalled(); + } + + mockValueChangeObserver.next.calls.reset(); + const invalidEvents: string[] = ["blur", "keyup", "change", "focus", "keydown", "keypress", "click"]; + + for (const eventType of invalidEvents) { + changeInputValue(inputElement, ""); + fixture.detectChanges(); + expect(hostComponent.formControl.value).toBe(""); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + + mockValueChangeObserver.next.calls.reset(); + changeInputValue(inputElement, "my-email@", eventType); + fixture.detectChanges(); + + // IMPORTANT: the formControl is not changed with invalid events, just with "input" events + expect(hostComponent.formControl.value).toBe(""); // no mask shown + expect(mockValueChangeObserver.next).not.toHaveBeenCalled(); + expect(mockValueChangeObserver.error).not.toHaveBeenCalled(); + expect(mockValueChangeObserver.complete).not.toHaveBeenCalled(); + } + }); + + it("should prevent invalid values to be entered in the input field when the value is changed manually", () => { + const invalidValues: string[] = ["@@", "@.a.", " @ .", "what@.ever@."]; + + for (const value of invalidValues) { + mockValueChangeObserver.next.calls.reset(); + changeInputValue(inputElement, value); + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe(""); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + expect(mockValueChangeObserver.error).not.toHaveBeenCalled(); + expect(mockValueChangeObserver.complete).not.toHaveBeenCalled(); + } + }); + + it("should remove the mask when the config is undefined", () => { + changeInputValue(inputElement, "my-email@"); + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe("my-email@ ."); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + + mockValueChangeObserver.next.calls.reset(); + hostComponent.emailMaskConfig = undefined; + fixture.detectChanges(); + expect(mockValueChangeObserver.next).not.toHaveBeenCalled(); // no value change, the mask is enabled by default + + mockValueChangeObserver.next.calls.reset(); + changeInputValue(inputElement, "what@.ever@."); + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe("my-email@ ."); // the mask is enabled by default + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + + mockValueChangeObserver.next.calls.reset(); + hostComponent.emailMaskConfig = ""; // use case when the directive is used with no inputs: + fixture.detectChanges(); + expect(mockValueChangeObserver.next).not.toHaveBeenCalled(); // no value change, the mask is enabled by default + + mockValueChangeObserver.next.calls.reset(); + changeInputValue(inputElement, "what@.ever@."); + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe("my-email@ ."); // the mask is enabled by default + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + + mockValueChangeObserver.next.calls.reset(); + hostComponent.emailMaskConfig = false; + fixture.detectChanges(); + expect(mockValueChangeObserver.next).not.toHaveBeenCalled(); // no value change, the mask was just disabled + + mockValueChangeObserver.next.calls.reset(); + changeInputValue(inputElement, "what@@.ever@."); + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe("what@@.ever@."); // no mask at all + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + expect(mockValueChangeObserver.error).not.toHaveBeenCalled(); + expect(mockValueChangeObserver.complete).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/stark-ui/src/modules/input-mask-directives/directives/email-mask.directive.ts b/packages/stark-ui/src/modules/input-mask-directives/directives/email-mask.directive.ts new file mode 100644 index 0000000000..2973fedb1d --- /dev/null +++ b/packages/stark-ui/src/modules/input-mask-directives/directives/email-mask.directive.ts @@ -0,0 +1,107 @@ +import { Directive, ElementRef, forwardRef, Inject, Input, OnChanges, Optional, Provider, Renderer2, SimpleChanges } from "@angular/core"; +import { COMPOSITION_BUFFER_MODE, NG_VALUE_ACCESSOR } from "@angular/forms"; +import { CombinedPipeMask } from "text-mask-core"; +import { emailMask } from "text-mask-addons"; +import { MaskedInputDirective, TextMaskConfig as Ng2TextMaskConfig } from "angular2-text-mask"; + +/** + * Name of the directive + */ +const directiveName: string = "[starkEmailMask]"; + +/** + * @ignore + */ +export const STARK_EMAIL_MASK_VALUE_ACCESSOR: Provider = { + provide: NG_VALUE_ACCESSOR, + // tslint:disable-next-line:no-forward-ref + useExisting: forwardRef(() => StarkEmailMaskDirective), + multi: true +}; + +/** + * Directive to display an email mask in input elements. This directive internally uses the {@link https://github.com/text-mask/text-mask/tree/master/core|text-mask-core} library + * to provide the input mask functionality. + * + * **`IMPORTANT:`** Currently the Email Mask supports only input of type text, tel, url, password, and search. + * Due to a limitation in browser API, other input types, such as email or number, cannot be supported. + * + * ### Disabling the mask + * Passing `false` to the directive will disable the mask: `` + * + * @example + * + * + * + * + * + * + * + * + * + * + * + * + */ +@Directive({ + host: { + "(input)": "_handleInput($event.target.value)", + "(blur)": "onTouched()", + "(compositionstart)": "_compositionStart()", + "(compositionend)": "_compositionEnd($event.target.value)" + }, + selector: directiveName, + exportAs: "starkEmailMask", + providers: [STARK_EMAIL_MASK_VALUE_ACCESSOR] +}) +export class StarkEmailMaskDirective extends MaskedInputDirective implements OnChanges { + /** + * Whether to display the email mask in the input field. + */ + /* tslint:disable:no-input-rename */ + @Input("starkEmailMask") + public maskConfig?: boolean = true; // enabled by default + + /** + * Class constructor + * @param _renderer - Angular Renderer wrapper for DOM manipulations. + * @param _elementRef - Reference to the DOM element where this directive is applied to. + * @param _compositionMode - Injected token to control if form directives buffer IME input until the "compositionend" event occurs. + */ + public constructor( + _renderer: Renderer2, + _elementRef: ElementRef, + @Optional() @Inject(COMPOSITION_BUFFER_MODE) _compositionMode: boolean + ) { + super(_renderer, _elementRef, _compositionMode); + } + + /** + * Component lifecycle hook + */ + public ngOnChanges(changes: SimpleChanges): void { + this.textMaskConfig = this.normalizeMaskConfig(this.maskConfig); + + super.ngOnChanges(changes); + } + + /** + * Create a valid configuration to be passed to the MaskedInputDirective + * @param maskConfig - The provided configuration via the directive's input + */ + public normalizeMaskConfig(maskConfig: boolean = true): Ng2TextMaskConfig { + // in case the directive is used without inputs: "" the maskConfig becomes an empty string '' + // therefore "undefined" or string values will also enable the mask + maskConfig = typeof maskConfig !== "boolean" ? true : maskConfig; + + if (!maskConfig) { + return { mask: false }; // remove the mask + } else { + // TODO: Ng2TextMaskConfig is not the same as Core TextMaskConfig + // even though emailMask is passed as a mask, it is actually made of both a mask and a pipe bundled together for convenience + // https://github.com/text-mask/text-mask/tree/master/addons + const { mask, pipe }: CombinedPipeMask = emailMask; + return { mask: mask, pipe: pipe }; + } + } +} diff --git a/packages/stark-ui/src/modules/input-mask-directives/directives/number-mask-config.intf.ts b/packages/stark-ui/src/modules/input-mask-directives/directives/number-mask-config.intf.ts new file mode 100644 index 0000000000..5778de2f9d --- /dev/null +++ b/packages/stark-ui/src/modules/input-mask-directives/directives/number-mask-config.intf.ts @@ -0,0 +1,62 @@ +/** + * Defines the configuration for the {@link StarkNumberMaskDirective}. + * + * Based on the API of the `createNumberMask` function from the {@link https://github.com/text-mask/text-mask/tree/master/addons|text-mask-addons} library + * See {@link https://github.com/text-mask/text-mask/tree/master/addons#createnumbermask} + */ +export interface StarkNumberMaskConfig { + /** + * String to be displayed before the amount. Default: empty string ("") + */ + prefix?: string; + + /** + * String to be displayed after the amount. Default: empty string ("") + */ + suffix?: string; + + /** + * Whether or not to separate thousands. Default: true + */ + includeThousandsSeparator?: boolean; + + /** + * Character to be used as thousands separator. Default: "," + */ + thousandsSeparatorSymbol?: string; + + /** + * Whether or not to allow the user to enter a fraction with the amount. Default: false + */ + allowDecimal?: boolean; + + /** + * Character to be used as decimal point. Default: "." + */ + decimalSymbol?: string; + + /** + * Number of digits to allow in the decimal part of the number. Default: 2 + */ + decimalLimit?: number; + + /** + * Limit the length of the integer number. Default: undefined (unlimited) + */ + integerLimit?: number; + + /** + * Whether or not to always include a decimal point and placeholder for decimal digits after the integer. Default: false + */ + requireDecimal?: boolean; + + /** + * Whether or not to allow negative numbers. Default: true + */ + allowNegative?: boolean; + + /** + * Whether or not to allow leading zeroes. Default: false + */ + allowLeadingZeroes?: boolean; +} diff --git a/packages/stark-ui/src/modules/input-mask-directives/directives/number-mask.directive.spec.ts b/packages/stark-ui/src/modules/input-mask-directives/directives/number-mask.directive.spec.ts new file mode 100644 index 0000000000..6df34a7e5e --- /dev/null +++ b/packages/stark-ui/src/modules/input-mask-directives/directives/number-mask.directive.spec.ts @@ -0,0 +1,379 @@ +/* tslint:disable:completed-docs no-duplicate-string no-identical-functions no-big-function */ +import { Component, DebugElement } from "@angular/core"; +import { FormControl, FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { By } from "@angular/platform-browser"; +import { ComponentFixture, fakeAsync, TestBed } from "@angular/core/testing"; +import { Observer } from "rxjs"; +import { StarkNumberMaskDirective } from "./number-mask.directive"; +import { StarkNumberMaskConfig } from "./number-mask-config.intf"; + +describe("NumberMaskDirective", () => { + let fixture: ComponentFixture; + let hostComponent: TestComponent; + let inputElement: DebugElement; + + const numberMaskConfig: StarkNumberMaskConfig = { + prefix: "", + suffix: "" + }; + + @Component({ + selector: "test-component", + template: getTemplate("[starkNumberMask]='numberMaskConfig'") + }) + class TestComponent { + public numberMaskConfig: StarkNumberMaskConfig = numberMaskConfig; + public ngModelValue: string = ""; + public formControl: FormControl = new FormControl(""); + } + + function getTemplate(numberMaskDirective: string): string { + return ""; + } + + function initializeComponentFixture(): void { + fixture = TestBed.createComponent(TestComponent); + hostComponent = fixture.componentInstance; + inputElement = fixture.debugElement.query(By.css("input")); + // trigger initial data binding + fixture.detectChanges(); + } + + function changeInputValue(inputDebugElement: DebugElement, value: string, eventType: string = "input"): void { + (inputDebugElement.nativeElement).value = value; + + // more verbose way to create and trigger an event (the only way it works in IE) + // https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events + const ev: Event = document.createEvent("Event"); + ev.initEvent(eventType, true, true); + (inputDebugElement.nativeElement).dispatchEvent(ev); + } + + // Inject module dependencies + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [StarkNumberMaskDirective, TestComponent], + imports: [FormsModule, ReactiveFormsModule], + providers: [] + }); + }); + + describe("uncontrolled", () => { + beforeEach(fakeAsync(() => { + // compile template and css + return TestBed.compileComponents(); + })); + + beforeEach(() => { + initializeComponentFixture(); + }); + + it("should render the appropriate content", () => { + expect(inputElement.attributes["ng-reflect-mask-config"]).toBeDefined(); // starkNumberMask directive + }); + + it("should update the input value and show the mask only when a valid event is triggered in the input field", () => { + // Angular2 text-mask directive handles only the "input" event + const validEvents: string[] = ["input"]; + + for (const eventType of validEvents) { + changeInputValue(inputElement, ""); + fixture.detectChanges(); + expect(inputElement.nativeElement.value).toBe(""); + + changeInputValue(inputElement, "12345", eventType); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("12,345"); + } + + const invalidEvents: string[] = ["blur", "keyup", "change", "focus", "keydown", "keypress", "click"]; + + for (const eventType of invalidEvents) { + changeInputValue(inputElement, ""); + fixture.detectChanges(); + expect(inputElement.nativeElement.value).toBe(""); + + changeInputValue(inputElement, "12345", eventType); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("12345"); // no mask shown + } + }); + + it("should prevent invalid values to be entered in the input field when the value is changed manually", () => { + const invalidValues: string[] = ["a", " ", "/*-+,."]; + + for (const value of invalidValues) { + changeInputValue(inputElement, value); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe(""); + } + + const invalidNumericValues: string[] = ["1-2-3.4.5", "+1*23-4/5", ".1.234,5"]; + + for (const numericValue of invalidNumericValues) { + changeInputValue(inputElement, numericValue); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("12,345"); + } + }); + + it("should refresh the mask whenever the configuration changes", () => { + changeInputValue(inputElement, "12345"); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("12,345"); + + hostComponent.numberMaskConfig = { ...numberMaskConfig, prefix: "%", suffix: " percent", thousandsSeparatorSymbol: "-" }; + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("%12-345 percent"); + }); + + it("should remove the mask when the config is undefined", () => { + changeInputValue(inputElement, "12345"); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("12,345"); + + hostComponent.numberMaskConfig = undefined; + fixture.detectChanges(); + + changeInputValue(inputElement, "whatever+1*23-4/5"); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("whatever+1*23-4/5"); // no mask at all + }); + }); + + describe("with ngModel", () => { + beforeEach(fakeAsync(() => { + const newTemplate: string = getTemplate("[(ngModel)]='ngModelValue' [starkNumberMask]='numberMaskConfig'"); + + TestBed.overrideTemplate(TestComponent, newTemplate); + + // compile template and css + return TestBed.compileComponents(); + })); + + beforeEach(() => { + initializeComponentFixture(); + }); + + it("should render the appropriate content", () => { + expect(inputElement.attributes["ng-reflect-mask-config"]).toBeDefined(); // starkNumberMask directive + }); + + it("should update the input value and show the mask only when a valid event is triggered in the input field", () => { + // Angular2 text-mask directive handles only the "input" event + const validEvents: string[] = ["input"]; + + for (const eventType of validEvents) { + changeInputValue(inputElement, ""); + fixture.detectChanges(); + expect(hostComponent.ngModelValue).toBe(""); + + changeInputValue(inputElement, "12345", eventType); + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe("12,345"); + } + + const invalidEvents: string[] = ["blur", "keyup", "change", "focus", "keydown", "keypress", "click"]; + + for (const eventType of invalidEvents) { + changeInputValue(inputElement, ""); + fixture.detectChanges(); + expect(hostComponent.ngModelValue).toBe(""); + + changeInputValue(inputElement, "12345", eventType); + fixture.detectChanges(); + + // IMPORTANT: the ngModel is not changed with invalid events, just with "input" events + expect(hostComponent.ngModelValue).toBe(""); // no mask shown + } + }); + + it("should prevent invalid values to be entered in the input field when the value is changed manually", () => { + const invalidValues: string[] = ["a", " ", "/*-+,."]; + + for (const value of invalidValues) { + changeInputValue(inputElement, value); + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe(""); + } + + const invalidNumericValues: string[] = ["1-2-3.4.5", "+1*23-4/5", ".1.234,5"]; + + for (const numericValue of invalidNumericValues) { + changeInputValue(inputElement, numericValue); + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe("12,345"); + } + }); + + it("should refresh the mask whenever the configuration changes", () => { + changeInputValue(inputElement, "12345"); + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe("12,345"); + + hostComponent.numberMaskConfig = { ...numberMaskConfig, prefix: "%", suffix: " percent", thousandsSeparatorSymbol: "-" }; + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe("%12-345 percent"); + }); + + it("should remove the mask when the config is undefined", () => { + changeInputValue(inputElement, "12345"); + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe("12,345"); + + hostComponent.numberMaskConfig = undefined; + fixture.detectChanges(); + + changeInputValue(inputElement, "whatever+1*23-4/5"); + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe("whatever+1*23-4/5"); // no mask at all + }); + }); + + describe("with FormControl", () => { + let mockValueChangeObserver: jasmine.SpyObj>; + + beforeEach(fakeAsync(() => { + const newTemplate: string = getTemplate("[formControl]='formControl' [starkNumberMask]='numberMaskConfig'"); + + TestBed.overrideTemplate(TestComponent, newTemplate); + + // compile template and css + return TestBed.compileComponents(); + })); + + beforeEach(() => { + initializeComponentFixture(); + + mockValueChangeObserver = jasmine.createSpyObj>("observerSpy", ["next", "error", "complete"]); + hostComponent.formControl.valueChanges.subscribe(mockValueChangeObserver); + }); + + it("should render the appropriate content", () => { + expect(inputElement.attributes["ng-reflect-mask-config"]).toBeDefined(); // starkNumberMask directive + }); + + it("should update the input value and show the mask only when a valid event is triggered in the input field", () => { + // Angular2 text-mask directive handles only the "input" event + const validEvents: string[] = ["input"]; + + for (const eventType of validEvents) { + changeInputValue(inputElement, ""); + fixture.detectChanges(); + expect(hostComponent.formControl.value).toBe(""); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + + mockValueChangeObserver.next.calls.reset(); + changeInputValue(inputElement, "12345", eventType); + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe("12,345"); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + expect(mockValueChangeObserver.error).not.toHaveBeenCalled(); + expect(mockValueChangeObserver.complete).not.toHaveBeenCalled(); + } + + mockValueChangeObserver.next.calls.reset(); + const invalidEvents: string[] = ["blur", "keyup", "change", "focus", "keydown", "keypress", "click"]; + + for (const eventType of invalidEvents) { + changeInputValue(inputElement, ""); + fixture.detectChanges(); + expect(hostComponent.formControl.value).toBe(""); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + + mockValueChangeObserver.next.calls.reset(); + changeInputValue(inputElement, "12345", eventType); + fixture.detectChanges(); + + // IMPORTANT: the formControl is not changed with invalid events, just with "input" events + expect(hostComponent.formControl.value).toBe(""); // no mask shown + expect(mockValueChangeObserver.next).not.toHaveBeenCalled(); + expect(mockValueChangeObserver.error).not.toHaveBeenCalled(); + expect(mockValueChangeObserver.complete).not.toHaveBeenCalled(); + } + }); + + it("should prevent invalid values to be entered in the input field when the value is changed manually", () => { + const invalidValues: string[] = ["a", " ", "/*-+,."]; + + for (const value of invalidValues) { + mockValueChangeObserver.next.calls.reset(); + changeInputValue(inputElement, value); + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe(""); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + expect(mockValueChangeObserver.error).not.toHaveBeenCalled(); + expect(mockValueChangeObserver.complete).not.toHaveBeenCalled(); + } + + const invalidNumericValues: string[] = ["1-2-3.4.5", "+1*23-4/5", ".1.234,5"]; + + for (const numericValue of invalidNumericValues) { + mockValueChangeObserver.next.calls.reset(); + changeInputValue(inputElement, numericValue); + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe("12,345"); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + expect(mockValueChangeObserver.error).not.toHaveBeenCalled(); + expect(mockValueChangeObserver.complete).not.toHaveBeenCalled(); + } + }); + + it("should refresh the mask whenever the configuration changes", () => { + changeInputValue(inputElement, "12345"); + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe("12,345"); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + + mockValueChangeObserver.next.calls.reset(); + hostComponent.numberMaskConfig = { ...numberMaskConfig, prefix: "%", suffix: " percent", thousandsSeparatorSymbol: "-" }; + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe("%12-345 percent"); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + expect(mockValueChangeObserver.error).not.toHaveBeenCalled(); + expect(mockValueChangeObserver.complete).not.toHaveBeenCalled(); + }); + + it("should remove the mask when the config is undefined", () => { + changeInputValue(inputElement, "12345"); + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe("12,345"); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + + mockValueChangeObserver.next.calls.reset(); + hostComponent.numberMaskConfig = undefined; + fixture.detectChanges(); + expect(mockValueChangeObserver.next).not.toHaveBeenCalled(); // no value change, the mask was just disabled + + mockValueChangeObserver.next.calls.reset(); + changeInputValue(inputElement, "whatever+1*23-4/5"); + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe("whatever+1*23-4/5"); // no mask at all + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + expect(mockValueChangeObserver.error).not.toHaveBeenCalled(); + expect(mockValueChangeObserver.complete).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/packages/stark-ui/src/modules/input-mask-directives/directives/number-mask.directive.ts b/packages/stark-ui/src/modules/input-mask-directives/directives/number-mask.directive.ts new file mode 100644 index 0000000000..34740c8079 --- /dev/null +++ b/packages/stark-ui/src/modules/input-mask-directives/directives/number-mask.directive.ts @@ -0,0 +1,127 @@ +import { Directive, ElementRef, forwardRef, Inject, Input, OnChanges, Optional, Provider, Renderer2, SimpleChanges } from "@angular/core"; +import { COMPOSITION_BUFFER_MODE, NG_VALUE_ACCESSOR } from "@angular/forms"; +import { MaskedInputDirective, TextMaskConfig as Ng2TextMaskConfig } from "angular2-text-mask"; +import { createNumberMask } from "text-mask-addons"; +import { StarkNumberMaskConfig } from "./number-mask-config.intf"; + +/** + * Name of the directive + */ +const directiveName: string = "[starkNumberMask]"; + +/** + * @ignore + */ +export const STARK_NUMBER_MASK_VALUE_ACCESSOR: Provider = { + provide: NG_VALUE_ACCESSOR, + // tslint:disable-next-line:no-forward-ref + useExisting: forwardRef(() => StarkNumberMaskDirective), + multi: true +}; + +/** + * Directive to display a number mask in input elements. This directive internally uses the {@link https://github.com/text-mask/text-mask/tree/master/core|text-mask-core} library + * to provide the input mask functionality. + * + * **`IMPORTANT:`** Currently the Number Mask supports only input of type text, tel, url, password, and search. + * Due to a limitation in browser API, other input types, such as email or number, cannot be supported. + * + * ### Disabling the mask + * Passing an `undefined` value as config to the directive will disable the mask. + * + * @example + * + * + * + * + * + * + */ +@Directive({ + host: { + "(input)": "_handleInput($event.target.value)", + "(blur)": "onTouched()", + "(compositionstart)": "_compositionStart()", + "(compositionend)": "_compositionEnd($event.target.value)" + }, + selector: directiveName, + exportAs: "starkNumberMask", + providers: [STARK_NUMBER_MASK_VALUE_ACCESSOR] +}) +export class StarkNumberMaskDirective extends MaskedInputDirective implements OnChanges { + /** + * Configuration object for the mask to be displayed in the input field. + */ + /* tslint:disable:no-input-rename */ + @Input("starkNumberMask") + public maskConfig: StarkNumberMaskConfig; + + /** + * @ignore + */ + public elementRef: ElementRef; + + /** + * Default configuration. + * It will be merged with the configuration passed to the directive. + */ + private readonly defaultNumberMaskConfig: StarkNumberMaskConfig = { + prefix: "", + suffix: "", + includeThousandsSeparator: true, + thousandsSeparatorSymbol: ",", + allowDecimal: false, + decimalSymbol: ".", + decimalLimit: 2, + requireDecimal: false, + allowNegative: true, + allowLeadingZeroes: false + }; + + /** + * Class constructor + * @param _renderer - Angular Renderer wrapper for DOM manipulations. + * @param _elementRef - Reference to the DOM element where this directive is applied to. + * @param _compositionMode - Injected token to control if form directives buffer IME input until the "compositionend" event occurs. + */ + public constructor( + _renderer: Renderer2, + _elementRef: ElementRef, + @Optional() @Inject(COMPOSITION_BUFFER_MODE) _compositionMode: boolean + ) { + super(_renderer, _elementRef, _compositionMode); + this.elementRef = _elementRef; + } + + /** + * Component lifecycle hook + */ + public ngOnChanges(changes: SimpleChanges): void { + this.textMaskConfig = this.normalizeMaskConfig(this.maskConfig); + + super.ngOnChanges(changes); + + // TODO: temporary workaround to update the model when the maskConfig changes since this is not yet implemented in text-mask and still being discussed + // see: https://github.com/text-mask/text-mask/issues/657 + if (changes["maskConfig"] && !changes["maskConfig"].isFirstChange() && this.textMaskConfig.mask !== false) { + // trigger a dummy "input" event in the input to trigger the changes in the model (only if the mask was not disabled!) + const ev: Event = document.createEvent("Event"); + ev.initEvent("input", true, true); + (this.elementRef.nativeElement).dispatchEvent(ev); + } + } + + /** + * Create a valid configuration to be passed to the MaskedInputDirective + * @param maskConfig - The provided configuration via the directive's input + */ + public normalizeMaskConfig(maskConfig: StarkNumberMaskConfig): Ng2TextMaskConfig { + if (typeof maskConfig === "undefined") { + return { mask: false }; // remove the mask + } else { + // TODO: Ng2TextMaskConfig is not the same as Core TextMaskConfig + const numberMaskConfig: StarkNumberMaskConfig = { ...this.defaultNumberMaskConfig, ...maskConfig }; + return { mask: createNumberMask(numberMaskConfig) }; + } + } +} diff --git a/packages/stark-ui/src/modules/input-mask-directives/directives/text-mask-config.intf.ts b/packages/stark-ui/src/modules/input-mask-directives/directives/text-mask-config.intf.ts index f5fb231ac0..853140e553 100644 --- a/packages/stark-ui/src/modules/input-mask-directives/directives/text-mask-config.intf.ts +++ b/packages/stark-ui/src/modules/input-mask-directives/directives/text-mask-config.intf.ts @@ -1,7 +1,7 @@ import { Mask, PipeFunction } from "text-mask-core"; /** - * Defines the base configuration for the mask directives provided by Stark. + * Defines the base configuration for the mask directives provided by Stark-UI. */ export interface StarkTextMaskBaseConfig { /** @@ -27,7 +27,7 @@ export interface StarkTextMaskBaseConfig { } /** - * Defines the configuration for the {@link StarkTextMaskDirective}. + * Defines the configuration object for the {@link StarkTextMaskDirective}. */ export interface StarkTextMaskConfig extends StarkTextMaskBaseConfig { /** diff --git a/packages/stark-ui/src/modules/input-mask-directives/directives/text-mask.directive.ts b/packages/stark-ui/src/modules/input-mask-directives/directives/text-mask.directive.ts index c1206cddb9..32b38160ab 100644 --- a/packages/stark-ui/src/modules/input-mask-directives/directives/text-mask.directive.ts +++ b/packages/stark-ui/src/modules/input-mask-directives/directives/text-mask.directive.ts @@ -18,16 +18,6 @@ export const STARK_TEXT_MASK_VALUE_ACCESSOR: Provider = { multi: true }; -/** - * @ignore - */ -const defaultTextMaskConfig: StarkTextMaskConfig = { - mask: false, // by default the mask is disabled - guide: true, - placeholderChar: "_", - keepCharPositions: true -}; - /** * Directive to display a mask in input elements. This directive internally uses the {@link https://github.com/text-mask/text-mask/tree/master/core|text-mask-core} library * to provide the input mask functionality. @@ -35,6 +25,9 @@ const defaultTextMaskConfig: StarkTextMaskConfig = { * **`IMPORTANT:`** Currently the Text Mask supports only input of type text, tel, url, password, and search. * Due to a limitation in browser API, other input types, such as email or number, cannot be supported. * + * ### Disabling the mask + * Passing an `undefined` value as config or a config object with `mask: false` will disable the mask. + * * @example * * @@ -67,6 +60,17 @@ export class StarkTextMaskDirective extends MaskedInputDirective implements OnCh */ private elementRef: ElementRef; + /** + * Default configuration. + * It will be merged with the configuration passed to the directive. + */ + private readonly defaultTextMaskConfig: StarkTextMaskConfig = { + mask: false, // by default the mask is disabled + guide: true, + placeholderChar: "_", + keepCharPositions: true + }; + /** * Class constructor * @param _renderer - Angular Renderer wrapper for DOM manipulations. @@ -106,6 +110,6 @@ export class StarkTextMaskDirective extends MaskedInputDirective implements OnCh */ public normalizeMaskConfig(maskConfig: StarkTextMaskConfig): Ng2TextMaskConfig { // TODO: Ng2TextMaskConfig is not the same as Core TextMaskConfig - return { ...defaultTextMaskConfig, ...(maskConfig) }; + return { ...this.defaultTextMaskConfig, ...(maskConfig) }; } } diff --git a/packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-mask-config.intf.ts b/packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-mask-config.intf.ts new file mode 100644 index 0000000000..7f4054841e --- /dev/null +++ b/packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-mask-config.intf.ts @@ -0,0 +1,9 @@ +/** + * Defines the configuration for the {@link StarkTimestampMaskDirective}. + */ +export interface StarkTimestampMaskConfig { + /** + * Format of the date and/or time. For example: "DD-MM-YYYY hh:mm:ss" + */ + format: string; +} diff --git a/packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-mask.directive.spec.ts b/packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-mask.directive.spec.ts new file mode 100644 index 0000000000..1f78687c1c --- /dev/null +++ b/packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-mask.directive.spec.ts @@ -0,0 +1,413 @@ +/* tslint:disable:completed-docs no-duplicate-string no-identical-functions no-big-function */ +import { Component, DebugElement } from "@angular/core"; +import { FormControl, FormsModule, ReactiveFormsModule } from "@angular/forms"; +import { By } from "@angular/platform-browser"; +import { ComponentFixture, fakeAsync, TestBed } from "@angular/core/testing"; +import { Observer } from "rxjs"; +import { StarkTimestampMaskDirective } from "./timestamp-mask.directive"; +import { StarkTimestampMaskConfig } from "./timestamp-mask-config.intf"; + +describe("TimestampMaskDirective", () => { + let fixture: ComponentFixture; + let hostComponent: TestComponent; + let inputElement: DebugElement; + + const timestampMaskConfig: StarkTimestampMaskConfig = { + format: "DD/MM/YYYY" + }; + + @Component({ + selector: "test-component", + template: getTemplate("[starkTimestampMask]='timestampMaskConfig'") + }) + class TestComponent { + public timestampMaskConfig: StarkTimestampMaskConfig = timestampMaskConfig; + public ngModelValue: string = ""; + public formControl: FormControl = new FormControl(""); + } + + function getTemplate(timestampMaskDirective: string): string { + return ""; + } + + function initializeComponentFixture(): void { + fixture = TestBed.createComponent(TestComponent); + hostComponent = fixture.componentInstance; + inputElement = fixture.debugElement.query(By.css("input")); + // trigger initial data binding + fixture.detectChanges(); + } + + function changeInputValue(inputDebugElement: DebugElement, value: string, eventType: string = "input"): void { + (inputDebugElement.nativeElement).value = value; + + // more verbose way to create and trigger an event (the only way it works in IE) + // https://developer.mozilla.org/en-US/docs/Web/Guide/Events/Creating_and_triggering_events + const ev: Event = document.createEvent("Event"); + ev.initEvent(eventType, true, true); + (inputDebugElement.nativeElement).dispatchEvent(ev); + } + + // Inject module dependencies + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [StarkTimestampMaskDirective, TestComponent], + imports: [FormsModule, ReactiveFormsModule], + providers: [] + }); + }); + + describe("uncontrolled", () => { + beforeEach(fakeAsync(() => { + // compile template and css + return TestBed.compileComponents(); + })); + + beforeEach(() => { + initializeComponentFixture(); + }); + + it("should render the appropriate content", () => { + expect(inputElement.attributes["ng-reflect-mask-config"]).toBeDefined(); // starkTimestampMask directive + }); + + it("should update the input value and show the mask only when a valid event is triggered in the input field", () => { + // Angular2 text-mask directive handles only the "input" event + const validEvents: string[] = ["input"]; + + for (const eventType of validEvents) { + changeInputValue(inputElement, ""); + fixture.detectChanges(); + expect(inputElement.nativeElement.value).toBe(""); + + changeInputValue(inputElement, "123", eventType); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("12/3_/____"); + } + + const invalidEvents: string[] = ["blur", "keyup", "change", "focus", "keydown", "keypress", "click"]; + + for (const eventType of invalidEvents) { + changeInputValue(inputElement, ""); + fixture.detectChanges(); + expect(inputElement.nativeElement.value).toBe(""); + + changeInputValue(inputElement, "123", eventType); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("123"); // no mask shown + } + }); + + it("should prevent invalid values to be entered in the input field when the value is changed manually", () => { + const invalidValues: string[] = ["a", " ", "whatever"]; + + for (const value of invalidValues) { + changeInputValue(inputElement, value); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe(""); + } + }); + + it("should refresh the mask whenever the configuration changes", () => { + changeInputValue(inputElement, "123"); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("12/3_/____"); + + hostComponent.timestampMaskConfig = { ...timestampMaskConfig, format: "DD-MM" }; + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("12-3_"); + }); + + it("should remove the mask when the config is undefined", () => { + changeInputValue(inputElement, "123"); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("12/3_/____"); + + hostComponent.timestampMaskConfig = undefined; + fixture.detectChanges(); + + changeInputValue(inputElement, "whatever"); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("whatever"); // no mask at all + }); + + it("should allow to enter February 29 manually in the input field when a year is foreseen but is not yet entered", () => { + changeInputValue(inputElement, "2902"); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("29/02/____"); + + // FIXME: currently the text-mask library throws if the value is not cleared before changing the config + // in fact the model in not changed after changing the config. which is not implemented yet in text-mask and still being discussed + // see: https://github.com/text-mask/text-mask/issues/657 + // the error is thrown as long as the entered value is not valid according to the new config + changeInputValue(inputElement, ""); + fixture.detectChanges(); + + hostComponent.timestampMaskConfig = { ...timestampMaskConfig, format: "MM-DD-YY" }; + fixture.detectChanges(); + + changeInputValue(inputElement, "0229"); + fixture.detectChanges(); + + expect(inputElement.nativeElement.value).toBe("02-29-__"); + }); + }); + + describe("with ngModel", () => { + beforeEach(fakeAsync(() => { + const newTemplate: string = getTemplate("[(ngModel)]='ngModelValue' [starkTimestampMask]='timestampMaskConfig'"); + + TestBed.overrideTemplate(TestComponent, newTemplate); + + // compile template and css + return TestBed.compileComponents(); + })); + + beforeEach(() => { + initializeComponentFixture(); + }); + + it("should render the appropriate content", () => { + expect(inputElement.attributes["ng-reflect-mask-config"]).toBeDefined(); // starkTimestampMask directive + }); + + it("should update the input value and show the mask only when a valid event is triggered in the input field", () => { + // Angular2 text-mask directive handles only the "input" event + const validEvents: string[] = ["input"]; + + for (const eventType of validEvents) { + changeInputValue(inputElement, ""); + fixture.detectChanges(); + expect(hostComponent.ngModelValue).toBe(""); + + changeInputValue(inputElement, "123", eventType); + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe("12/3_/____"); + } + + const invalidEvents: string[] = ["blur", "keyup", "change", "focus", "keydown", "keypress", "click"]; + + for (const eventType of invalidEvents) { + changeInputValue(inputElement, ""); + fixture.detectChanges(); + expect(hostComponent.ngModelValue).toBe(""); + + changeInputValue(inputElement, "123", eventType); + fixture.detectChanges(); + + // IMPORTANT: the ngModel is not changed with invalid events, just with "input" events + expect(hostComponent.ngModelValue).toBe(""); // no mask shown + } + }); + + it("should prevent invalid values to be entered in the input field when the value is changed manually", () => { + const invalidValues: string[] = ["a", " ", "whatever"]; + + for (const value of invalidValues) { + changeInputValue(inputElement, value); + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe(""); + } + }); + + it("should refresh the mask whenever the configuration changes", () => { + changeInputValue(inputElement, "123"); + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe("12/3_/____"); + + hostComponent.timestampMaskConfig = { ...timestampMaskConfig, format: "DD-MM" }; + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe("12-3_"); + }); + + it("should remove the mask when the config is undefined", () => { + changeInputValue(inputElement, "123"); + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe("12/3_/____"); + + hostComponent.timestampMaskConfig = undefined; + fixture.detectChanges(); + + changeInputValue(inputElement, "whatever"); + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe("whatever"); // no mask at all + }); + + it("should allow to enter February 29 manually in the input field when a year is foreseen but is not yet entered", () => { + changeInputValue(inputElement, "2902"); + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe("29/02/____"); + + // FIXME: currently the text-mask library throws if the value is not cleared before changing the config + // in fact the model in not changed after changing the config. which is not implemented yet in text-mask and still being discussed + // see: https://github.com/text-mask/text-mask/issues/657 + // the error is thrown as long as the entered value is not valid according to the new config + changeInputValue(inputElement, ""); + fixture.detectChanges(); + + hostComponent.timestampMaskConfig = { ...timestampMaskConfig, format: "MM-DD-YY" }; + fixture.detectChanges(); + + changeInputValue(inputElement, "0229"); + fixture.detectChanges(); + + expect(hostComponent.ngModelValue).toBe("02-29-__"); + }); + }); + + describe("with FormControl", () => { + let mockValueChangeObserver: jasmine.SpyObj>; + + beforeEach(fakeAsync(() => { + const newTemplate: string = getTemplate("[formControl]='formControl' [starkTimestampMask]='timestampMaskConfig'"); + + TestBed.overrideTemplate(TestComponent, newTemplate); + + // compile template and css + return TestBed.compileComponents(); + })); + + beforeEach(() => { + initializeComponentFixture(); + + mockValueChangeObserver = jasmine.createSpyObj>("observerSpy", ["next", "error", "complete"]); + hostComponent.formControl.valueChanges.subscribe(mockValueChangeObserver); + }); + + it("should render the appropriate content", () => { + expect(inputElement.attributes["ng-reflect-mask-config"]).toBeDefined(); // starkTimestampMask directive + }); + + it("should update the input value and show the mask only when a valid event is triggered in the input field", () => { + // Angular2 text-mask directive handles only the "input" event + const validEvents: string[] = ["input"]; + + for (const eventType of validEvents) { + changeInputValue(inputElement, ""); + fixture.detectChanges(); + expect(hostComponent.formControl.value).toBe(""); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + + mockValueChangeObserver.next.calls.reset(); + changeInputValue(inputElement, "123", eventType); + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe("12/3_/____"); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + expect(mockValueChangeObserver.error).not.toHaveBeenCalled(); + expect(mockValueChangeObserver.complete).not.toHaveBeenCalled(); + } + + mockValueChangeObserver.next.calls.reset(); + const invalidEvents: string[] = ["blur", "keyup", "change", "focus", "keydown", "keypress", "click"]; + + for (const eventType of invalidEvents) { + changeInputValue(inputElement, ""); + fixture.detectChanges(); + expect(hostComponent.formControl.value).toBe(""); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + + mockValueChangeObserver.next.calls.reset(); + changeInputValue(inputElement, "123", eventType); + fixture.detectChanges(); + + // IMPORTANT: the formControl is not changed with invalid events, just with "input" events + expect(hostComponent.formControl.value).toBe(""); // no mask shown + expect(mockValueChangeObserver.next).not.toHaveBeenCalled(); + expect(mockValueChangeObserver.error).not.toHaveBeenCalled(); + expect(mockValueChangeObserver.complete).not.toHaveBeenCalled(); + } + }); + + it("should prevent invalid values to be entered in the input field when the value is changed manually", () => { + const invalidValues: string[] = ["a", " ", "whatever"]; + + for (const value of invalidValues) { + mockValueChangeObserver.next.calls.reset(); + changeInputValue(inputElement, value); + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe(""); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + expect(mockValueChangeObserver.error).not.toHaveBeenCalled(); + expect(mockValueChangeObserver.complete).not.toHaveBeenCalled(); + } + }); + + it("should refresh the mask whenever the configuration changes", () => { + changeInputValue(inputElement, "123"); + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe("12/3_/____"); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + + mockValueChangeObserver.next.calls.reset(); + hostComponent.timestampMaskConfig = { ...timestampMaskConfig, format: "DD-MM" }; + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe("12-3_"); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + expect(mockValueChangeObserver.error).not.toHaveBeenCalled(); + expect(mockValueChangeObserver.complete).not.toHaveBeenCalled(); + }); + + it("should remove the mask when the config is undefined", () => { + changeInputValue(inputElement, "123"); + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe("12/3_/____"); + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + + mockValueChangeObserver.next.calls.reset(); + hostComponent.timestampMaskConfig = undefined; + fixture.detectChanges(); + expect(mockValueChangeObserver.next).not.toHaveBeenCalled(); // no value change, the mask was just disabled + + mockValueChangeObserver.next.calls.reset(); + changeInputValue(inputElement, "whatever"); + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe("whatever"); // no mask at all + expect(mockValueChangeObserver.next).toHaveBeenCalledTimes(1); + expect(mockValueChangeObserver.error).not.toHaveBeenCalled(); + expect(mockValueChangeObserver.complete).not.toHaveBeenCalled(); + }); + + it("should allow to enter February 29 manually in the input field when a year is foreseen but is not yet entered", () => { + changeInputValue(inputElement, "2902"); + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe("29/02/____"); + + // FIXME: currently the text-mask library throws if the value is not cleared before changing the config + // in fact the model in not changed after changing the config. which is not implemented yet in text-mask and still being discussed + // see: https://github.com/text-mask/text-mask/issues/657 + // the error is thrown as long as the entered value is not valid according to the new config + changeInputValue(inputElement, ""); + fixture.detectChanges(); + + hostComponent.timestampMaskConfig = { ...timestampMaskConfig, format: "MM-DD-YY" }; + fixture.detectChanges(); + + changeInputValue(inputElement, "0229"); + fixture.detectChanges(); + + expect(hostComponent.formControl.value).toBe("02-29-__"); + }); + }); +}); diff --git a/packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-mask.directive.ts b/packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-mask.directive.ts new file mode 100644 index 0000000000..ff071483fa --- /dev/null +++ b/packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-mask.directive.ts @@ -0,0 +1,147 @@ +import { Directive, ElementRef, forwardRef, Inject, Input, OnChanges, Optional, Provider, Renderer2, SimpleChanges } from "@angular/core"; +import { COMPOSITION_BUFFER_MODE, NG_VALUE_ACCESSOR } from "@angular/forms"; +import { MaskedInputDirective, TextMaskConfig as Ng2TextMaskConfig } from "angular2-text-mask"; +import { MaskArray } from "text-mask-core"; +import { StarkTimestampMaskConfig } from "./timestamp-mask-config.intf"; +import { createTimestampPipe } from "./timestamp-pipe.fn"; + +/** + * Name of the directive + */ +const directiveName: string = "[starkTimestampMask]"; + +/** + * @ignore + */ +export const STARK_TIMESTAMP_MASK_VALUE_ACCESSOR: Provider = { + provide: NG_VALUE_ACCESSOR, + // tslint:disable-next-line:no-forward-ref + useExisting: forwardRef(() => StarkTimestampMaskDirective), + multi: true +}; + +/** + * Directive to display a timestamp mask in input elements. This directive internally uses the {@link https://github.com/text-mask/text-mask/tree/master/core|text-mask-core} library + * to provide the input mask functionality. + * + * **`IMPORTANT:`** Currently the Number Mask supports only input of type text, tel, url, password, and search. + * Due to a limitation in browser API, other input types, such as email or number, cannot be supported. + * + * ### Disabling the mask + * Passing an `undefined` value as config to the directive will disable the mask. + * + * @example + * + * + * + * + * + * + */ +@Directive({ + host: { + "(input)": "_handleInput($event.target.value)", + "(blur)": "onTouched()", + "(compositionstart)": "_compositionStart()", + "(compositionend)": "_compositionEnd($event.target.value)" + }, + selector: directiveName, + exportAs: "starkTimestampMask", + providers: [STARK_TIMESTAMP_MASK_VALUE_ACCESSOR] +}) +export class StarkTimestampMaskDirective extends MaskedInputDirective implements OnChanges { + /** + * Configuration object for the mask to be displayed in the input field. + */ + /* tslint:disable:no-input-rename */ + @Input("starkTimestampMask") + public maskConfig: StarkTimestampMaskConfig; + + /** + * @ignore + */ + public elementRef: ElementRef; + + /** + * Default configuration. + * It will be merged with the configuration passed to the directive. + */ + private readonly defaultTimestampMaskConfig: StarkTimestampMaskConfig = { + format: "DD-MM-YYYY HH:mm:ss" + }; + + /** + * Class constructor + * @param _renderer - Angular Renderer wrapper for DOM manipulations. + * @param _elementRef - Reference to the DOM element where this directive is applied to. + * @param _compositionMode - Injected token to control if form directives buffer IME input until the "compositionend" event occurs. + */ + public constructor( + _renderer: Renderer2, + _elementRef: ElementRef, + @Optional() @Inject(COMPOSITION_BUFFER_MODE) _compositionMode: boolean + ) { + super(_renderer, _elementRef, _compositionMode); + this.elementRef = _elementRef; + } + + /** + * Component lifecycle hook + */ + public ngOnChanges(changes: SimpleChanges): void { + this.textMaskConfig = this.normalizeMaskConfig(this.maskConfig); + + super.ngOnChanges(changes); + + // TODO: temporary workaround to update the model when the maskConfig changes since this is not yet implemented in text-mask and still being discussed + // see: https://github.com/text-mask/text-mask/issues/657 + if (changes["maskConfig"] && !changes["maskConfig"].isFirstChange() && this.textMaskConfig.mask !== false) { + // trigger a dummy "input" event in the input to trigger the changes in the model (only if the mask was not disabled!) + const ev: Event = document.createEvent("Event"); + ev.initEvent("input", true, true); + (this.elementRef.nativeElement).dispatchEvent(ev); + } + } + + /** + * Create a valid configuration to be passed to the MaskedInputDirective + * @param maskConfig - The provided configuration via the directive's input + */ + public normalizeMaskConfig(maskConfig: StarkTimestampMaskConfig): Ng2TextMaskConfig { + if (typeof maskConfig === "undefined") { + return { mask: false }; // remove the mask + } else { + // TODO: Ng2TextMaskConfig is not the same as Core TextMaskConfig + const timestampMaskConfig: StarkTimestampMaskConfig = { ...this.defaultTimestampMaskConfig, ...maskConfig }; + + return { + pipe: createTimestampPipe(timestampMaskConfig.format), + mask: this.convertFormatIntoMask(timestampMaskConfig.format), + placeholderChar: "_" + }; + } + } + + /** + * Construct a valid Mask out of the given timestamp format string + * @param format - The timestamp format string + */ + public convertFormatIntoMask(format: string): MaskArray { + const mask: MaskArray = []; + for (let i: number = 0; i < format.length; i++) { + if ( + format.charAt(i) === "D" || + format.charAt(i) === "M" || + format.charAt(i) === "Y" || + format.charAt(i) === "H" || + format.charAt(i) === "m" || + format.charAt(i) === "s" + ) { + mask[i] = /\d/; + } else { + mask[i] = format.charAt(i); + } + } + return mask; + } +} diff --git a/packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-pipe.fn.spec.ts b/packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-pipe.fn.spec.ts new file mode 100644 index 0000000000..a17e1e03b1 --- /dev/null +++ b/packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-pipe.fn.spec.ts @@ -0,0 +1,122 @@ +/* tslint:disable:completed-docs */ +import { createTimestampPipe } from "./timestamp-pipe.fn"; + +describe("createTimestampPipe", () => { + const fullDateTimeLongYearFormat: string = "YYYY-DD-MM HH:mm:ss"; + const fullDateTimeShortYearFormat: string = "DD-MM-YY HH:mm:ss"; + + function assertTimestampsValidity(dateTimeStrings: string[], shouldBeValid: boolean, customFormat?: string): void { + const timestampPipeFn: Function = createTimestampPipe(customFormat); + + for (const dateTimeStr of dateTimeStrings) { + const expectedResult: boolean | string = shouldBeValid ? dateTimeStr : false; + expect(timestampPipeFn(dateTimeStr)).toBe(expectedResult); + } + } + + it("should return a pipe function regardless of whether a custom format is passed or not", () => { + let timestampPipeFn: Function = createTimestampPipe(fullDateTimeLongYearFormat); + expect(typeof timestampPipeFn).toBe("function"); + + timestampPipeFn = createTimestampPipe(fullDateTimeShortYearFormat); + expect(typeof timestampPipeFn).toBe("function"); + + timestampPipeFn = createTimestampPipe(); + expect(typeof timestampPipeFn).toBe("function"); + }); + + describe("with the default format: 'DD-MM-YYYY HH:mm:ss'", () => { + it("should return the same date time string if a part of date time string is correct", () => { + const validDateTimeStrings: string[] = [ + "31", + "29-02", + "29-02-20", + "29-02-200", + "0", // when typing a day starting with 0 + "29-0" // when typing a month starting with 0 + ]; + + assertTimestampsValidity(validDateTimeStrings, true); + }); + + it("should return FALSE if a part of date time string is incorrect", () => { + const invalidDateTimeStrings: string[] = ["02-13", "30-02", "31-04"]; + + assertTimestampsValidity(invalidDateTimeStrings, false); + }); + + it("should return FALSE if the date time string doesn't match the format", () => { + const invalidDateTimeStrings: string[] = [ + "32-12-2000 12:12:12", + "40-12-2000 12:12:12", + "22-13-2000 12:12:12", + "22-20-2000 12:12:12", + "22-11-3000 12:12:12", + "22-11-2000 62:12:12", + "22-11-2000 70:12:12", + "22-11-2000 44:13:12", + "22-11-2000 44:70:12", + "22-11-2000 44:44:62", + "22-11-2000 44:44:70" + ]; + + assertTimestampsValidity(invalidDateTimeStrings, false); + }); + }); + + describe("with a custom format", () => { + it("should return the same date time string if a part of date time string is correct", () => { + let validDateTimeStrings: string[] = [ + "2017-30-12", + "2016-29-02 10:15:20" // leap year + ]; + + assertTimestampsValidity(validDateTimeStrings, true, fullDateTimeLongYearFormat); + + validDateTimeStrings = [ + "22-11-15 12:12:12", + "29-02", + "29-02-16 10:15:20" // leap year + ]; + + assertTimestampsValidity(validDateTimeStrings, true, fullDateTimeShortYearFormat); + }); + + it("should return FALSE if 29 February is given and the format given has no year", () => { + // it is invalid because when there is no year specified, it is assumed to be something recurrent + // and you shouldn't put something recurrent on a Feb 29! + + let validDateTimeStrings: string[] = [ + "29-02" // valid date until no year is entered + ]; + + assertTimestampsValidity(validDateTimeStrings, false, "DD-MM"); + + validDateTimeStrings = [ + "02-29" // valid date until no year is entered + ]; + + assertTimestampsValidity(validDateTimeStrings, false, "MM-DD"); + }); + + it("should return FALSE if the date time string doesn't match the format", () => { + let invalidDateTimeStrings: string[] = [ + "2017/30/12", + "2017/30/12 12:12:12", + "2017-29-02 10:15:20", // non leap year + "31-12-2000" + ]; + + assertTimestampsValidity(invalidDateTimeStrings, false, fullDateTimeLongYearFormat); + + invalidDateTimeStrings = [ + "30/12/17", + "30/12/2017 12:12:12", + "29-02-17 10:15:20", // non leap year + "31-12-2000" + ]; + + assertTimestampsValidity(invalidDateTimeStrings, false, fullDateTimeShortYearFormat); + }); + }); +}); diff --git a/packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-pipe.fn.ts b/packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-pipe.fn.ts new file mode 100644 index 0000000000..47364ac942 --- /dev/null +++ b/packages/stark-ui/src/modules/input-mask-directives/directives/timestamp-pipe.fn.ts @@ -0,0 +1,88 @@ +import { starkIsDateTime } from "@nationalbankbelgium/stark-core"; +import { PipeFunction } from "text-mask-core"; + +// TODO: refactor this function to reduce its cognitive complexity +/** + * Creates a PipeFunction to be used with the {@link StarkTimestampMaskDirective} to enforce an specific timestamp format. + * @param timestampFormat - Timestamp format to be enforced by the pipe function to be created + */ +// tslint:disable-next-line:cognitive-complexity +export function createTimestampPipe(timestampFormat: string = "DD-MM-YYYY HH:mm:ss"): PipeFunction { + return (conformedValue: string) => { + const dateFormatArray: string[] = timestampFormat.split(/[^DMYHms]+/); + const maxValue: object = { DD: 31, MM: 12, YYYY: 2999, HH: 24, mm: 60, ss: 60 }; + const minValue: object = { DD: 0, MM: 0, YYYY: 1, HH: 0, mm: 0, ss: 0 }; + + let skipValidation: boolean = false; + + // Check for invalid date + const isInvalid: boolean = dateFormatArray.some((format: string) => { + const position: number = timestampFormat.indexOf(format); + const length: number = format.length; + const textValue: string = conformedValue.substr(position, length).replace(/\D/g, ""); + const value: number = parseInt(textValue, 10); + + // skip the validation if the day starts with 0, but is not 00 + // because if we would validate it would give not valid, because day 0 doesn't exist + // but maybe we want to type for example 02 + // it should not give invalid if we only already have typed the 0 + if (format === "DD" && (value === 0 && textValue !== "00")) { + skipValidation = true; + // same for month + } else if (format === "MM" && (value === 0 && textValue !== "00")) { + skipValidation = true; + } + return value > maxValue[format] || (textValue.length === length && value < minValue[format]); + }); + + // remove all non digits at the end of the conformed value + const inputValue: string = conformedValue.replace(/\D*$/, ""); + const partialFormat: string = timestampFormat.substring(0, inputValue.length); + + // MomentJs gives always false for input 31, but it depends on the month + // so we say it is always true + // if 31 is a month or year or hour than we couldn't even type the 3 + if (inputValue === "31") { + skipValidation = true; + + // 29 february must be checked after we have typed the year if there is a year in the format + } else if (isLeapDay(inputValue, partialFormat, timestampFormat)) { + skipValidation = true; + } + + if (!skipValidation && !isInvalid && inputValue.length > 0 && !starkIsDateTime(inputValue, partialFormat)) { + return false; + } + + skipValidation = false; + + if (isInvalid) { + return false; + } + + return conformedValue; + }; +} + +/** + * @ignore + */ +function isLeapDay(value: string, format: string, fullFormat: string): boolean { + const textValue: string = value.replace(/\D/, ""); // removing all non digits + const dayMonthFormat: string = format.replace(/[^DM]/, ""); // keeping only day and month parts + const leapDays: { format: string; date: string }[] = [{ format: "DDMM", date: "2902" }, { format: "MMDD", date: "0229" }]; + + // is leap day as long as there is no year entered yet and the full format does have a year part + for (const leapDay of leapDays) { + const indexOfDayMonth: number = dayMonthFormat.indexOf(leapDay.format); + if ( + textValue.substr(indexOfDayMonth, 4) === leapDay.date && + dayMonthFormat.substr(indexOfDayMonth, 4) === leapDay.format && + ((fullFormat.indexOf("YYYY") > 0 && value.length < fullFormat.indexOf("YYYY") + 4) || + (fullFormat.indexOf("YY") > 0 && value.length < fullFormat.indexOf("YY") + 2)) + ) { + return true; + } + } + return false; +} diff --git a/packages/stark-ui/src/modules/input-mask-directives/input-mask-directives.module.ts b/packages/stark-ui/src/modules/input-mask-directives/input-mask-directives.module.ts index 8faf26a0c9..f3fb7013a8 100644 --- a/packages/stark-ui/src/modules/input-mask-directives/input-mask-directives.module.ts +++ b/packages/stark-ui/src/modules/input-mask-directives/input-mask-directives.module.ts @@ -1,8 +1,8 @@ import { NgModule } from "@angular/core"; -import { StarkTextMaskDirective } from "./directives"; +import { StarkEmailMaskDirective, StarkNumberMaskDirective, StarkTextMaskDirective, StarkTimestampMaskDirective } from "./directives"; @NgModule({ - declarations: [StarkTextMaskDirective], - exports: [StarkTextMaskDirective] + declarations: [StarkEmailMaskDirective, StarkNumberMaskDirective, StarkTextMaskDirective, StarkTimestampMaskDirective], + exports: [StarkEmailMaskDirective, StarkNumberMaskDirective, StarkTextMaskDirective, StarkTimestampMaskDirective] }) export class StarkInputMaskDirectivesModule {} diff --git a/showcase/src/app/demo-ui/demo-ui.module.ts b/showcase/src/app/demo-ui/demo-ui.module.ts index ca18b837c7..1ef8cf78e6 100644 --- a/showcase/src/app/demo-ui/demo-ui.module.ts +++ b/showcase/src/app/demo-ui/demo-ui.module.ts @@ -33,7 +33,6 @@ import { StarkGenericSearchModule, StarkInputMaskDirectivesModule, StarkKeyboardDirectivesModule, - StarkTransformInputDirectiveModule, StarkLanguageSelectorModule, StarkMinimapModule, StarkPaginationModule, @@ -42,7 +41,8 @@ import { StarkRouteSearchModule, StarkSliderModule, StarkSvgViewBoxModule, - StarkTableModule + StarkTableModule, + StarkTransformInputDirectiveModule } from "@nationalbankbelgium/stark-ui"; import { DemoActionBarPageComponent, @@ -166,7 +166,7 @@ import { TableWithCustomStylingComponent, DemoToastPageComponent, DemoGenericSearchFormComponent, - DemoTransformInputDirectivePageComponent, + DemoTransformInputDirectivePageComponent ], exports: [ DemoActionBarPageComponent, diff --git a/showcase/src/app/demo-ui/pages/input-mask-directives/demo-input-mask-directives-page.component.html b/showcase/src/app/demo-ui/pages/input-mask-directives/demo-input-mask-directives-page.component.html index 550532b264..8323c0736c 100644 --- a/showcase/src/app/demo-ui/pages/input-mask-directives/demo-input-mask-directives-page.component.html +++ b/showcase/src/app/demo-ui/pages/input-mask-directives/demo-input-mask-directives-page.component.html @@ -9,13 +9,7 @@

SHOWCASE.DEMO.SHARED.EXAMPLE_VIEWER_LIST

-
+
SHOWCASE.DEMO.DIRECTIVES.INPUT_MASK.TEXT_MASK.CREDIT_CARD_MASK SHOWCASE.DEMO.SHARED.EXAMPLE_VIEWER_LIST id="structured-message-input-mask" type="text" [(ngModel)]="structuredMessage" - (input)="logModelChange()" + (input)="logModelChange(structuredMessage)" placeholder="+++###/####/#####/+++" [starkTextMask]="structuredMessageMaskConfig" /> - SHOWCASE.DEMO.DIRECTIVES.INPUT_MASK.TEXT_MASK.CUSTOM_MASK + SHOWCASE.DEMO.DIRECTIVES.INPUT_MASK.TEXT_MASK.PHONE_NUMBER_MASK + +
+ + + + + + +
+ + SHOWCASE.DEMO.DIRECTIVES.INPUT_MASK.NUMBER_MASK.EURO_AMOUNT_MASK + + + + + SHOWCASE.DEMO.DIRECTIVES.INPUT_MASK.NUMBER_MASK.US_DOLLARS_AMOUNT_MASK + + + + + SHOWCASE.DEMO.DIRECTIVES.INPUT_MASK.NUMBER_MASK.PERCENTAGE_MASK + + +
+
+ + + + + +
+ + SHOWCASE.DEMO.DIRECTIVES.INPUT_MASK.EMAIL_MASK.EMAIL_MASK + + +
+
+ + + + + +
+ + SHOWCASE.DEMO.DIRECTIVES.INPUT_MASK.TIMESTAMP_MASK.TIMESTAMP_DMY_MASK + + + + + SHOWCASE.DEMO.DIRECTIVES.INPUT_MASK.TIMESTAMP_MASK.TIMESTAMP_MDY_MASK + + + + + SHOWCASE.DEMO.DIRECTIVES.INPUT_MASK.TIMESTAMP_MASK.FULL_DATE_DMY_MASK + + + + + SHOWCASE.DEMO.DIRECTIVES.INPUT_MASK.TIMESTAMP_MASK.DAY_MONTH_MASK + + + + + SHOWCASE.DEMO.DIRECTIVES.INPUT_MASK.TIMESTAMP_MASK.MONTH_DAY_MASK + + + + + SHOWCASE.DEMO.DIRECTIVES.INPUT_MASK.TIMESTAMP_MASK.TIME_MASK +
diff --git a/showcase/src/app/demo-ui/pages/input-mask-directives/demo-input-mask-directives-page.component.ts b/showcase/src/app/demo-ui/pages/input-mask-directives/demo-input-mask-directives-page.component.ts index 8a59a6317c..4e6b4640d1 100644 --- a/showcase/src/app/demo-ui/pages/input-mask-directives/demo-input-mask-directives-page.component.ts +++ b/showcase/src/app/demo-ui/pages/input-mask-directives/demo-input-mask-directives-page.component.ts @@ -1,8 +1,9 @@ import { Component, Inject, OnInit } from "@angular/core"; import { FormControl } from "@angular/forms"; import { STARK_LOGGING_SERVICE, StarkLoggingService } from "@nationalbankbelgium/stark-core"; -import { StarkTextMasks, StarkTextMaskConfig } from "@nationalbankbelgium/stark-ui"; +import { StarkTextMasks, StarkTextMaskConfig, StarkTimestampMaskConfig, StarkNumberMaskConfig } from "@nationalbankbelgium/stark-ui"; import { ReferenceLink } from "../../../shared/components/reference-block"; +import { merge } from "rxjs"; @Component({ selector: "showcase-demo-mask-directives", @@ -12,13 +13,33 @@ import { ReferenceLink } from "../../../shared/components/reference-block"; export class DemoInputMaskDirectivesPageComponent implements OnInit { public creditCardMaskConfig: StarkTextMaskConfig; public structuredMessageMaskConfig: StarkTextMaskConfig; - public customMaskConfig: StarkTextMaskConfig; + public phoneNumberMaskConfig: StarkTextMaskConfig; + public dateMaskConfig: StarkTimestampMaskConfig; + public dollarsMaskConfig: StarkNumberMaskConfig; + public eurosMaskConfig: StarkNumberMaskConfig; + public percentageMaskConfig: StarkNumberMaskConfig; + public timestampDMYMaskConfig: StarkTimestampMaskConfig; + public dayMonthMaskConfig: StarkTimestampMaskConfig; + public monthDayMaskConfig: StarkTimestampMaskConfig; + public timestampMDYMaskConfig: StarkTimestampMaskConfig; + public timeMaskConfig: StarkTimestampMaskConfig; + public structuredMessage: string; + public dollarsAmount: string; + public email: string; + public timestamp: string; + public monthDay: string; public phoneNumberField: FormControl; + public percentageField: FormControl; + public fullDateField: FormControl; + public timeField: FormControl; public referenceList: ReferenceLink[]; public constructor(@Inject(STARK_LOGGING_SERVICE) private logger: StarkLoggingService) { this.phoneNumberField = new FormControl(); + this.fullDateField = new FormControl(); + this.timeField = new FormControl(); + this.percentageField = new FormControl(); this.creditCardMaskConfig = { mask: StarkTextMasks.CREDITCARD_NUMBER @@ -28,15 +49,78 @@ export class DemoInputMaskDirectivesPageComponent implements OnInit { mask: StarkTextMasks.STRUCTURED_COMMUNICATION_NUMBER }; - this.customMaskConfig = { + this.phoneNumberMaskConfig = { mask: ["(", "+", "3", "2", ")", " ", /\d/, /\d/, /\d/, " ", /\d/, /\d/, " ", /\d/, /\d/, " ", /\d/, /\d/], placeholderChar: "#" }; + this.timestampDMYMaskConfig = { format: "DD-MM-YYYY HH:mm:ss" }; + + this.timestampMDYMaskConfig = { format: "MM-DD-YYYY HH:mm:ss" }; + + this.dateMaskConfig = { format: "DD-MM-YYYY" }; + + this.dayMonthMaskConfig = { format: "DD-MM" }; + + this.monthDayMaskConfig = { format: "MM/DD" }; + + this.timeMaskConfig = { format: "HH:mm:ss" }; + + this.eurosMaskConfig = { + prefix: "", + suffix: " €", + includeThousandsSeparator: true, + thousandsSeparatorSymbol: ",", + allowDecimal: true, + decimalSymbol: ".", + decimalLimit: 2, + integerLimit: 9, + allowNegative: false, + allowLeadingZeroes: false + }; + + this.dollarsMaskConfig = { + prefix: "$ ", + suffix: "", + includeThousandsSeparator: true, + thousandsSeparatorSymbol: ",", + allowDecimal: true, + decimalSymbol: ".", + decimalLimit: 2, + integerLimit: 9, + allowNegative: false, + allowLeadingZeroes: false + }; + + this.percentageMaskConfig = { + prefix: "% ", + suffix: "", + includeThousandsSeparator: true, + thousandsSeparatorSymbol: ",", + allowDecimal: true, + decimalSymbol: ".", + decimalLimit: 3, + integerLimit: 3, + allowNegative: true, + allowLeadingZeroes: true + }; + this.referenceList = [ + { + label: "Stark Email Mask directive", + url: "https://stark.nbb.be/api-docs/stark-ui/latest/directives/StarkEmailMaskDirective.html" + }, + { + label: "Stark Number Mask directive", + url: "https://stark.nbb.be/api-docs/stark-ui/latest/directives/StarkNumberMaskDirective.html" + }, { label: "Stark Text Mask directive", url: "https://stark.nbb.be/api-docs/stark-ui/latest/directives/StarkTextMaskDirective.html" + }, + { + label: "Stark Timestamp Mask directive", + url: "https://stark.nbb.be/api-docs/stark-ui/latest/directives/StarkTimestampMaskDirective.html" } ]; } @@ -45,16 +129,21 @@ export class DemoInputMaskDirectivesPageComponent implements OnInit { * Component lifecycle hook */ public ngOnInit(): void { - this.phoneNumberField.valueChanges.subscribe((value: string) => this.logger.debug("phoneNumberField value changed: ", value)); + merge( + this.phoneNumberField.valueChanges, + this.fullDateField.valueChanges, + this.timeField.valueChanges, + this.percentageField.valueChanges + ).subscribe((changedValue: string) => this.logger.debug("formControl value changed: ", changedValue)); this.logger.debug("DemoInputMaskDirectivesComponent - initialized"); } public logChange(event: Event): void { - this.logger.debug("creditCard value changed", (event.srcElement).value); + this.logger.debug("input value changed", (event.srcElement).value); } - public logModelChange(): void { - this.logger.debug("structuredMessage value changed", this.structuredMessage); + public logModelChange(model: any): void { + this.logger.debug("model value changed", model); } } diff --git a/showcase/src/assets/examples/input-mask-directives/email-mask-directive.html b/showcase/src/assets/examples/input-mask-directives/email-mask-directive.html new file mode 100644 index 0000000000..ec3f868120 --- /dev/null +++ b/showcase/src/assets/examples/input-mask-directives/email-mask-directive.html @@ -0,0 +1,14 @@ +
+ + Email + + +
diff --git a/showcase/src/assets/examples/input-mask-directives/email-mask-directive.ts b/showcase/src/assets/examples/input-mask-directives/email-mask-directive.ts new file mode 100644 index 0000000000..6d135c25a5 --- /dev/null +++ b/showcase/src/assets/examples/input-mask-directives/email-mask-directive.ts @@ -0,0 +1,17 @@ +import { Component, Inject } from "@angular/core"; +import { STARK_LOGGING_SERVICE, StarkLoggingService } from "@nationalbankbelgium/stark-core"; + +@Component({ + selector: "demo-email-mask", + styleUrls: ["./demo-email-mask.component.scss"], + templateUrl: "./demo-email-mask.component.html" +}) +export class DemoEmailMaskComponent { + public email: string; + + public constructor(@Inject(STARK_LOGGING_SERVICE) private logger: StarkLoggingService) {} + + public logModelChange(model: any): void { + this.logger.debug("model value changed", model); + } +} diff --git a/showcase/src/assets/examples/input-mask-directives/number-mask-directive.html b/showcase/src/assets/examples/input-mask-directives/number-mask-directive.html new file mode 100644 index 0000000000..de63b25225 --- /dev/null +++ b/showcase/src/assets/examples/input-mask-directives/number-mask-directive.html @@ -0,0 +1,38 @@ +
+ + Amount in Euros + + + + + Amount in US Dollars + + + + + Percentage + + +
diff --git a/showcase/src/assets/examples/input-mask-directives/number-mask-directive.ts b/showcase/src/assets/examples/input-mask-directives/number-mask-directive.ts new file mode 100644 index 0000000000..fa07c73fb8 --- /dev/null +++ b/showcase/src/assets/examples/input-mask-directives/number-mask-directive.ts @@ -0,0 +1,78 @@ +import { Component, Inject, OnInit } from "@angular/core"; +import { FormControl } from "@angular/forms"; +import { STARK_LOGGING_SERVICE, StarkLoggingService } from "@nationalbankbelgium/stark-core"; +import { StarkNumberMaskConfig } from "@nationalbankbelgium/stark-ui"; + +@Component({ + selector: "demo-number-mask", + styleUrls: ["./demo-number-mask.component.scss"], + templateUrl: "./demo-number-mask.component.html" +}) +export class DemoNumberMaskComponent implements OnInit { + public dollarsMaskConfig: StarkNumberMaskConfig; + public eurosMaskConfig: StarkNumberMaskConfig; + public percentageMaskConfig: StarkNumberMaskConfig; + + public dollarsAmount: string; + public percentageField: FormControl; + + public constructor(@Inject(STARK_LOGGING_SERVICE) private logger: StarkLoggingService) { + this.percentageField = new FormControl(); + + this.eurosMaskConfig = { + prefix: "", + suffix: " €", + includeThousandsSeparator: true, + thousandsSeparatorSymbol: ",", + allowDecimal: true, + decimalSymbol: ".", + decimalLimit: 2, + integerLimit: 9, + allowNegative: false, + allowLeadingZeroes: false + }; + + this.dollarsMaskConfig = { + prefix: "$ ", + suffix: "", + includeThousandsSeparator: true, + thousandsSeparatorSymbol: ",", + allowDecimal: true, + decimalSymbol: ".", + decimalLimit: 2, + integerLimit: 9, + allowNegative: false, + allowLeadingZeroes: false + }; + + this.percentageMaskConfig = { + prefix: "% ", + suffix: "", + includeThousandsSeparator: true, + thousandsSeparatorSymbol: ",", + allowDecimal: true, + decimalSymbol: ".", + decimalLimit: 3, + integerLimit: 3, + allowNegative: true, + allowLeadingZeroes: true + }; + } + + /** + * Component lifecycle hook + */ + public ngOnInit(): void { + this.percentageField.valueChanges.subscribe((changedValue: string) => + this.logger.debug("formControl value changed: ", changedValue) + ); + } + + public logChange(event: Event): void { + this.logger.debug("input value changed", (event.srcElement).value); + } + + public logModelChange(model: any): void { + this.logger.debug("model value changed", model); + } +} diff --git a/showcase/src/assets/examples/input-mask-directives/text-mask-directive.html b/showcase/src/assets/examples/input-mask-directives/text-mask-directive.html index 67dc4c0696..14d6421b27 100644 --- a/showcase/src/assets/examples/input-mask-directives/text-mask-directive.html +++ b/showcase/src/assets/examples/input-mask-directives/text-mask-directive.html @@ -1,6 +1,6 @@ -
+
- Credit Card + Credit Card - Structured Communication Message + Structured Communication Message - Custom Mask + Phone number
diff --git a/showcase/src/assets/examples/input-mask-directives/text-mask-directive.ts b/showcase/src/assets/examples/input-mask-directives/text-mask-directive.ts index a4fb9f34e0..0c83f2f100 100644 --- a/showcase/src/assets/examples/input-mask-directives/text-mask-directive.ts +++ b/showcase/src/assets/examples/input-mask-directives/text-mask-directive.ts @@ -11,7 +11,7 @@ import { StarkTextMasks, StarkTextMaskConfig } from "@nationalbankbelgium/stark- export class DemoTextMaskComponent implements OnInit { public creditCardMaskConfig: StarkTextMaskConfig; public structuredMessageMaskConfig: StarkTextMaskConfig; - public customMaskConfig: StarkTextMaskConfig; + public phoneNumberMaskConfig: StarkTextMaskConfig; public structuredMessage: string; public phoneNumberField: FormControl; @@ -26,7 +26,7 @@ export class DemoTextMaskComponent implements OnInit { mask: StarkTextMasks.STRUCTURED_COMMUNICATION_NUMBER }; - this.customMaskConfig = { + this.phoneNumberMaskConfig = { mask: ["(", "+", "3", "2", ")", " ", /\d/, /\d/, /\d/, " ", /\d/, /\d/, " ", /\d/, /\d/, " ", /\d/, /\d/], placeholderChar: "#" }; @@ -36,14 +36,16 @@ export class DemoTextMaskComponent implements OnInit { * Component lifecycle hook */ public ngOnInit(): void { - this.phoneNumberField.valueChanges.subscribe((value: string) => this.logger.debug("phoneNumberField value changed: ", value)); + this.phoneNumberField.valueChanges.subscribe((changedValue: string) => + this.logger.debug("formControl value changed: ", changedValue) + ); } public logChange(event: Event): void { - this.logger.debug("creditCard value changed", (event.srcElement).value); + this.logger.debug("input value changed", (event.srcElement).value); } - public logModelChange(): void { - this.logger.debug("structuredMessage value changed", this.structuredMessage); + public logModelChange(model: any): void { + this.logger.debug("model value changed", model); } } diff --git a/showcase/src/assets/examples/input-mask-directives/timestamp-mask-directive.html b/showcase/src/assets/examples/input-mask-directives/timestamp-mask-directive.html new file mode 100644 index 0000000000..4d852f92a6 --- /dev/null +++ b/showcase/src/assets/examples/input-mask-directives/timestamp-mask-directive.html @@ -0,0 +1,75 @@ +
+ + Timestamp in DMY format + + + + + Timestamp in MDY format + + + + + Date in DMY format + + + + + Day-Month + + + + + Month / Day + + + + + Time + + +
diff --git a/showcase/src/assets/examples/input-mask-directives/timestamp-mask-directive.ts b/showcase/src/assets/examples/input-mask-directives/timestamp-mask-directive.ts new file mode 100644 index 0000000000..da33466b25 --- /dev/null +++ b/showcase/src/assets/examples/input-mask-directives/timestamp-mask-directive.ts @@ -0,0 +1,58 @@ +import { Component, Inject, OnInit } from "@angular/core"; +import { FormControl } from "@angular/forms"; +import { STARK_LOGGING_SERVICE, StarkLoggingService } from "@nationalbankbelgium/stark-core"; +import { StarkTimestampMaskConfig } from "@nationalbankbelgium/stark-ui"; +import { merge } from "rxjs"; + +@Component({ + selector: "demo-timestamp-mask", + styleUrls: ["./demo-timestamp-mask.component.scss"], + templateUrl: "./demo-timestamp-mask.component.html" +}) +export class DemoTimestampMaskComponent implements OnInit { + public dateMaskConfig: StarkTimestampMaskConfig; + public timestampDMYMaskConfig: StarkTimestampMaskConfig; + public dayMonthMaskConfig: StarkTimestampMaskConfig; + public monthDayMaskConfig: StarkTimestampMaskConfig; + public timestampMDYMaskConfig: StarkTimestampMaskConfig; + public timeMaskConfig: StarkTimestampMaskConfig; + + public timestamp: string; + public monthDay: string; + public fullDateField: FormControl; + public timeField: FormControl; + + public constructor(@Inject(STARK_LOGGING_SERVICE) private logger: StarkLoggingService) { + this.fullDateField = new FormControl(); + this.timeField = new FormControl(); + + this.timestampDMYMaskConfig = { format: "DD-MM-YYYY HH:mm:ss" }; + + this.timestampMDYMaskConfig = { format: "MM-DD-YYYY HH:mm:ss" }; + + this.dateMaskConfig = { format: "DD-MM-YYYY" }; + + this.dayMonthMaskConfig = { format: "DD-MM" }; + + this.monthDayMaskConfig = { format: "MM/DD" }; + + this.timeMaskConfig = { format: "HH:mm:ss" }; + } + + /** + * Component lifecycle hook + */ + public ngOnInit(): void { + merge(this.fullDateField.valueChanges, this.timeField.valueChanges).subscribe((changedValue: string) => + this.logger.debug("formControl value changed: ", changedValue) + ); + } + + public logChange(event: Event): void { + this.logger.debug("input value changed", (event.srcElement).value); + } + + public logModelChange(model: any): void { + this.logger.debug("model value changed", model); + } +} diff --git a/showcase/src/assets/translations/en.json b/showcase/src/assets/translations/en.json index f96531122b..58b00e1137 100644 --- a/showcase/src/assets/translations/en.json +++ b/showcase/src/assets/translations/en.json @@ -96,11 +96,37 @@ }, "DIRECTIVES": { "INPUT_MASK": { + "EMAIL_MASK": { + "TITLE": "Email Mask directive", + "EMAIL_MASK": "Email", + "EMAIL_PLACEHOLDER": "Type an email address" + }, + "NUMBER_MASK": { + "TITLE": "Number Mask directive", + "US_DOLLARS_AMOUNT_MASK": "Amount in US Dollars ", + "EURO_AMOUNT_MASK": "Amount in Euros", + "PERCENTAGE_MASK": "Percentage" + }, "TEXT_MASK": { "TITLE": "Text Mask directive", - "CREDIT_CARD_MASK": "Credit Card", - "STRUCTURED_MESSAGE_MASK": "Structured Communication Message", - "CUSTOM_MASK": "Custom Mask" + "CREDIT_CARD_MASK": "Credit card", + "STRUCTURED_MESSAGE_MASK": "Structured communication message", + "PHONE_NUMBER_MASK": "Phone number" + }, + "TIMESTAMP_MASK": { + "TITLE": "Timestamp Mask directive", + "TIMESTAMP_DMY_MASK": "Timestamp in DMY format", + "TIMESTAMP_DMY_PLACEHOLDER": "DD-MM-YYYY HH:mm:ss", + "TIMESTAMP_MDY_MASK": "Timestamp in MDY format", + "TIMESTAMP_MDY_PLACEHOLDER": "MM-DD-YYYY HH:mm:ss", + "FULL_DATE_DMY_MASK": "Date in DMY format", + "FULL_DATE_DMY_PLACEHOLDER": "DD-MM-YYYY", + "DAY_MONTH_MASK": "Day-Month", + "DAY_MONTH_PLACEHOLDER": "DD-MM", + "MONTH_DAY_MASK": "Month / Day", + "MONTH_DAY_PLACEHOLDER": "MM/DD", + "TIME_MASK": "Time", + "TIME_PLACEHOLDER": "HH:mm:ss" }, "TITLE": "Input Mask directives" }, diff --git a/showcase/src/assets/translations/fr.json b/showcase/src/assets/translations/fr.json index 2eb1ebf4d0..b2a896eeeb 100644 --- a/showcase/src/assets/translations/fr.json +++ b/showcase/src/assets/translations/fr.json @@ -96,11 +96,37 @@ }, "DIRECTIVES": { "INPUT_MASK": { + "EMAIL_MASK": { + "TITLE": "Email Mask directive", + "EMAIL_MASK": "Email", + "EMAIL_PLACEHOLDER": "Tapez une adresse email" + }, + "NUMBER_MASK": { + "TITLE": "Number Mask directive", + "US_DOLLARS_AMOUNT_MASK": "Montant en US Dollars ", + "EURO_AMOUNT_MASK": "Montant en Euros", + "PERCENTAGE_MASK": "Percentage" + }, "TEXT_MASK": { "TITLE": "Text Mask directive", "CREDIT_CARD_MASK": "Carte de crédit", "STRUCTURED_MESSAGE_MASK": "Message de communication structurée", - "CUSTOM_MASK": "Masque personnalisé" + "PHONE_NUMBER_MASK": "Numéro de téléphone" + }, + "TIMESTAMP_MASK": { + "TITLE": "Timestamp Mask directive", + "TIMESTAMP_DMY_MASK": "Horodatage au format JMA", + "TIMESTAMP_DMY_PLACEHOLDER": "DD-MM-YYYY HH:mm:ss", + "TIMESTAMP_MDY_MASK": "Horodatage au format MJA", + "TIMESTAMP_MDY_PLACEHOLDER": "MM-DD-YYYY HH:mm:ss", + "FULL_DATE_DMY_MASK": "Date au format JMA", + "FULL_DATE_DMY_PLACEHOLDER": "DD-MM-YYYY", + "DAY_MONTH_MASK": "Jour-Mois", + "DAY_MONTH_PLACEHOLDER": "DD-MM", + "MONTH_DAY_MASK": "Mois / Jour", + "MONTH_DAY_PLACEHOLDER": "MM/DD", + "TIME_MASK": "Temps", + "TIME_PLACEHOLDER": "HH:mm:ss" }, "TITLE": "Input Mask directives" }, diff --git a/showcase/src/assets/translations/nl.json b/showcase/src/assets/translations/nl.json index 956f8d8041..e04ae9d56a 100644 --- a/showcase/src/assets/translations/nl.json +++ b/showcase/src/assets/translations/nl.json @@ -96,11 +96,37 @@ }, "DIRECTIVES": { "INPUT_MASK": { + "EMAIL_MASK": { + "TITLE": "Email Mask directive", + "EMAIL_MASK": "E-mail", + "EMAIL_PLACEHOLDER": "Typ een e-mailadres" + }, + "NUMBER_MASK": { + "TITLE": "Number Mask directive", + "US_DOLLARS_AMOUNT_MASK": "Bedrag in US Dollars ", + "EURO_AMOUNT_MASK": "Bedrag in Euros", + "PERCENTAGE_MASK": "Percentage" + }, "TEXT_MASK": { "TITLE": "Text Mask directive", "CREDIT_CARD_MASK": "Kredietkaart", "STRUCTURED_MESSAGE_MASK": "Gestructureerde mededeling", - "CUSTOM_MASK": "Aangepast masker" + "PHONE_NUMBER_MASK": "Telefoonnummer" + }, + "TIMESTAMP_MASK": { + "TITLE": "Timestamp Mask directive", + "TIMESTAMP_DMY_MASK": "Timestamp in DMJ format", + "TIMESTAMP_DMY_PLACEHOLDER": "DD-MM-YYYY HH:mm:ss", + "TIMESTAMP_MDY_MASK": "Timestamp in MDJ format", + "TIMESTAMP_MDY_PLACEHOLDER": "MM-DD-YYYY HH:mm:ss", + "FULL_DATE_DMY_MASK": "Datum in DMJ format", + "FULL_DATE_DMY_PLACEHOLDER": "DD-MM-YYYY", + "DAY_MONTH_MASK": "Dag-Maand", + "DAY_MONTH_PLACEHOLDER": "DD-MM", + "MONTH_DAY_MASK": "Maand / Dag", + "MONTH_DAY_PLACEHOLDER": "MM/DD", + "TIME_MASK": "Tijd", + "TIME_PLACEHOLDER": "HH:mm:ss" }, "TITLE": "Input Mask directives" },