novo-elements
Version:
1 lines • 119 kB
Source Map (JSON)
{"version":3,"file":"novo-elements-elements-field.mjs","sources":["../../../projects/novo-elements/src/elements/field/error/error.ts","../../../projects/novo-elements/src/elements/field/error/error.html","../../../projects/novo-elements/src/elements/field/field-control.ts","../../../projects/novo-elements/src/elements/field/hint/hint.ts","../../../projects/novo-elements/src/elements/field/hint/hint.html","../../../projects/novo-elements/src/elements/field/field.ts","../../../projects/novo-elements/src/elements/field/field.html","../../../projects/novo-elements/src/elements/field/fieldset.ts","../../../projects/novo-elements/src/elements/field/fieldset.html","../../../projects/novo-elements/src/elements/field/formats/base-format.ts","../../../projects/novo-elements/src/elements/field/formats/date-format.ts","../../../projects/novo-elements/src/elements/field/formats/date-range-format.ts","../../../projects/novo-elements/src/elements/field/formats/date-time-format.ts","../../../projects/novo-elements/src/elements/field/formats/time-format.ts","../../../projects/novo-elements/src/elements/field/input.ts","../../../projects/novo-elements/src/elements/field/picker.directive.ts","../../../projects/novo-elements/src/elements/field/toggle/picker-toggle.component.ts","../../../projects/novo-elements/src/elements/field/toggle/picker-toggle.component.html","../../../projects/novo-elements/src/elements/field/field.module.ts","../../../projects/novo-elements/src/elements/field/novo-elements-elements-field.ts"],"sourcesContent":["// NG2\nimport { Component, OnInit } from '@angular/core';\nimport { DomSanitizer } from '@angular/platform-browser';\n\n@Component({\n selector: 'novo-error',\n templateUrl: './error.html',\n styleUrls: ['./error.scss'],\n standalone: false,\n})\nexport class NovoErrorElement implements OnInit {\n constructor(private sanitizer: DomSanitizer) {}\n\n ngOnInit(): any {}\n}\n","<ng-content></ng-content>","import { Directive } from '@angular/core';\nimport { NgControl } from '@angular/forms';\nimport { Observable } from 'rxjs';\n\n/** An interface which allows a control to work inside of a `NovoField`. */\n@Directive()\nexport abstract class NovoFieldControl<T> {\n /** The value of the control. */\n value: T | null;\n\n /** The last key pressed. */\n lastKeyValue: string | null;\n\n /** The last cursor position. */\n lastCaretPosition: number | null;\n\n /**\n * Stream that emits whenever the state of the control changes such that the parent `NovoField`\n * needs to run change detection.\n */\n readonly stateChanges: Observable<void>;\n\n /** The element ID for this control. */\n readonly id: string;\n\n /** The placeholder for this control. */\n readonly placeholder: string;\n\n /** Gets the NgControl for this control. */\n readonly ngControl: NgControl | null;\n\n /** Whether the control is focused. */\n readonly focused: boolean;\n\n /** Whether the control is empty. */\n readonly empty: boolean;\n\n /** Whether the `NovoField` label should try to float. */\n // readonly shouldLabelFloat: boolean;\n // readonly shouldFieldHaveUnderline: boolean;\n\n /** Whether the control is required. */\n readonly required: boolean;\n\n /** Whether the control is disabled. */\n readonly disabled: boolean;\n\n /** Whether the control is in an error state. */\n readonly errorState: boolean;\n\n /** Whether the control can have multiple values. */\n readonly multiple?: boolean;\n /**\n * An optional name for the control type that can be used to distinguish `novo-form-field` elements\n * based on their control type. The form field will add a class,\n * `novo-form-field-type-{{controlType}}` to its root element.\n */\n readonly controlType?: string;\n\n /**\n * Whether the input is currently in an autofilled state. If property is not present on the\n * control it is assumed to be false.\n */\n readonly autofilled?: boolean;\n\n /** Sets the list of element IDs that currently describe this control. */\n abstract setDescribedByIds(ids: string[]): void;\n\n /** Handles a click on the control's container. */\n abstract onContainerClick(event: MouseEvent): void;\n\n abstract focus(options?: FocusOptions): void;\n}\n","// NG2\nimport { Component, Input, OnInit } from '@angular/core';\n\nlet nextUniqueId = 0;\n@Component({\n selector: 'novo-hint',\n templateUrl: './hint.html',\n styleUrls: ['./hint.scss'],\n host: {\n class: 'novo-hint',\n '[class.novo-field-hint-end]': 'align === \"end\"',\n '[attr.id]': 'id',\n // Remove align attribute to prevent it from interfering with layout.\n '[attr.align]': 'null',\n },\n standalone: false,\n})\nexport class NovoHintElement implements OnInit {\n /** Whether to align the hint label at the start or end of the line. */\n @Input() align: 'start' | 'end' = 'start';\n\n /** Unique ID for the hint. Used for the aria-describedby on the form field control. */\n @Input() id: string = `novo-hint-${nextUniqueId++}`;\n\n ngOnInit(): any {}\n}\n","<ng-content></ng-content>","// NG2\nimport {\n AfterContentInit,\n ChangeDetectionStrategy,\n ChangeDetectorRef,\n Component,\n ContentChild,\n ContentChildren,\n Directive,\n ElementRef,\n EventEmitter,\n HostListener,\n InjectionToken,\n Input,\n OnDestroy,\n Output,\n QueryList,\n ViewChild,\n} from '@angular/core';\nimport { NgControl } from '@angular/forms';\nimport { fromEvent, Subject, Subscription } from 'rxjs';\nimport { startWith, takeUntil } from 'rxjs/operators';\nimport { NovoLabel, HasOverlay, NOVO_OVERLAY_CONTAINER } from 'novo-elements/elements/common';\nimport { NovoErrorElement } from './error/error';\nimport { NovoFieldControl } from './field-control';\nimport { NovoHintElement } from './hint/hint';\n\n@Directive({\n selector: '[novoPrefix]',\n standalone: false,\n})\nexport class NovoFieldPrefixDirective {}\n@Directive({\n selector: '[novoSuffix]',\n standalone: false,\n})\nexport class NovoFieldSuffixDirective {}\n\nconst NOVO_INPUT_UNDERLINED_TYPES = [\n 'text',\n 'date',\n 'time',\n 'datetime-local',\n 'password',\n 'email',\n 'tel',\n 'select',\n 'textarea',\n 'number',\n 'novo-chip-list',\n 'chip-list',\n];\nexport const NOVO_FORM_FIELD = new InjectionToken<NovoFieldElement>('NovoFormField');\n\n@Component({\n selector: 'novo-field',\n templateUrl: './field.html',\n styleUrls: ['./field.scss', './field-standard.scss', './field-fill.scss', './field-outline.scss', './field-list.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: {\n class: 'novo-field',\n '[class.novo-field-layout-horizontal]': 'layout==\"horizontal\"',\n '[class.novo-field-layout-vertical]': 'layout==\"vertical\"',\n '[class.novo-field-appearance-standard]': 'appearance == \"standard\"',\n '[class.novo-field-appearance-fill]': 'appearance == \"fill\"',\n '[class.novo-field-appearance-outline]': 'appearance == \"outline\"',\n '[class.novo-field-appearance-list]': 'appearance == \"list\"',\n '[class.novo-field-appearance-underlined]': '_isUnderlinedInput()',\n '[class.novo-field-invalid]': '_control.errorState',\n '[class.novo-field-has-label]': '_hasLabel()',\n '[class.novo-field-no-label]': '!_hasLabel()',\n '[class.novo-field-disabled]': '_control.disabled',\n '[class.novo-field-autofilled]': '_control.autofilled',\n '[class.novo-focused]': '_control.focused',\n '[class.ng-untouched]': '_shouldForward(\"untouched\")',\n '[class.ng-touched]': '_shouldForward(\"touched\")',\n '[class.ng-pristine]': '_shouldForward(\"pristine\")',\n '[class.ng-dirty]': '_shouldForward(\"dirty\")',\n '[class.ng-valid]': '_shouldForward(\"valid\")',\n '[class.ng-invalid]': '_shouldForward(\"invalid\")',\n '[class.ng-pending]': '_shouldForward(\"pending\")',\n },\n providers: [{ provide: NOVO_FORM_FIELD, useExisting: NovoFieldElement }],\n standalone: false,\n})\nexport class NovoFieldElement implements AfterContentInit, OnDestroy {\n private _labelClicks = Subscription.EMPTY;\n\n @ViewChild('inputContainer') _inputContainerRef: ElementRef;\n\n @ContentChild(NovoLabel) _labelElement: NovoLabel;\n @ContentChildren(NovoHintElement) _hintElements: QueryList<NovoHintElement>;\n @ContentChildren(NovoErrorElement) _errorElements: QueryList<NovoErrorElement>;\n @ContentChildren(NovoFieldPrefixDirective) _prefixElements: QueryList<NovoFieldPrefixDirective>;\n @ContentChildren(NovoFieldSuffixDirective) _suffixElements: QueryList<NovoFieldSuffixDirective>;\n @ContentChildren(NOVO_OVERLAY_CONTAINER) _overlayElements: QueryList<HasOverlay>;\n\n @ContentChild(NovoFieldControl) _control: NovoFieldControl<any>;\n\n @Input() layout: 'horizontal' | 'vertical' = 'vertical';\n @Input() appearance: 'standard' | 'outline' | 'fill' | 'list' = 'standard';\n /**\n * When this field has a picker element, express which element it should be parented to\n */\n @Input() customOverlayOrigin: ElementRef;\n\n @Input()\n width: string;\n\n private _destroyed = new Subject<void>();\n\n @Output() valueChanges: EventEmitter<any> = new EventEmitter();\n @Output() stateChanges: EventEmitter<void> = new EventEmitter();\n\n constructor(public _elementRef: ElementRef, private _changeDetectorRef: ChangeDetectorRef) {}\n /**\n * Gets an ElementRef for the element that a overlay attached to the form-field should be\n * positioned relative to.\n */\n getConnectedOverlayOrigin(): ElementRef {\n return this.customOverlayOrigin || this._inputContainerRef || this._elementRef;\n }\n\n ngAfterContentInit(): any {\n this._validateControlChild();\n\n const control = this._control;\n if (control.controlType) {\n this._elementRef.nativeElement.classList.add(`novo-field-type-${control.controlType}`);\n this._elementRef.nativeElement.setAttribute('data-control-type', control.controlType);\n }\n\n if (control.id) {\n this._elementRef.nativeElement.setAttribute('data-control-id', control.id);\n }\n\n if (control.ngControl?.name) {\n this._elementRef.nativeElement.setAttribute('data-control-key', control.ngControl.name);\n }\n\n // Subscribe to changes in the child control state in order to update the form field UI.\n control.stateChanges.pipe(startWith(null)).subscribe(() => {\n this.stateChanges.next();\n this._changeDetectorRef.markForCheck();\n });\n\n // Run change detection if the value changes.\n if (control.ngControl && control.ngControl.valueChanges) {\n control.ngControl.valueChanges.pipe(takeUntil(this._destroyed)).subscribe((v) => {\n this.valueChanges.next(v);\n this._changeDetectorRef.markForCheck();\n });\n }\n\n if (this._hasLabel()) {\n this._labelClicks = fromEvent(this._labelElement.nativeElement, 'click').subscribe(() => this._control.focus());\n }\n }\n\n ngOnDestroy() {\n this._destroyed.next();\n this._destroyed.complete();\n this._labelClicks.unsubscribe();\n }\n\n /** Throws an error if the form field's control is missing. */\n protected _validateControlChild() {\n if (!this._control) {\n throw new Error('Missing Novo Control');\n }\n }\n\n public blurEventIsInField(blurEvt: FocusEvent): boolean {\n return this._elementRef.nativeElement.contains(blurEvt.relatedTarget) || this._overlayElements.some(hasOverlay => hasOverlay.overlay?.isBlurRecipient(blurEvt));\n }\n\n @HostListener('click', ['$event'])\n _handleContainerClick(evt: MouseEvent) {\n this._control.onContainerClick(evt);\n }\n\n _isUnderlinedInput(): boolean {\n return NOVO_INPUT_UNDERLINED_TYPES.includes(this._control.controlType);\n }\n\n /** Determines whether to display hints or errors. */\n _getDisplayedMessages(): 'error' | 'hint' {\n return this._errorElements && this._errorElements.length > 0 && this._control.errorState ? 'error' : 'hint';\n }\n\n /** Determines whether a class from the NgControl should be forwarded to the host element. */\n _shouldForward(prop: keyof NgControl): boolean {\n const ngControl = this._control ? this._control.ngControl : null;\n return ngControl && ngControl[prop];\n }\n\n _hasLabel() {\n return !!this._labelElement;\n }\n}\n","<div class=\"novo-field-label\">\n <ng-content select=\"novo-label\"></ng-content>\n</div>\n<div class=\"novo-field-input\" [style.width]=\"width\" #inputContainer>\n <div class=\"novo-field-prefix\">\n <ng-content select=\"[novoPrefix]\"></ng-content>\n </div>\n <div class=\"novo-field-infix\">\n <ng-content></ng-content>\n </div>\n <div class=\"novo-field-suffix\">\n <ng-content select=\"[novoSuffix]\"></ng-content>\n </div>\n</div>\n<div class=\"novo-field-messages\" [ngSwitch]=\"_getDisplayedMessages()\">\n <div class=\"novo-field-hint-wrapper\" *ngSwitchCase=\"'error'\">\n <ng-content select=\"novo-error\"></ng-content>\n </div>\n <div class=\"novo-field-hint-wrapper\" *ngSwitchCase=\"'hint'\">\n <ng-content select=\"novo-hint\"></ng-content>\n <ng-content select=\"novo-hint[align=end]\"></ng-content>\n </div>\n</div>","// NG2\nimport { AfterContentInit, ChangeDetectionStrategy, Component, ContentChildren, HostBinding, Input, QueryList } from '@angular/core';\nimport { BooleanInput } from 'novo-elements/utils';\nimport { NovoFieldElement } from './field';\n\n@Component({\n selector: 'novo-fields',\n templateUrl: './fieldset.html',\n styleUrls: ['./fieldset.scss'],\n changeDetection: ChangeDetectionStrategy.OnPush,\n host: {\n class: 'novo-field',\n '[class.novo-fieldset-appearance-standard]': 'appearance == \"standard\"',\n '[class.novo-fieldset-appearance-fill]': 'appearance == \"fill\"',\n '[class.novo-fieldset-appearance-outline]': 'appearance == \"outline\"',\n '[class.novo-fieldset-appearance-list]': 'appearance == \"list\"',\n },\n standalone: false,\n})\nexport class NovoFieldsElement implements AfterContentInit {\n @ContentChildren(NovoFieldElement)\n _fields: QueryList<NovoFieldElement>;\n\n _layout: 'horizontal' | 'vertical' = 'horizontal';\n @Input() get layout(): any {\n return this._layout;\n }\n set layout(value) {\n if (this._layout !== value) {\n this._layout = value;\n this._updateFieldLayout();\n }\n }\n\n _appearance: 'standard' | 'outline' | 'fill' | 'list' = 'standard';\n @Input() get appearance(): any {\n return this._appearance;\n }\n set appearance(value) {\n if (this._appearance !== value) {\n this._appearance = value;\n this._updateFieldAppearance();\n }\n }\n\n @HostBinding('class.full-width')\n @Input()\n @BooleanInput()\n fullWidth: boolean = false;\n\n ngAfterContentInit(): any {\n this._updateFieldLayout();\n this._updateFieldAppearance();\n }\n\n private _updateFieldLayout(): void {\n if (this._fields) {\n this._fields.forEach((field) => {\n field.layout = this.layout;\n });\n }\n }\n\n private _updateFieldAppearance(): void {\n if (this._fields) {\n this._fields.forEach((field) => {\n field.appearance = this.appearance;\n });\n }\n }\n}\n","<ng-content></ng-content>","import { EventEmitter, InjectionToken } from '@angular/core';\nimport { ControlValueAccessor } from '@angular/forms';\n\nexport const NOVO_INPUT_FORMAT = new InjectionToken<NovoInputFormat>('NovoInputFormat');\n\nexport interface NovoInputFormat<T = any> extends ControlValueAccessor {\n valueChange: EventEmitter<any>;\n formatValue(value: T): string;\n}\n\nexport enum DATE_FORMATS {\n DATE = 'date',\n ISO8601 = 'iso8601',\n STRING = 'string',\n YEAR_MONTH_DAY = 'yyyy-mm-dd',\n}\n","import { Directive, EventEmitter, forwardRef, Input } from '@angular/core';\nimport { NG_VALUE_ACCESSOR } from '@angular/forms';\nimport { IMaskDirective } from 'angular-imask';\nimport { isValid } from 'date-fns';\nimport { MaskedRange } from 'imask';\nimport { DateFormatService, NovoLabelService } from 'novo-elements/services';\nimport { DateParseOptions, DateUtil } from 'novo-elements/utils';\nimport { DATE_FORMATS, NOVO_INPUT_FORMAT } from './base-format';\n\nexport const DATEFORMAT_VALUE_ACCESSOR = {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => NovoDateFormatDirective),\n multi: true,\n};\n\n@Directive({\n selector: 'input[dateFormat]',\n host: {\n class: 'novo-date-format',\n },\n providers: [DATEFORMAT_VALUE_ACCESSOR, { provide: NOVO_INPUT_FORMAT, useExisting: NovoDateFormatDirective }],\n standalone: false,\n})\nexport class NovoDateFormatDirective extends IMaskDirective<any> {\n valueChange: EventEmitter<any> = new EventEmitter();\n\n @Input() dateFormat: DATE_FORMATS = DATE_FORMATS.DATE;\n\n constructor(private labels: NovoLabelService, private dateFormatService: DateFormatService) {\n super();\n this.unmask = 'typed' as unknown as false; // typing is to work around angular-imask bug\n this.imask = {\n mask: Date,\n pattern: this.dateFormatService.dateFormatAsImaskPattern,\n overwrite: true,\n autofix: true,\n lazy: false,\n min: new Date(1900, 0, 1),\n max: new Date(2100, 0, 1),\n prepare: (str) => str.toUpperCase(),\n format: (date) => this.formatValue(date, { userDateFormat: this.labels.dateFormatString()}),\n parse: (str) => DateUtil.parse(str, { userDateFormat: this.labels.dateFormatString().toUpperCase() }),\n blocks: {\n d: {\n mask: MaskedRange,\n placeholderChar: 'D',\n from: 1,\n to: 31,\n maxLength: 2,\n },\n m: {\n mask: MaskedRange,\n placeholderChar: 'M',\n from: 1,\n to: 12,\n maxLength: 2,\n },\n Y: {\n mask: MaskedRange,\n placeholderChar: 'Y',\n from: 1900,\n to: 9999,\n },\n },\n };\n }\n\n normalize(value: string) {\n const pattern = this.labels.dateFormatString().toUpperCase();\n if (!value) {\n return '';\n }\n return DateUtil.format(DateUtil.parse(value, { userDateFormat: this.labels.dateFormatString()}), pattern);\n }\n\n formatAsIso(date: Date): string {\n if (date && isValid(date)) {\n return date.toISOString().slice(0, 10);\n }\n return null;\n }\n\n formatYearMonthDay(date: Date): string {\n if (date && isValid(date)) {\n return DateUtil.format(date, 'YYYY-MM-DD');\n }\n return null;\n }\n\n formatValue(value: any, options?: DateParseOptions): string {\n if (value == null) {\n return '';\n }\n const dateFormat = this.labels.dateFormatString().toUpperCase();\n const date = DateUtil.parse(value, options);\n if (isValid(date)) {\n return DateUtil.format(date, dateFormat);\n }\n return this.normalize(value);\n }\n\n writeValue(value: any) {\n const initialValue = this['_initialValue'];\n if (initialValue != null && value === initialValue) {\n // This value has already been formatted from the first call to writeValue, simply use it.\n super.writeValue(initialValue);\n } else {\n super.writeValue(this.formatValue(value));\n this.valueChange.emit(value);\n }\n }\n\n registerOnChange(fn: (_: any) => void): void {\n this.onChange = (date: any) => {\n let formatted = date;\n switch (this.dateFormat) {\n case DATE_FORMATS.ISO8601:\n formatted = this.formatAsIso(date);\n break;\n case DATE_FORMATS.STRING:\n formatted = this.formatValue(date);\n break;\n case DATE_FORMATS.YEAR_MONTH_DAY:\n formatted = this.formatYearMonthDay(date);\n break;\n default:\n formatted = date;\n break;\n }\n this.valueChange.emit(date);\n fn(formatted);\n };\n }\n}\n","import { Directive, EventEmitter, forwardRef, Input } from '@angular/core';\nimport { NG_VALUE_ACCESSOR } from '@angular/forms';\nimport { IMaskDirective } from 'angular-imask';\nimport { isValid } from 'date-fns';\nimport { MaskedRange } from 'imask';\nimport { DateFormatService, NovoLabelService } from 'novo-elements/services';\nimport { DateParseOptions, DateUtil } from 'novo-elements/utils';\nimport { DATE_FORMATS, NOVO_INPUT_FORMAT } from './base-format';\n\nexport const DATERANGEFORMAT_VALUE_ACCESSOR = {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => NovoDateRangeFormatDirective),\n multi: true,\n};\n\ntype DateRange = {\n startDate: Date;\n endDate: Date;\n};\n\n@Directive({\n selector: 'input[dateRangeFormat]',\n host: {\n class: 'novo-date-range-format',\n },\n providers: [DATERANGEFORMAT_VALUE_ACCESSOR, { provide: NOVO_INPUT_FORMAT, useExisting: NovoDateRangeFormatDirective }],\n standalone: false,\n})\nexport class NovoDateRangeFormatDirective extends IMaskDirective<any> {\n valueChange: EventEmitter<any> = new EventEmitter();\n\n @Input() dateRangeFormat: DATE_FORMATS = DATE_FORMATS.DATE;\n\n constructor(private labels: NovoLabelService, private dateFormat: DateFormatService) {\n super();\n this.unmask = false;\n this.imask = {\n mask: `${this.dateFormat.dateFormatAsImaskPattern} - ${this.dateFormat.dateFormatAsImaskPattern}`,\n overwrite: true,\n autofix: true,\n lazy: false,\n blocks: {\n d: {\n mask: MaskedRange,\n placeholderChar: 'D',\n from: 1,\n to: 31,\n maxLength: 2,\n },\n m: {\n mask: MaskedRange,\n placeholderChar: 'M',\n from: 1,\n to: 12,\n maxLength: 2,\n },\n Y: {\n mask: MaskedRange,\n placeholderChar: 'Y',\n from: 1900,\n to: 9999,\n },\n },\n };\n }\n\n normalize(value: string | Date, options?: DateParseOptions) {\n const pattern = this.labels.dateFormatString().toUpperCase();\n return DateUtil.format(value ? DateUtil.parse(value, options) : null, pattern);\n }\n\n formatAsIso(value: DateRange): string {\n if (!value) {\n return '';\n }\n const { startDate, endDate } = value;\n if (startDate && isValid(startDate) && endDate && isValid(endDate)) {\n const startIso = startDate.toISOString().slice(0, 10);\n const endIso = endDate.toISOString().slice(0, 10);\n return `${startIso}/${endIso}`;\n }\n return null;\n }\n\n formatValue(value: DateRange): string {\n if (!value) {\n return '';\n }\n const { startDate, endDate } = value;\n return `${this.formatDate(startDate)} - ${this.formatDate(endDate)}`;\n }\n\n formatDate(source: Date | string) {\n const dateRangeFormat = this.labels.dateFormatString().toUpperCase();\n const date = DateUtil.parse(source);\n if (isValid(date)) {\n return DateUtil.format(date, dateRangeFormat);\n }\n return this.normalize(source);\n }\n\n writeValue(value: any) {\n if (this['_initialValue'] && value === this['_initialValue']) {\n // if this call is coming from the super class, skip through.\n // If we ever wanted to reduce the need for this hack/workaround, we could refactor\n // IMaskDirective to exist as a child portion of DateRangeFormatDirective.\n super.writeValue(value);\n return;\n }\n const formattedValue = this.formatValue(value);\n if (formattedValue !== this.maskValue) {\n super.writeValue(this.formatValue(value));\n this.onChange(this.formatValue(value));\n this.valueChange.emit(value);\n }\n }\n\n registerOnChange(fn: (_: any) => void): void {\n this.onChange = (input: any) => {\n if (this.validate(input)) {\n const dates = this.extractDatesFromInput(input);\n let formatted: DateRange | string = dates;\n switch (this.dateRangeFormat) {\n case DATE_FORMATS.ISO8601:\n formatted = this.formatAsIso(dates);\n break;\n case DATE_FORMATS.STRING:\n formatted = this.formatValue(dates);\n break;\n default:\n formatted = dates;\n break;\n }\n this.valueChange.emit(dates);\n fn(formatted);\n }\n };\n }\n\n extractDatesFromInput(value) {\n const [startStr, endStr] = value.split(' - ');\n const startDate = DateUtil.parse(startStr, { userDateFormat: this.labels.dateFormatString()});\n const endDate = DateUtil.parse(endStr, { userDateFormat: this.labels.dateFormatString()});\n return { startDate, endDate };\n }\n\n validate(dateStr: string) {\n const { startDate, endDate } = this.extractDatesFromInput(dateStr);\n return isValid(startDate) && isValid(endDate);\n }\n}\n","import { Directive, EventEmitter, Input, OnChanges, SimpleChanges, forwardRef } from '@angular/core';\nimport { NG_VALUE_ACCESSOR } from '@angular/forms';\nimport { IMaskDirective } from 'angular-imask';\nimport { isValid } from 'date-fns';\nimport { MaskedEnum, MaskedRange } from 'imask';\nimport { DateFormatService, NovoLabelService } from 'novo-elements/services';\nimport { DateParseOptions, DateUtil, Key } from 'novo-elements/utils';\nimport { DATE_FORMATS, NOVO_INPUT_FORMAT, NovoInputFormat } from './base-format';\n\nexport const DATETIMEFORMAT_VALUE_ACCESSOR = {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => NovoDateTimeFormatDirective),\n multi: true,\n};\n\n@Directive({\n selector: 'input[dateTimeFormat]',\n host: {\n class: 'novo-date-time-format',\n '(input)': '_checkInput($event)',\n '(blur)': '_handleBlur($event)',\n '(keydown)': '_handleKeydown($event)',\n },\n providers: [DATETIMEFORMAT_VALUE_ACCESSOR, { provide: NOVO_INPUT_FORMAT, useExisting: NovoDateTimeFormatDirective }],\n standalone: false,\n})\nexport class NovoDateTimeFormatDirective extends IMaskDirective<any> implements NovoInputFormat, OnChanges {\n valueChange: EventEmitter<any> = new EventEmitter();\n\n @Input() military: boolean = false;\n @Input() dateTimeFormat: DATE_FORMATS = DATE_FORMATS.DATE;\n\n constructor(private labels: NovoLabelService, private dateFormat: DateFormatService) {\n super();\n this.initFormatOptions();\n }\n\n initFormatOptions() {\n const amFormat = this.labels.timeFormatAM.toUpperCase();\n const pmFormat = this.labels.timeFormatPM.toUpperCase();\n this.unmask = 'typed' as unknown as false; // typing is to work around angular-imask bug\n let pattern = `${this.dateFormat.dateFormatAsImaskPattern}, HH:mm`;\n if (!this.military) {\n pattern += ' aa';\n }\n this.imask = {\n mask: Date,\n pattern,\n overwrite: true,\n autofix: true,\n lazy: false,\n min: new Date(1900, 0, 1),\n max: new Date(2100, 0, 1),\n prepare: (str) => str.toUpperCase(),\n format: (date) => this.formatValue(date, { userDateFormat: this.labels.dateFormatString() }),\n parse: (str) => DateUtil.parse(str, { userDateFormat: this.labels.dateFormatString().toUpperCase() }),\n blocks: {\n d: {\n mask: MaskedRange,\n placeholderChar: 'D',\n from: 1,\n to: 31,\n maxLength: 2,\n },\n m: {\n mask: MaskedRange,\n placeholderChar: 'M',\n from: 1,\n to: 12,\n maxLength: 2,\n },\n Y: {\n mask: MaskedRange,\n placeholderChar: 'Y',\n from: 1900,\n to: 9999,\n },\n HH: {\n mask: MaskedRange,\n placeholderChar: '-',\n maxLength: 2,\n from: 0,\n to: 23,\n },\n hh: {\n mask: MaskedRange,\n placeholderChar: '-',\n maxLength: 2,\n from: 1,\n to: 12,\n },\n mm: {\n mask: MaskedRange,\n placeholderChar: '-',\n maxLength: 2,\n from: 0,\n to: 59,\n },\n ss: {\n mask: MaskedRange,\n placeholderChar: '-',\n maxLength: 2,\n from: 0,\n to: 59,\n },\n aa: {\n mask: MaskedEnum,\n placeholderChar: '-',\n enum: ['AM', 'PM', 'am', 'pm', amFormat, pmFormat],\n },\n },\n };\n }\n\n ngOnChanges(changes: SimpleChanges): void {\n if (Object.keys(changes).some((key) => ['military', 'dateFormat'].includes(key))) {\n this.initFormatOptions();\n }\n }\n\n _checkInput(event: InputEvent): void {\n if (document.activeElement === event.target) {\n const text = (event.target as HTMLInputElement).value;\n const dateTime = text.split(', ');\n const hour = dateTime[1].slice(0, 2);\n if ((this.military && Number(dateTime[1][0]) > 2) || (!this.military && Number(dateTime[1][0]) > 1)) {\n event.preventDefault();\n const value = `0${dateTime[1]}`;\n (event.target as HTMLInputElement).value = value;\n }\n if (!this.military) {\n const input = dateTime[1].substr(5, 4).replace(/\\-/g, '').trim().slice(0, 2);\n const timePeriod = this.imask.blocks.aa.enum.find((it) => it[0] === input[0]);\n if (timePeriod) {\n (event.target as HTMLInputElement).value = `${dateTime[0]}, ${dateTime[1].slice(0, 5)} ${timePeriod}`;\n }\n if ((event.target as HTMLInputElement).selectionStart >= 3 && this.hourOneFormatRequired(hour)) {\n (event.target as HTMLInputElement).value = `${dateTime[0]}, 01:${(event.target as HTMLInputElement).value.slice(\n 3,\n (event.target as HTMLInputElement).value.length,\n )}`;\n }\n }\n }\n }\n\n _handleBlur(event: FocusEvent): void {\n const text = (event.target as HTMLInputElement).value;\n const dateTime = text.split(', ');\n const hour: string = dateTime[1].slice(0, 2);\n if (!this.military) {\n const input = dateTime[1].substr(5, 4).replace(/\\-/g, '').trim().slice(0, 2);\n const timePeriod = this.imask.blocks.aa.enum.find((it) => it[0] === input[0]);\n if (this.hourOneFormatRequired(hour)) {\n (event.target as HTMLInputElement).value = `${dateTime[0]}, 01:${dateTime[1].slice(3, dateTime[1].length)}`;\n }\n if (!timePeriod) {\n (event.target as HTMLInputElement).value = `${dateTime[0]}, ${dateTime[1].slice(0, 5)} --`;\n }\n }\n }\n\n _handleKeydown(event: KeyboardEvent): void {\n const input = event.target as HTMLInputElement;\n const dateTime = input.value.split(', ');\n const hour: string = dateTime[1].slice(0, 2);\n\n if (event.key === Key.Backspace && input.selectionStart === dateTime[1].length) {\n (event.target as HTMLInputElement).value = `${dateTime[1].slice(0, 5)} --`;\n } else if (event.key === Key.Tab && input.selectionStart <= 2 && this.hourOneFormatRequired(hour)) {\n event.preventDefault();\n event.stopPropagation();\n event.stopImmediatePropagation();\n input.value = `${dateTime[0]}, 01:${dateTime[1].slice(3, dateTime[1].length)}`;\n input.setSelectionRange(15, 15);\n } else if (event.key === Key.ArrowRight && input.selectionStart >= 2 && this.hourOneFormatRequired(hour)) {\n input.value = `${dateTime[0]}, 01:${dateTime[1].slice(3, dateTime[1].length)}`;\n input.setSelectionRange(14, 14);\n }\n }\n\n normalize(value: string, options?: DateParseOptions) {\n const pattern = this.labels.dateFormatString().toUpperCase();\n return DateUtil.format(value ? DateUtil.parse(value, options) : null, pattern);\n }\n\n formatAsIso(date: Date): string {\n if (date && isValid(date)) {\n return date.toISOString();\n }\n return null;\n }\n\n convertTime12to24(time12h: string) {\n const pmFormat = this.labels.timeFormatPM.toUpperCase();\n const [time, meridian] = time12h.split(' ');\n // eslint-disable-next-line prefer-const\n let [hours, minutes] = time.split(':');\n if (hours === '12') {\n hours = '00';\n }\n if (['PM', pmFormat].includes(meridian)) {\n hours = `${parseInt(hours, 10) + 12}`.padStart(2, '0');\n }\n return `${hours}:${minutes}`;\n }\n\n convertTime24to12(time24h: string) {\n if (time24h.length === 5) {\n const date = DateUtil.parse(`2020-01-01T${time24h}`);\n return DateUtil.format(date, 'hh:mm A');\n }\n return time24h;\n }\n\n formatValue(value: Date | string, options?: DateParseOptions): string {\n if (value == null) {\n return '';\n }\n // Use `parse` because it keeps dates in locale\n const date = DateUtil.parse(value, options);\n if (isValid(date)) {\n const dateFormat = `${this.labels.dateFormatString().toUpperCase()}, ${this.military ? 'HH:mm' : 'hh:mm A'}`;\n return DateUtil.format(date, dateFormat);\n }\n return this.normalize(value as string, options);\n }\n\n writeValue(value: any) {\n const initialValue = this['_initialValue'];\n if (initialValue != null && value === initialValue) {\n // This value has already been formatted from the first call to writeValue, simply use it.\n super.writeValue(initialValue);\n } else {\n super.writeValue(this.formatValue(value));\n this.valueChange.emit(value);\n }\n }\n\n registerOnChange(fn: (_: any) => void): void {\n this.onChange = (date: Date) => {\n let formatted: Date | string;\n switch (this.dateTimeFormat) {\n case DATE_FORMATS.ISO8601:\n formatted = this.formatAsIso(date);\n break;\n case DATE_FORMATS.STRING:\n formatted = this.formatValue(date);\n break;\n default:\n formatted = date;\n break;\n }\n this.valueChange.emit(date);\n fn(formatted);\n };\n }\n\n hourOneFormatRequired(hourInput: string): boolean {\n return hourInput === '-1' || hourInput === '1-';\n }\n\n get initialValue(): any {\n return this.maskValue;\n }\n}\n","import {\n AfterViewInit,\n ChangeDetectorRef,\n Directive,\n EventEmitter,\n forwardRef,\n Input,\n OnChanges,\n SimpleChanges,\n} from '@angular/core';\nimport { NG_VALUE_ACCESSOR } from '@angular/forms';\nimport { IMaskDirective } from 'angular-imask';\nimport { isValid } from 'date-fns';\nimport { MaskedEnum, MaskedRange } from 'imask';\nimport { NovoLabelService } from 'novo-elements/services';\nimport { DateUtil, Key } from 'novo-elements/utils';\nimport { NOVO_INPUT_FORMAT, NovoInputFormat } from './base-format';\n\nexport const TIMEFORMAT_VALUE_ACCESSOR = {\n provide: NG_VALUE_ACCESSOR,\n useExisting: forwardRef(() => NovoTimeFormatDirective),\n multi: true,\n};\n\nexport enum TIME_FORMATS {\n DATE = 'date',\n ISO8601 = 'iso8601',\n STRING = 'string',\n}\n\n@Directive({\n selector: 'input[timeFormat]',\n host: {\n class: 'novo-time-format',\n '(input)': '_checkInput($event)',\n '(blur)': '_handleBlur($event)',\n '(keydown)': '_handleKeydown($event)',\n },\n providers: [TIMEFORMAT_VALUE_ACCESSOR, { provide: NOVO_INPUT_FORMAT, useExisting: NovoTimeFormatDirective }],\n standalone: false,\n})\nexport class NovoTimeFormatDirective extends IMaskDirective<any> implements NovoInputFormat, AfterViewInit, OnChanges {\n valueChange: EventEmitter<any> = new EventEmitter();\n\n @Input() military: boolean = false;\n @Input() timeFormat: TIME_FORMATS = TIME_FORMATS.DATE;\n\n constructor(private labels: NovoLabelService, private cdr: ChangeDetectorRef) {\n super();\n this.initFormatOptions();\n }\n\n ngOnChanges(changes: SimpleChanges): void {\n if (Object.keys(changes).some((key) => ['military', 'timeFormat'].includes(key))) {\n this.initFormatOptions();\n }\n }\n\n initFormatOptions() {\n const amFormat = this.labels.timeFormatAM.toUpperCase();\n const pmFormat = this.labels.timeFormatPM.toUpperCase();\n this.unmask = 'typed' as unknown as false; // typing is to work around angular-imask bug\n this.imask = {\n mask: Date,\n pattern: this.military ? 'HH:mm' : 'hh:mm aa',\n overwrite: true,\n autofix: true,\n lazy: false,\n min: new Date(1970, 0, 1),\n max: new Date(2100, 0, 1),\n prepare: (str) => str.toUpperCase(),\n format: (value) => this.formatValue(value),\n parse: (str) => {\n const time = this.convertTime12to24(str);\n return DateUtil.parse(`${DateUtil.format(Date.now(), 'YYYY-MM-DD')}T${time}`);\n },\n blocks: {\n HH: {\n mask: MaskedRange,\n placeholderChar: '-',\n maxLength: 2,\n from: 0,\n to: 23,\n },\n hh: {\n mask: MaskedRange,\n placeholderChar: '-',\n maxLength: 2,\n from: 1,\n to: 12,\n },\n mm: {\n mask: MaskedRange,\n placeholderChar: '-',\n maxLength: 2,\n from: 0,\n to: 59,\n },\n aa: {\n mask: MaskedEnum,\n placeholderChar: '-',\n enum: ['AM', 'PM', 'am', 'pm', amFormat, pmFormat],\n },\n },\n };\n }\n\n _checkInput(event: InputEvent): void {\n if (document.activeElement === event.target) {\n const text = (event.target as HTMLInputElement).value;\n const hour = text.slice(0, 2);\n if ((this.military && Number(text[0]) > 2) || (!this.military && Number(text[0]) > 1)) {\n event.preventDefault();\n const value = `0${text}`;\n (event.target as HTMLInputElement).value = value;\n }\n if (!this.military) {\n const input = text.substr(5, 4).replace(/\\-/g, '').trim().slice(0, 2);\n const timePeriod = this.imask.blocks.aa.enum.find((it) => it[0] === input[0]);\n if (timePeriod) {\n (event.target as HTMLInputElement).value = `${text.slice(0, 5)} ${timePeriod}`;\n }\n if ((event.target as HTMLInputElement).selectionStart >= 3 && this.hourOneFormatRequired(hour)) {\n (event.target as HTMLInputElement).value = `01:${(event.target as HTMLInputElement).value.slice(\n 3,\n (event.target as HTMLInputElement).value.length,\n )}`;\n }\n }\n }\n }\n\n _handleBlur(event: FocusEvent): void {\n const text = (event.target as HTMLInputElement).value;\n const hour: string = text.slice(0, 2);\n if (!this.military) {\n const input = text.substr(5, 4).replace(/\\-/g, '').trim().slice(0, 2);\n const timePeriod = this.imask.blocks.aa.enum.find((it) => it[0] === input[0]);\n if (this.hourOneFormatRequired(hour)) {\n (event.target as HTMLInputElement).value = `01:${text.slice(3, text.length)}`;\n }\n if (!timePeriod) {\n (event.target as HTMLInputElement).value = `${text.slice(0, 5)} --`;\n }\n }\n }\n\n _handleKeydown(event: KeyboardEvent): void {\n const input = event.target as HTMLInputElement;\n const hour: string = input.value.slice(0, 2);\n\n if (event.key === Key.Backspace && input.selectionStart === input.value.length) {\n (event.target as HTMLInputElement).value = `${input.value.slice(0, 5)} --`;\n } else if (event.key === Key.Tab && input.selectionStart <= 2 && this.hourOneFormatRequired(hour)) {\n event.preventDefault();\n event.stopPropagation();\n event.stopImmediatePropagation();\n input.value = `01:${input.value.slice(3, input.value.length)}`;\n input.setSelectionRange(3, 3);\n } else if (event.key === Key.ArrowRight && input.selectionStart >= 2 && this.hourOneFormatRequired(hour)) {\n input.value = `01:${input.value.slice(3, input.value.length)}`;\n input.setSelectionRange(2, 2);\n }\n }\n\n normalize(value: string) {\n if (this.military) {\n return this.convertTime12to24(value);\n }\n return this.convertTime24to12(value);\n }\n\n formatValue(value: any): string {\n const date = DateUtil.parse(value);\n if (isValid(date)) {\n const pattern = this.military ? 'HH:mm' : 'hh:mm A';\n return DateUtil.format(date, pattern);\n }\n return this.normalize(value);\n }\n\n formatAsIso(date: Date): string {\n if (date && isValid(date)) {\n return DateUtil.format(date, 'HH:mm');\n }\n return null;\n }\n\n convertTime12to24(time12h: string) {\n const pmFormat = this.labels.timeFormatPM.toUpperCase();\n const [time, meridian] = time12h.split(' ');\n // eslint-disable-next-line prefer-const\n let [hours, minutes] = time.split(':');\n if (hours === '12') {\n hours = '00';\n }\n if (['PM', pmFormat].includes(meridian)) {\n hours = `${parseInt(hours, 10) + 12}`.padStart(2, '0');\n }\n return `${hours}:${minutes}`;\n }\n\n convertTime24to12(time24h: string) {\n if (time24h.length === 5) {\n const date = DateUtil.parse(`2020-01-01T${time24h}`);\n return DateUtil.format(date, 'hh:mm A');\n }\n return time24h;\n }\n\n writeValue(value: any) {\n super.writeValue(this.formatValue(value));\n this.valueChange.emit(value);\n }\n\n registerOnChange(fn: (date: any) => void): void {\n this.onChange = (date: any) => {\n let formatted = date;\n switch (this.timeFormat) {\n case TIME_FORMATS.ISO8601:\n formatted = this.formatAsIso(date);\n break;\n case TIME_FORMATS.STRING:\n formatted = this.formatValue(date);\n break;\n default:\n formatted = date;\n break;\n }\n this.valueChange.emit(date);\n fn(formatted);\n };\n }\n\n hourOneFormatRequired(hourInput: string): boolean {\n return hourInput === '-1' || hourInput === '1-';\n }\n}\n","/**\n * @license\n * Copyright Google LLC All Rights Reserved.\n *\n * Use of this source code is governed by an MIT-style license that can be\n * found in the LICENSE file at https://angular.io/license\n */\n\nimport { BooleanInput, coerceBooleanProperty } from '@angular/cdk/coercion';\nimport { getSupportedInputTypes, Platform } from '@angular/cdk/platform';\nimport { AutofillMonitor } from '@angular/cdk/text-field';\nimport {\n AfterViewInit,\n Directive,\n DoCheck,\n ElementRef,\n EventEmitter,\n HostBinding,\n HostListener,\n Inject,\n InjectionToken,\n Input,\n NgZone,\n OnChanges,\n OnDestroy,\n Optional,\n Output,\n Self,\n} from '@angular/core';\nimport { FormGroupDirective, NgControl, NgForm } from '@angular/forms';\nimport { Subject } from 'rxjs';\nimport { NovoFieldControl } from './field-control';\n\n/**\n * This token is used to inject the object whose value should be set into `NovoInput`. If none is\n * provided, the native `HTMLInputElement` is used. Directives like `MatDatepickerInput` can provide\n * themselves for this token, in order to make `NovoInput` delegate the getting and setting of the\n * value to them.\n */\nexport const NOVO_INPUT_VALUE_ACCESSOR = new InjectionToken<{ value: any }>('NOVO_INPUT_VALUE_ACCESSOR');\n\n// Invalid input type. Using one of these will throw an NovoInputUnsupportedTypeError.\nconst NOVO_INPUT_INVALID_TYPES = ['button', 'file', 'hidden', 'image', 'radio', 'reset', 'submit'];\n\nlet nextUniqueId = 0;\n\n// Boilerplate for applying mixins to NovoInput.\nclass NovoInputBase {\n constructor(\n public _parentForm: NgForm,\n public _parentFormGroup: FormGroupDirective,\n /** @docs-private */\n public ngControl: NgControl,\n ) {}\n}\n\n/** Directive that allows a native input to work inside a `NovoField`. */\n@Directive({\n selector: 'input[novoInput], textarea[novoInput], select[novoInput]',\n host: {\n class: 'novo-input-element',\n '[attr.id]': 'id',\n '[attr.placeholder]': 'placeholder',\n '[disabled]': 'disabled',\n '[required]': 'required',\n '[attr.readonly]': 'readonly && !_isNativeSelect || null',\n '[attr.aria-invalid]': 'errorState',\n '[attr.aria-required]': 'required.toString()',\n '[attr.autocomplete]': \"'off'\",\n },\n providers: [{ provide: NovoFieldControl, useExisting: NovoInput }],\n standalone: false,\n})\nexport class NovoInput extends NovoInputBase implements NovoFieldControl<any>, OnChanges, OnDestroy, AfterViewInit, DoCheck {\n protected _uid = `novo-input-${nextUniqueId++}`;\n protected _previousNativeValue: any;\n private _inputValueAccessor: { value: any };\n /** The aria-describedby attribute on the input for improved a11y. */\n @HostBinding('attr.aria-describedby') _ariaDescribedby: string;\n\n /** Whether the component is being rendered on the server. */\n readonly _isServer: boolean;\n\n /** Whether the component is a native html select. */\n readonly _isNativeSelect: boolean;\n\n /** Whether the component is a textarea. */\n readonly _isTextarea: boolean;\n\n /**\n * Implemented as part of NovoFieldControl.\n * @docs-private\n */\n focused: boolean = false;\n\n get errorState(): boolean {\n return this.ngControl && this.ngControl.invalid && (this.ngControl.dirty || this.ngControl.touched) || false;\n }\n\n /** @docs-private Implemented as part of NovoFieldControl. */\n lastKeyValue: string = null;\n /** @docs-private Implemented as part of NovoFieldControl.*/\n lastCaretPosition: number | null;\n\n /**\n * Implemented as part of NovoFieldControl.\n * @docs-private\n */\n readonly stateChanges: Subject<void> = new Subject<void>();\n\n /**\n * Implemented as part of NovoFieldControl.\n * @docs-private\n */\n controlType: string = 'novo-input';\n\n /**\n * Implemented as part of NovoFieldControl.\n * @docs-private\n */\n autofilled = false;\n\n /**\n * Implemented as part of NovoFieldControl.\n * @docs-private\n */\n @Input()\n get disabled(): boolean {\n if (this.ngControl && this.ngControl.disabled !== null) {\n return this.ngControl.disabled;\n }\n return this._disabled;\n }\n set disabled(value: boolean) {\n this._disabled = coerceBooleanProperty(value);\n\n // Browsers may not fire the blur event if the input is disabled too quickly.\n // Reset from here to ensure that the element doesn't become stuck.\n if (this.focused) {\n this.focused = false;\n this.stateChanges.next();\n }\n }\n protected _disabled = false;\n\n /**\n * Implemented as part of NovoFieldControl.\n * @docs-private\n */\n @Input()\n get id(): string {\n return this._id;\n }\n set id(value: string) {\n this._id = value || this._uid;\n }\n protected _id: string;\n\n /**\n * Implemented as part of NovoFieldControl.\n * @docs-private\n */\n @Input() placeholder: string;\n\n /**\n * Implemented as part of NovoFieldControl.\n * @docs-private\n */\n @Input()\n get required(): boolean {\n return this._required;\n }\n set required(value: boolean) {\n this._required = coerceBooleanProperty(value);\n }\n protected _required = false;\n\n /** Input type of the element. */\n @Input()\n get type(): string {\n return this._type;\n }\n set type(value: string) {\n this._type = value || 'text';\n this._validateType();\n\n // When using Angular inputs, developers are no longer able to set the properties on the native\n // input element. To ensure that bindings for `type` work, we need to sync the setter\n // with the native property. Textarea elements don't support the type property or attribute.\n if (!this._isTextarea && getSupportedInputTypes().has(this._type)) {\n (this._elementRef.nativeElement as HTMLInputElement).type = this._type;\n }\n }\n protected _type = 'text';\n\n /**\n * Implemented as part of NovoFieldControl.\n * @docs-private\n */\n @Input()\n get value(): string {\n return this._inputValueAccessor.value;\n }\n set value(value: string) {\n if (value !== this.value) {\n this._inputValueAccessor.value = value;\n this.stateChanges.next();\n }\n }\n\n /** Whether the element is readonly. */\n @Input()\n get readonly(): boolean {\n return this._readonly;\n }\n set readonly(value: boolean) {\n this._readonly = coerceBooleanProperty(value);\n }\n private _readonly = false;\n\n protected _neverEmptyInputTypes = ['date', 'datetime', 'datetime-local', 'month', 'time', 'week'].filter((t) =>\n getSupportedInputTypes().has(t),\n );\n\n constructor(\n protected _elementRef: ElementRef<HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement>,\n protected _platform: Platform,\n /** @docs-private */\n @Optional() @Self() public ngControl: NgControl,\n @Optional() _parentForm: NgForm,\n @Optional() _parentFormGroup: FormGroupDirective,\n @Optional() @Self() @Inject(NOVO_INPUT_VALUE_ACCESSOR) inputValueAccessor: any,\n private _autofillMonitor: AutofillMonitor,\n ngZone: NgZone,\n ) {\n super(_parentForm, _parentFormGroup, ngControl);\n\n const element = this._elementRef.nativeElement;\n const nodeName = element.nodeName.toLowerCase();\n\n // If no input value accessor was explicitly specified, use the element as the input value\n // accessor.\n this._inputValueAccessor = inputValueAccessor || element;\n\n this._previousNativeValue = this.value;\n\n // Force setter to be called in case id was not specified.\n this.id = this.id;\n\n // On some versions of iOS the caret gets stuck in the wrong place when holding down the delete\n // key. In order to get around this we need to \"jiggle\" the caret loose. Since this bug only\n // exists on iOS, we only bother to install the listener on iOS.\n if (_platform.IOS) {\n ngZone.runOutsideAngular(() => {\n _elementRef.nativeElement.addEventListener('keyup', (event: Event) => {\n const el = event.target as HTMLInputElement;\n if (!el.value && !el.selectionStart &