UNPKG

moh-common-lib

Version:

A library of Angular components, services, and styles for B.C. Government Ministry of Health (MoH).

1,116 lines (1,101 loc) 355 kB
import { CommonModule } from '@angular/common'; import * as zxcvbn_ from 'zxcvbn'; import { ProgressbarModule } from 'ngx-bootstrap/progressbar'; import { TypeaheadModule } from 'ngx-bootstrap/typeahead'; import getDaysInMonth from 'date-fns/getDaysInMonth'; import isAfter from 'date-fns/isAfter'; import isBefore from 'date-fns/isBefore'; import startOfToday from 'date-fns/startOfToday'; import addYears from 'date-fns/addYears'; import subYears from 'date-fns/subYears'; import { NgxMyDatePickerModule } from 'ngx-mydatepicker'; import * as PDFJS_ from 'pdfjs-dist/legacy/build/pdf'; import { pdfJsWorker } from 'pdfjs-dist/legacy/build/pdf.worker.entry'; import { TextMaskModule } from 'angular2-text-mask'; import { NgSelectModule } from '@ng-select/ng-select'; import { UUID } from 'angular2-uuid'; import * as moment_ from 'moment'; import { AccordionModule } from 'ngx-bootstrap/accordion'; import { compareAsc, startOfDay, addDays, format, startOfToday as startOfToday$1, differenceInYears } from 'date-fns'; import { ModalModule } from 'ngx-bootstrap/modal'; import { filter, map, catchError, tap, debounceTime, distinctUntilChanged, switchMap } from 'rxjs/operators'; import { HttpClient, HttpHeaders, HttpParams } from '@angular/common/http'; import { __awaiter } from 'tslib'; import { throwError, Subject, of, BehaviorSubject } from 'rxjs'; import { ControlContainer, NgForm, NgControl, NG_VALIDATORS, NG_VALUE_ACCESSOR, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { Router, NavigationEnd, RouterModule } from '@angular/router'; import { Component, Input, Output, EventEmitter, forwardRef, ViewEncapsulation, HostListener, ViewChild, ViewChildren, ChangeDetectionStrategy, ChangeDetectorRef, Optional, Self, Inject, NgZone, ViewContainerRef, Injectable, Directive, NgModule, InjectionToken, Injector, defineInjectable, inject, INJECTOR } from '@angular/core'; /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ /** * * The base styles for a breadcrumb with slots for content to go. If you need a * complex breadcrumb, the idea is you can extend this CoreBreadcrumb and use * the base styles. There are 3 slots: left, center, and right - all are * optional and any configuration works. You select the slot by adding it as an * attribute to the top level elements inside of the breadcrumb element. * * Example usage: * * <common-core-breadcrumb> * <div left> * <a routerLink="/provisioner/">Dashboard</a> / * <strong>Provision by {{ IS_SHOWING_PERSON ? "User" : "Site" }}</strong> * </div> * <div center></div> * <div right></div> * </common-core-breadcrumb> * * @export */ class CoreBreadcrumbComponent { constructor() { } } CoreBreadcrumbComponent.decorators = [ { type: Component, args: [{ selector: 'common-core-breadcrumb', template: "<nav class=\"breadcrumb d-flex justify-content-between horizontal-scroll\" aria-label=\"breadcrumb\">\r\n <ng-content select=\"[left]\"></ng-content>\r\n <ng-content class='d-flex' select=\"[center]\"></ng-content>\r\n <ng-content select=\"[right]\"></ng-content>\r\n</nav>\r\n", styles: [".breadcrumb{display:flex;justify-content:space-between;background-color:transparent}@media (min-width:992px){.breadcrumb{flex-wrap:nowrap}}.horizontal-scroll{overflow-x:scroll;overflow-y:hidden}.horizontal-scroll::-webkit-scrollbar{background:0 0;height:5px}.horizontal-scroll::-webkit-scrollbar-thumb{background:#5475a7;border-radius:10px}"] }] } ]; /** @nocollapse */ CoreBreadcrumbComponent.ctorParameters = () => []; /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ /** * @param {?=} top * @return {?} */ function scrollTo(top = 0) { /** @type {?} */ const supportsNativeSmoothScroll = 'scrollBehavior' in document.documentElement.style; if (supportsNativeSmoothScroll) { window.scrollTo({ top: top, behavior: 'smooth' }); } else { /** * IE does not support ScrollToOptions (behavior, top, left), but does support * scrollTo() with parameters for x and y coorindiates. */ window.scrollTo(0, top); } } /** * Get the first visible common-error-container and smoothly scroll to it. * @param {?=} yOffset * @return {?} */ function scrollToError(yOffset = -75) { /** @type {?} */ const el = document.querySelector('common-error-container .text-danger'); if (el) { /** @type {?} */ const top = el.getBoundingClientRect().top + window.pageYOffset + yOffset; scrollTo(top); } } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ class FormActionBarComponent { constructor() { this.submitLabel = 'Continue'; this.canContinue = true; this.isLoading = false; this.defaultColor = true; this.btnClick = new EventEmitter(); this.scrollToErrorsOnSubmit = true; } /** * @return {?} */ ngOnInit() { } /** * @param {?} $event * @return {?} */ onClick($event) { if (!this.isLoading && this.canContinue) { this.btnClick.emit($event); if (this.scrollToErrorsOnSubmit) { // Scroll to error after 50ms, give time for errors to display etc. // This timeout is outside of Angular change detection. setTimeout(scrollToError, 50); } } $event.stopPropagation(); return false; } } FormActionBarComponent.decorators = [ { type: Component, args: [{ selector: 'common-form-action-bar', template: "<div class=\"form-action-bar form-bar\" [ngClass]=\"{disabled: !canContinue}\">\r\n <button class=\"btn btn-lg {{defaultColor ? 'btn-primary' : 'btn-secondary' }} submit\"\r\n (click)=\"onClick($event)\"\r\n [ngClass]=\"{disabled: !canContinue || isLoading}\">\r\n <ng-container *ngIf=\"!isLoading; else loadingSpinner\"> {{submitLabel}} </ng-container>\r\n \r\n </button>\r\n</div>\r\n\r\n<ng-template #loadingSpinner>\r\n <i class=\"fa fa-spinner fa-pulse fa-fw\"></i>\r\n</ng-template>", // TODO: Figure out why this is required. viewProviders: [{ provide: ControlContainer, useExisting: forwardRef((/** * @return {?} */ () => NgForm)) }], styles: [".form-action-bar{display:flex;justify-content:flex-end;padding:1em;background-color:#cdd9e4;transition:background-color .3s;position:-webkit-sticky;position:-moz-sticky;position:-ms-sticky;position:-o-sticky;position:sticky;bottom:0;z-index:10;left:5rem;right:5rem}.form-action-bar.disabled{background-color:#d5d9dd}@media (max-width:991.98px){.form-action-bar{left:2.5rem;right:2.5rem}}.submit{min-width:240px}@media (max-width:767.98px){.form-action-bar{width:calc(100% + 30px);margin-left:-15px}.submit{min-width:100%}}.btn{white-space:normal!important;word-wrap:break-word}.btn-secondary.disabled{color:#000}"] }] } ]; /** @nocollapse */ FormActionBarComponent.ctorParameters = () => []; FormActionBarComponent.propDecorators = { submitLabel: [{ type: Input }], canContinue: [{ type: Input }], isLoading: [{ type: Input }], defaultColor: [{ type: Input }], btnClick: [{ type: Output }], scrollToErrorsOnSubmit: [{ type: Input }] }; /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ /** * The "Page Framework" is a template to be used on FPCare pages to ensure * consistent layout. It should be used on every page unless there is a good * exception. * * Note: CommonFormActionBar (and SubmitBar) must come *AFTER* this element, not * inside in version 2.0.0 and above. * * \@example * <common-page-framework> * <div>This will go in the middle column</div> * <p>So will this</p> * <div aside> This will go in the side column, or tips.</div> * </common-page-framework> * <common-form-action-bar></common-form-action-bar> * * @export */ class PageFrameworkComponent { constructor() { this.layout = 'default'; } /** * @return {?} */ ngOnInit() { } } PageFrameworkComponent.decorators = [ { type: Component, args: [{ selector: 'common-page-framework', template: "<div class=\"row\" [ngSwitch]=\"layout\">\r\n\r\n <ng-container *ngSwitchCase=\"'default'\">\r\n <div class=\"col-md-8\">\r\n <div class=\"px-lg-4 px-md-3 py-3\">\r\n <ng-container *ngTemplateOutlet=\"content\"></ng-container>\r\n </div>\r\n </div>\r\n <div class=\"col-md-4 aside-col\">\r\n <div class='pr-lg-5 pr-md-4 py-2'>\r\n <ng-container *ngTemplateOutlet=\"aside\"></ng-container>\r\n </div>\r\n </div>\r\n </ng-container>\r\n\r\n\r\n <ng-container *ngSwitchCase=\"'double'\">\r\n <div class=\"col-md-6\">\r\n <div class=\"px-lg-5 px-md-3 py-3\">\r\n <ng-container *ngTemplateOutlet=\"content\"></ng-container>\r\n </div>\r\n </div>\r\n <div class=\"col-md-6\">\r\n <div class='px-lg-5 px-md-3 py-3'>\r\n <ng-container *ngTemplateOutlet=\"aside\"></ng-container>\r\n </div>\r\n </div>\r\n </ng-container>\r\n\r\n <ng-container *ngSwitchCase=\"'single'\">\r\n <div class=\"col-md-8 offset-lg-2\">\r\n <div class=\"px-lg-5 px-md-3 py-3\">\r\n <ng-container *ngTemplateOutlet=\"content\"></ng-container>\r\n </div>\r\n </div>\r\n </ng-container>\r\n\r\n <ng-container *ngSwitchCase=\"'blank'\">\r\n <div class=\"col-sm-12\">\r\n <div class=\"px-lg-5 px-md-3 py-3\">\r\n <ng-container *ngTemplateOutlet=\"content\"></ng-container>\r\n </div>\r\n </div>\r\n </ng-container>\r\n</div>\r\n<ng-container *ngTemplateOutlet=\"submit\"></ng-container>\r\n\r\n\r\n<!-- We use ng-template here to get around a bug with having multiple ng-contents in one template. By default, if there are duplicate ng-contents in a template Angular will select the very first one - even if latter ones are 'removed' by ngSwitch or ngIf.-->\r\n<ng-template #content>\r\n <ng-content></ng-content>\r\n</ng-template>\r\n\r\n<ng-template #aside>\r\n <ng-content select='aside'></ng-content>\r\n</ng-template>\r\n\r\n<ng-template #submit>\r\n <ng-content select='common-form-action-bar'></ng-content>\r\n <ng-content select='common-form-submit-bar'></ng-content>\r\n</ng-template>\r\n", encapsulation: ViewEncapsulation.None, styles: ["common-page-framework{background:#fcfcfc;display:block;padding-left:30px;padding-right:30px;min-height:calc(100vh - 280px)}common-page-framework common-form-action-bar>.form-bar,common-page-framework common-form-submit-bar>.form-bar{background-color:red!important}common-page-framework common-form-action-bar .btn,common-page-framework common-form-submit-bar .btn{font-size:0}common-page-framework common-form-action-bar .btn:after,common-page-framework common-form-submit-bar .btn:after{font-size:1rem;content:\"FORM BAR MUST BE PLACED OUTSIDE OF COMMON-PAGE-FRAMEWORK\"}.h5,h5{font-size:1rem;font-weight:700;margin-bottom:0}p+.h5,p+h5{margin-top:1.5rem}.aside-col aside{background:#f2f2f2;padding:1em;border-radius:5px}@media (min-width:768px){.aside-col aside{margin-top:1rem}}"] }] } ]; PageFrameworkComponent.propDecorators = { layout: [{ type: Input }] }; /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ /** * Base class. Components extend this class to have object IDs. * NPM package dependencies: * a) moment */ class Base { constructor() { /** * An identifier for parents to keep track of components */ this.objectId = UUID.UUID(); } } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ // Awkward necessary workaround due to bug in build tools // https://github.com/jvandemo/generator-angular2-library/issues/221#issuecomment-355945207 /** @type {?} */ const zxcvbn = zxcvbn_; /** * PasswordComponent is a text input for a user's password. It includes: * * - A password strength bar * - Minimum length validations * * Note - if your application has requirements to check things like username is not * present in password, we recommend doing this in the (passwordChange) callback. * * \@example * <common-password componentLabel="{{newPwdLabel}}" * [showPasswordStrength]="true" * [minLen]="pwdMinLen" * [pwdCriteria]="pwdValidChars" * [password]="data.password" * (passwordChange)="setNewPassword($event)"></common-password> * * @export */ class PasswordComponent extends Base { constructor() { super(); // Inputs for the component this.label = 'Password'; this.isRequired = true; this.isDisabled = false; this.minLen = '8'; this.maxLen = '32'; this.showPasswordStrength = false; this.objectID = 'password_' + this.objectId; // Output from the component this.passwordChange = new EventEmitter(); this.blurEvent = new EventEmitter(); // Flag for the fa-eye to show or hide password this.hideValue = true; this.strengthPercentage = 0; // default messages this.requiredMsgSeg = ' is required.'; this.minLenMsgSeg1 = ' must be at least '; this.minLenMsgSeg2 = ' characters in length.'; this.criteriaMsg = ' contains invalid characters.'; } /** * @return {?} */ ngOnInit() { // Set default messages this.errMsg = { required: this.label + this.requiredMsgSeg, minLength: this.label + this.minLenMsgSeg1 + this.minLen + this.minLenMsgSeg2, criteria: this.label + this.criteriaMsg }; // Replace default message if provided if (this.errorMessages) { if (this.errorMessages.required) { this.errMsg.required = this.errorMessages.required; } if (this.errorMessages.minLength) { this.errMsg.minLength = this.errorMessages.minLength; } if (this.errorMessages.criteria) { this.errMsg.criteria = this.errorMessages.criteria; } } } /** * @param {?} changes * @return {?} */ ngOnChanges(changes) { if (changes.password && this.password) { // Check strength of password this.pswdStrength = this.getPasswordStrength(this.password); this.strengthPercentage = ((this.pswdStrength + 1) / 5) * 100; } } /** * Passes the value entered back to the calling component * @param {?} password value the was entered by * @return {?} */ setPassword(password) { this.passwordChange.emit(password); } /** * @param {?} $event * @return {?} */ onInputBlur($event) { this.blurEvent.emit(event); } // Prevent user from pasting data into the text box /** * @param {?} event * @return {?} */ onPaste(event) { return false; } /** * Get the strength of the password * 0 = too guessable: risky password. (guesses < 10^3) * 1 = very guessable: protection from throttled online attacks. (guesses < 10^6) * 2 = somewhat guessable: protection from unthrottled online attacks. (guesses < 10^8) * 3 = safely unguessable: moderate protection from offline slow-hash scenario. (guesses < 10^10) * 4 = very unguessable: strong protection from offline slow-hash scenario. (guesses >= 10^10) * * https://github.com/dropbox/zxcvbn * @private * @param {?} password * @return {?} */ getPasswordStrength(password) { // Password strength feedback /** @type {?} */ const pswdFeedback = zxcvbn(password); return pswdFeedback.score; } } PasswordComponent.decorators = [ { type: Component, args: [{ selector: 'common-password', template: "<label class=\"control-label\" for=\"{{objectID}}\">{{label}}</label>\r\n<input #pswdRef=\"ngModel\"\r\n type=\"{{hideValue? 'password': 'text'}}\"\r\n class=\"form-control password-field\"\r\n name=\"{{objectID}}\"\r\n id=\"{{objectID}}\"\r\n [ngModel]=\"password\"\r\n (ngModelChange)=\"setPassword($event)\"\r\n (blur)=\"onInputBlur($event)\"\r\n [pattern]=\"pwdCriteria\"\r\n [required]=\"isRequired\"\r\n [minlength]=\"minLen\"\r\n [maxlength]=\"maxLen\"\r\n [disabled]=\"isDisabled\"\r\n autocomplete=\"off\"/>\r\n<span class=\"fa fa-fw {{hideValue? 'fa-eye' : 'fa-eye-slash'}} password-field-icon\"\r\n (click)='hideValue = !hideValue'></span>\r\n<div *ngIf='password && showPasswordStrength'>\r\n<!-- The progress bar -->\r\n <div class=\"progress password-strength-bar\" >\r\n <div class=\"progress-bar {{pswdStrength >= 4? 'bg-success' : (pswdStrength >= 3? 'bg-warning' : 'bg-danger')}}\"\r\n role=\"progressbar\"\r\n [style.width]='strengthPercentage + \"%\"'\r\n [attr.aria-valuenow]=\"strengthPercentage\"\r\n aria-valuemin=\"0\"\r\n aria-valuemax=\"100\">\r\n </div>\r\n </div>\r\n <span class=\"password-progress-label {{pswdStrength >= 4? 'text-success' : (pswdStrength >= 3? 'text-warning' : 'text-danger')}}\"></span>\r\n</div>\r\n<!-- Error messages for component -->\r\n<div *ngIf=\"!pswdRef.disabled && (pswdRef.touched || pswdRef.dirty)\"\r\n role=\"alert\"\r\n class='error-container'\r\n aria-live=\"assertive\">\r\n <div class=\"text-danger\" *ngIf=\"pswdRef?.errors?.required\">\r\n {{errMsg.required}}\r\n </div>\r\n <div class=\"text-danger\" *ngIf=\"pswdRef?.errors?.minlength\">\r\n {{errMsg.minLength}}\r\n </div>\r\n <div class=\"text-danger\" *ngIf=\"pswdRef?.errors?.pattern && !pswdRef?.errors?.minlength\">\r\n {{errMsg.criteria}}\r\n </div>\r\n</div>\r\n\r\n", /* Re-use the same ngForm that it's parent is using. The component will show * up in its parents `this.form`, and will auto-update `this.form.valid` */ viewProviders: [{ provide: ControlContainer, useExisting: forwardRef((/** * @return {?} */ () => NgForm)) }], styles: [".password-field-icon{float:right;margin-left:-25px;margin-top:-25px;margin-right:.5rem;position:relative;z-index:2}.password-field{padding-right:2rem}.password-strength-bar{min-width:0;max-width:75%;height:.5rem;margin-top:1em}.password-progress-label{float:right;margin-left:-15px;margin-top:-15px;position:relative;z-index:2}.password-strength-bar+.text-success::after{content:'Good'}.password-strength-bar+.text-warning::after{content:'OK'}.password-strength-bar+.text-danger::after{content:'Bad'}input{width:100%}input[type=password]::-ms-clear,input[type=password]::-ms-reveal{display:none}"] }] } ]; /** @nocollapse */ PasswordComponent.ctorParameters = () => []; PasswordComponent.propDecorators = { label: [{ type: Input }], isRequired: [{ type: Input }], isDisabled: [{ type: Input }], password: [{ type: Input }], pwdCriteria: [{ type: Input }], minLen: [{ type: Input }], maxLen: [{ type: Input }], errorMessages: [{ type: Input }], showPasswordStrength: [{ type: Input }], objectID: [{ type: Input }], passwordChange: [{ type: Output }], blurEvent: [{ type: Output }], onPaste: [{ type: HostListener, args: ['document:paste', ['$event'],] }] }; /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ /** * NPM Dependencies: * a) rxjs * b) ngx-bootstrap */ class WizardProgressBarComponent { /** * @param {?} router * @param {?} cd */ constructor(router, cd) { this.router = router; this.cd = cd; this.progressSteps = []; } /** * @return {?} */ ngOnInit() { // Update the progress bar view on route change and _only_ route chaange. // Skip most of Angular's ChangeDetection in favour of manually optimizing. this.routerEvents$ = this.router.events.pipe(filter((/** * @param {?} ev * @return {?} */ ev => ev instanceof NavigationEnd)), map((/** * @param {?} ev * @return {?} */ (ev) => ev.url))).subscribe((/** * @param {?} url * @return {?} */ url => { this.activeIndex = this.getActiveIndex(url); this.cd.detectChanges(); this.scrollStepIntoView(); })); // Must schedule first run manually, or bar won't be set. this.activeIndex = this.getActiveIndex(this.router.url); } /** * @return {?} */ ngOnDestroy() { this.cd.detach(); this.routerEvents$.unsubscribe(); } /** * @return {?} */ calculateProgressPercentage() { /** @type {?} */ const denominator = this.progressSteps.length; /** @type {?} */ const numerator = this.activeIndex + 1; if (denominator === 0 || numerator > denominator) { return 100; } // Because we've switched from space-evenly to space-around (for IE), we // have to handle the half-space that space-around adds to the start/end of // the container /** @type {?} */ const halfSpace = 1 / (denominator * 2); return Math.round(((numerator / denominator) - halfSpace) * 100); } /** * @param {?} url * @return {?} */ getActiveIndex(url) { return this.progressSteps.findIndex((/** * @param {?} x * @return {?} */ x => url.endsWith(x.route))); } /** * Primarily for mobile, this horizontally scrolls the step into view. * * Note - be very careful with any changes to this function because it steps * outside of Angular to call native browser functions. * @private * @return {?} */ scrollStepIntoView() { /** @type {?} */ const target = this.steps.toArray()[this.activeIndex]; /** @type {?} */ const container = document.getElementsByClassName('horizontal-scroll'); if (container.length === 1) { // Since we're already breaking out of Angular, we try and be safe by using a try/catch. // Otherwise an error here could halt execution, try { container[0].scrollLeft = Math.abs(target.nativeElement.offsetLeft - (window.outerWidth / 2)); } catch (error) { } } } } WizardProgressBarComponent.decorators = [ { type: Component, args: [{ selector: 'common-wizard-progress-bar', template: "<progressbar\r\n [value]=\"calculateProgressPercentage()\"\r\n [max]=\"100\"\r\n [animate]=\"true\">\r\n</progressbar>\r\n\r\n<div class=\"step-container\" #stepContainer>\r\n\r\n <a #steps *ngFor=\"let step of progressSteps; let i = index;\" [routerLink]=\"step.route\">\r\n\r\n <div class=\"step\" [ngClass]=\"{active: i === activeIndex}\">\r\n <span class=\"step-text\">{{step.title}}</span>\r\n </div>\r\n\r\n </a>\r\n\r\n</div>\r\n", changeDetection: ChangeDetectionStrategy.OnPush, styles: [":host{flex:1;padding:1em 2em;min-height:2em;min-width:650px}.step-container{display:flex;justify-content:space-around}progressbar{background-color:#adb5bd;height:.5rem}.step{position:relative;-webkit-transform:translateX(-.5em);transform:translateX(-.5em);margin-top:.25rem}.step:before{content:\" \";position:absolute;width:1em;height:1em;border-radius:100%;background:#fff;border:3px solid #036;right:0;left:0;margin:0 auto;bottom:100%}.step:not(.active) .step-text{opacity:.8}.step:not(.active):before{background:#ced4da}.step .step-text{position:absolute;-webkit-transform:translateX(-33%);transform:translateX(-33%);white-space:nowrap;font-size:small;color:#164d80}"] }] } ]; /** @nocollapse */ WizardProgressBarComponent.ctorParameters = () => [ { type: Router }, { type: ChangeDetectorRef } ]; WizardProgressBarComponent.propDecorators = { progressSteps: [{ type: Input }], stepContainer: [{ type: ViewChild, args: ['stepContainer',] }], steps: [{ type: ViewChildren, args: ['steps',] }] }; /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ /** * In most cases this will be the part of the error message that will appear after the field label. * * {{label}} is required. * * Date component will subsitute '{label}' for the label in the component. * Ex. { required: '{label} is required.' } * * Note: '{label}' is exported in constant 'LabelReplacementTag'. * @type {?} */ const LabelReplacementTag = '{label}'; // To catch all occurances of the label tag in the message /** @type {?} */ const regExpLabel = new RegExp(LabelReplacementTag, 'g'); // Function only used with library /** * @param {?} str * @param {?} value * @return {?} */ function replaceLabelTag(str, value) { return str.replace(regExpLabel, value); } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ class MoHCommonLibraryError extends Error { /** * @param {?=} message */ constructor(message) { super(message); // 'Error' breaks prototype chain here Object.setPrototypeOf(this, new.target.prototype); // restore prototype chain } } /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ // Class does not get exported - used internally /** * @abstract */ class AbstractFormControl extends Base { constructor() { super(...arguments); // Default messages - must be defined in each component this._defaultErrMsg = {}; this.disabled = false; // Required for implementing ControlValueAccessor this._onChange = (/** * @param {?} _ * @return {?} */ (_) => { }); this._onTouched = (/** * @param {?=} _ * @return {?} */ (_) => { }); } /** * @return {?} */ ngOnInit() { this.setErrorMsg(); } // Register change function /** * @param {?} fn * @return {?} */ registerOnChange(fn) { this._onChange = fn; } // Register touched function /** * @param {?} fn * @return {?} */ registerOnTouched(fn) { this._onTouched = fn; } // Disable control /** * @param {?} isDisabled * @return {?} */ setDisabledState(isDisabled) { this.disabled = isDisabled; } /** * @protected * @return {?} */ setErrorMsg() { this.validateLabel(); // Some components have logic based off no label being submitted - strip off '(optional)' /** @type {?} */ const _label = this.label ? this.label.replace(/\s*\(.*?\)\s*/g, '') : 'Field'; if (this.errorMessage) { Object.keys(this.errorMessage).map((/** * @param {?} x * @return {?} */ x => this._defaultErrMsg[x] = this.errorMessage[x])); } // Replace label tags with label Object.keys(this._defaultErrMsg).map((/** * @param {?} x * @return {?} */ x => this._defaultErrMsg[x] = replaceLabelTag(this._defaultErrMsg[x], _label))); } /** * Register self validating method * @protected * @param {?} ngControl * @param {?} fn function for validating self * @return {?} */ registerValidation(ngControl, fn) { // Register validateSelf validator so that it will be added on component initialization. // Makes the component a self validating component. return Promise.resolve().then((/** * @return {?} */ () => { if (ngControl) { /** @type {?} */ const allValidators = [fn.bind(this)]; if (ngControl.control.validator) { allValidators.push(ngControl.control.validator); } ngControl.control.setValidators(allValidators); ngControl.control.updateValueAndValidity(); return ngControl; } })); } /** * @private * @return {?} */ validateLabel() { /** @type {?} */ const labelType = typeof this.label; // If labelType is falsy, do not throw the error, since we have a backup label in case it's falsy. if (labelType !== 'string' && this.label) { /** @type {?} */ const typeMsg = `<AbstractFormControl> Invalid input provided to [label]. Label must be a string and you provided a ${labelType}`; throw new MoHCommonLibraryError(typeMsg); } } } AbstractFormControl.propDecorators = { label: [{ type: Input }], disabled: [{ type: Input }], errorMessage: [{ type: Input }] }; /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ /** * DateComponent * You cannot use "[restrictDate]" in combination with either "[dateRangeEnd]" or "[dateRangeStart]". * You must use either [restrictDate] or the [dateRange*] inputs. * * \@example * To trigger 'no future dates allowed' using date ranges set the range end date to yesterday's date. * <common-date name='effectiveDate' * label="Effective Date" * [dateRangeEnd]='yesterday' * formControlName="effectiveDate"></common-date> * ' * To trigger 'no past dates allowed' using date ranges set the range start date to today's date. * <common-date name='effectiveDate' * label="Effective Date" * [dateRangeStart]="today" * formControlName="effectiveDate"></common-date> * * To allow instructions under label. * <common-date name='effectiveDate' * label="Effective Date" * [dateRangeEnd]='yesterday' * formControlName="effectiveDate"> * <p>This is a test.</p> * </common-date> * @export * * @type {?} */ const MAX_YEAR_RANGE = 150; /** @type {?} */ const distantFuture = addYears(startOfToday(), MAX_YEAR_RANGE); /** @type {?} */ const distantPast = subYears(startOfToday(), MAX_YEAR_RANGE); class DateComponent extends AbstractFormControl { // TODO: remove if not required - value does not get set when using Reactive forms /** * @param {?} controlDir * @param {?} injectedValidators */ constructor(controlDir, injectedValidators) { super(); this.controlDir = controlDir; this.injectedValidators = injectedValidators; this.dateChange = new EventEmitter(); this.label = 'Date'; /** * Can be one of: "future", "past". "future" includes today, "past" does not. */ this.restrictDate = 'any'; this._month = 'null'; // this makes it so the blank option is selected in the input // variables for date ranges this._dateRangeStart = null; this._dateRangeEnd = null; this.monthList = [ 'January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December' ]; this.monthLabelforId = 'month_' + this.objectId; this.dayLabelforId = 'day_' + this.objectId; this.yearLabelforId = 'year_' + this.objectId; // Abstact variable defined this._defaultErrMsg = { required: `${LabelReplacementTag} is required.`, dayOutOfRange: `Invalid ${LabelReplacementTag}.`, yearDistantPast: `Invalid ${LabelReplacementTag}.`, yearDistantFuture: `Invalid ${LabelReplacementTag}.`, noPastDatesAllowed: `Invalid ${LabelReplacementTag}.`, noFutureDatesAllowed: `Invalid ${LabelReplacementTag}.`, invalidValue: `Invalid ${LabelReplacementTag}.`, invalidRange: `Invalid ${LabelReplacementTag}.` }; this.today = startOfToday(); this.tomorrow = addDays(this.today, 1); if (controlDir) { controlDir.valueAccessor = this; } } /** * The earliest valid date that can be used. * Do NOT combine with restrictDates, as they set the same underlying values. * @param {?} dt * @return {?} */ set dateRangeStart(dt) { // Set time on date to 00:00:00 for comparing later this._dateRangeStart = dt ? startOfDay(dt) : null; } /** * The latest valid date that can be used. * Do NOT combine with restrictDates, as they set the same underlying values. * @param {?} dt * @return {?} */ set dateRangeEnd(dt) { // Set time on date to 00:00:00 for comparing later this._dateRangeEnd = dt ? startOfDay(dt) : null; } /** * @param {?} changes * @return {?} */ ngOnChanges(changes) { /* * Works, creates new object literall * obj = { * errorMessage: 'new'message'; * } * * Doesn't work, modifies existing object. Would have to manually call cd.detectChanges() afterwards. * obj.errorMessage = 'newMessage'; */ if (changes['errorMessage']) { this.setErrorMsg(); } } /** * @return {?} */ ngOnInit() { this.setErrorMsg(); // Set to midnight, so we don't accidentally compare against hours/minutes/seconds if (this.restrictDate !== 'any' && (this._dateRangeEnd || this._dateRangeStart)) { /** @type {?} */ const msg = `<common-date> - Invalid @Input() option configuration. You cannot use "[restrictDate]" in combination with either "[dateRangeEnd]" or "[dateRangeStart]". You must use either [restrictDate] or the [dateRange*] inputs. <common-date name='effectiveDate' label="Effective Date" [restrictDate]="'past'" <<< problem, choose one [dateRangeEnd]='today' <<< problem, choose one formControlName="effectiveDate" ></common-date> `; throw new MoHCommonLibraryError(msg); } // Initialize date range logic if (this.restrictDate === 'past') { // past does allow for today this._dateRangeEnd = this.today; this._dateRangeStart = null; } else if (this.restrictDate === 'future') { // future does NOT allow for today this._dateRangeEnd = null; this._dateRangeStart = this.tomorrow; } this.registerValidation(this.controlDir, this.validateSelf) .then((/** * @param {?} _ * @return {?} */ _ => { if (this.injectedValidators && this.injectedValidators.length) { // TODO: Potentially move to AbstractFormControl // Inspect the validator functions for one that has a {required: true} // property. Importantly, we are inspecting the validator function // itself and NOT the current state of the NgControl. -- Does not work for reactive forms this.isRequired = this.injectedValidators .filter((/** * @param {?} x * @return {?} */ x => x.required)) .length >= 1; } })); } /** * @return {?} */ get month() { if (this.date) { return this.date.getMonth(); } } /** * @return {?} */ get day() { if (this.date) { return this.date.getDate(); } } /** * @return {?} */ get year() { if (this.date) { return this.date.getFullYear(); } } /** * Handles creating / destroying date and emitting changes based on user behaviour. * @private * @return {?} */ processDate() { if (this.canCreateDate()) { /** @type {?} */ const year = this.getNumericValue(this._year); /** @type {?} */ const month = this.getNumericValue(this._month); /** @type {?} */ const day = this.getNumericValue(this._day); // Date function appears to use setYear() so any year 0-99 results in year 1900 to 1999 // Set each field individually, use setFullYear() instead of setYear() // Set time on date to 00:00:00 for comparing later this.date = startOfDay(new Date(year, month, day)); this.date.setFullYear(year); } else { // Trigger validator for emptying fields use case. This is to remove the 'Invalid date' error. if (this.date || (!this._year && !this._day && this._month === 'null')) { // Destroys the internal Date object. this.date = null; } } this._onChange(this.date); this._onTouched(this.date); this.dateChange.emit(this.date); } /** * Returns true if and only if the day/month/year fields are all filled out. * @private * @return {?} */ canCreateDate() { // special because "0" is valid (Jan) /** @type {?} */ const monthCheck = (typeof this._month === 'string' && this._month !== 'null') || typeof this._month === 'number'; if (!!this._year && !!this._day && monthCheck) { return true; } return false; } /** * @private * @param {?} dt * @return {?} */ _triggerOnChange(dt) { this._onChange(dt); this.dateChange.emit(dt); } /** * Convert string to numeric value or null if not * @private * @param {?} value * @return {?} */ getNumericValue(value) { /** @type {?} */ const parsed = parseInt(value, 10); return (isNaN(parsed) ? null : parsed); } /** * @private * @return {?} */ setDisplayVariables() { this._day = this.date.getDate().toString(); this._month = this.date.getMonth().toString(); this._year = this.date.getFullYear().toString(); } /** * @param {?} value * @return {?} */ writeValue(value) { if (value) { this.date = value; this.setDisplayVariables(); } } /** * @param {?} value * @return {?} */ onBlurDay(value) { this._day = value; this.processDate(); } /** * @param {?} value * @return {?} */ onBlurYear(value) { this._year = value; this.processDate(); } /** * @param {?} value * @return {?} */ onBlurMonth(value) { this._month = value; this.processDate(); } /** * Validates the DateComponent instance itself, using internal private variables. * * @private * @return {?} */ validateSelf() { /** @type {?} */ const year = parseInt(this._year, 10); /** @type {?} */ const month = parseInt(this._month, 10); /** @type {?} */ const day = parseInt(this._day, 10); // Nothing empty fields - nothing to validate OR have required error if (isNaN(year) && isNaN(month) && isNaN(day)) { return null; } // Partially filled out is always invalid, if year is present it must be greater than zero if (isNaN(year) || isNaN(month) || isNaN(day) || (!isNaN(year) && year <= 0)) { return { invalidValue: true }; } // We can hardcode the day, since we're only interested in total days for that month. /** @type {?} */ const daysInMonth = getDaysInMonth(new Date(year, month, 1)); if (day > daysInMonth || day < 1) { return { dayOutOfRange: true }; } /** @type {?} */ const dateRangeResult = this.validateRange(); if (dateRangeResult) { return dateRangeResult; } /** @type {?} */ const distantDatesResult = this.validateDistantDates(); if (distantDatesResult) { return distantDatesResult; } return null; } // If you set restrictDate, it will return noFutureDatesAllowed / noPastDatesAllowed // If you just set dateRangeStart / dateRangeEnd, you get invalidRange /** * @private * @return {?} */ validateRange() { /** @type {?} */ const _dt = startOfDay(this.date); if (this._dateRangeEnd && isAfter(_dt, this._dateRangeEnd)) { if (this.restrictDate === 'past' || compareAsc(this._dateRangeEnd, this.today) === 0) { return { noFutureDatesAllowed: true }; } return { invalidRange: true }; } if (this._dateRangeStart && isBefore(_dt, this._dateRangeStart)) { if (this.restrictDate === 'future' || compareAsc(this._dateRangeStart, this.tomorrow) === 0) { return { noPastDatesAllowed: true }; } return { invalidRange: true }; } return null; } /** * @private * @return {?} */ validateDistantDates() { // Null end range only allow 150 years in future if (!this._dateRangeEnd && isAfter(this.date, distantFuture)) { return { yearDistantFuture: true }; } // Null start range only allow 150 years in past if (!this._dateRangeStart && isBefore(this.date, distantPast)) { return { yearDistantPast: true }; } return null; } } DateComponent.decorators = [ { type: Component, args: [{ selector: 'common-date', template: "<fieldset>\r\n <legend class=\"date--legend\">{{label}}</legend>\r\n <ng-container *ngTemplateOutlet=\"instructionText\"></ng-container>\r\n <div class=\"date-row\">\r\n <select class=\"form-control monthSelect\"\r\n id=\"{{monthLabelforId}}\"\r\n name=\"{{monthLabelforId}}\"\r\n [value]=\"_month\"\r\n (blur)=\"onBlurMonth($event.target.value)\"\r\n [disabled]='disabled'\r\n [required]=\"isRequired\"\r\n aria-label=\"Month\">\r\n <!-- We show the blank option so the user can clear out their data.-->\r\n <option value=\"null\" label=\"Month\" selected [disabled]='isRequired'></option>\r\n <option *ngFor=\"let month of monthList; let i = index;\" [value]=\"i\">{{month}}</option>\r\n </select>\r\n\r\n <input type=\"number\"\r\n class=\"form-control dayInput\"\r\n id=\"{{dayLabelforId}}\"\r\n name=\"{{dayLabelforId}}\"\r\n placeholder=\"Day\"\r\n [value]=\"_day\"\r\n (blur)=\"onBlurDay($event.target.value)\"\r\n commonDateFieldFormat\r\n [disabled]='disabled'\r\n [required]=\"isRequired\"\r\n maxlength=\"2\"\r\n step=\"any\"\r\n aria-label=\"Day\"\r\n autocomplete=\"off\"/>\r\n\r\n <input type=\"number\"\r\n class=\"form-control yearInput\"\r\n id=\"{{yearLabelforId}}\"\r\n name=\"{{yearLabelforId}}\"\r\n placeholder=\"Year\"\r\n [value]=\"_year\"\r\n (blur)=\"onBlurYear($event.target.value)\"\r\n commonDateFieldFormat\r\n [disabled]='disabled'\r\n [required]=\"isRequired\"\r\n maxlength=\"4\"\r\n step=\"any\"\r\n aria-label=\"Year\"\r\n autocomplete=\"off\"/>\r\n\r\n </div>\r\n</fieldset>\r\n\r\n<common-error-container\r\n [displayError]=\"!disabled && (controlDir?.touched || controlDir?.dirty) && controlDir.errors\">\r\n <div *ngIf=\"controlDir?.errors?.required; else ComponentErrors\">\r\n {{_defaultErrMsg.required}}\r\n </div>\r\n</common-error-container>\r\n\r\n<ng-template #instructionText>\r\n <ng-content></ng-content>\r\n</ng-template>\r\n\r\n<!--Errors triggered by self-validation within component-->\r\n<ng-template #ComponentErrors>\r\n <div *ngIf=\"controlDir?.errors?.dayOutOfRange\">\r\n {{_defaultErrMsg.dayOutOfRange}}\r\n </div>\r\n <div *ngIf=\"controlDir?.errors?.yearDistantPast\">\r\n {{_defaultErrMsg.yearDistantPast}}\r\n </div>\r\n <div *ngIf=\"controlDir?.errors?.yearDistantFuture\">\r\n {{_defaultErrMsg.yearDistantFuture}}\r\n </div>\r\n <div *ngIf=\"controlDir?.errors?.noPastDatesAllowed\">\r\n {{_defaultErrMsg.noPastDatesAllowed}}\r\n </div>\r\n <div *ngIf=\"controlDir?.errors?.noFutureDatesAllowed\">\r\n {{_defaultErrMsg.noFutureDatesAllowed}}\r\n </div>\r\n <div *ngIf=\"controlDir?.errors?.invalidRange\">\r\n {{_defaultErrMsg.invalidRange}}\r\n </div>\r\n\r\n <!-- Case should not happen until something is not formatted correctly-->\r\n <div *ngIf=\"controlDir?.errors?.invalidValue\">\r\n {{_defaultErrMsg.invalidValue}}\r\n </div>\r\n</ng-template>\r\n", styles: [".date--legend{font-size:inherit;font-weight:700}.date-row{display:flex;flex-wrap:nowrap;justify-content:space-between}.monthSelect{max-width:50%;height:35px;margin-right:1em}.dayInput{max-width:25%;height:35px;margin-right:1em}.yearInput{max-width:25%;height:35px}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{-webkit-appearance:none;-moz-appearance:none;appearance:none;margin:0}select option[selected]{color:gray!important}"] }] } ]; /** @nocollapse */ DateComponent.ctorParameters = () => [ { type: NgControl, decorators: [{ type: Optional }, { type: Self }] }, { type: Array, decorators: [{ type: Optional }, { type: Self }, { type: Inject, args: [NG_VALIDATORS,] }] } ]; DateComponent.propDecorators = { date: [{ type: Input }], dateChange: [{ type: Output }], label: [{ type: Input }], restrictDate: [{ type: Input }], dateRangeStart: [{ type: Input }], dateRangeEnd: [{ type: Input }] }; /** * @fileoverview added by tsickle * @suppress {checkTypes,extraRequire,missingOverride,missingReturn,unusedPrivateMembers,uselessCode} checked by tsc */ /** * PRIME datepicker component. Largely a wrapper for ngx-mydatepicker * https://github.com/kekeh/ngx-mydatepicker * * NOTE - YOU MUST INCLUDE NGX-MYDATEPICKER IN YOUR PARENT APPLICATION TO USE * THIS COMPONENT! This is due to some poor implementation in ngx-mydatepicker. * Make sure to use the same version that this library uses. */ class DatepickerComponent extends Base { constructor() { super(); /** * Component size can be reduced, see Datepickersizes for options */ this.size = DatepickerSizes.DEFAULT; this.dateChange = new EventEmitter(); this.required = false; /** * Control visibility of the clear 'x' button on the mini datepicker. * * **'visible'** is default, button exists * * **'none'** means the element does not exist * * **'invisible'** means the element takes up space but is not visible / cannot be * used. * * Invisible is useful when you want to make sure a datepicker is the same * size as a visible one.