diff --git a/src/components/date-time-input/date-time-input.ts b/src/components/date-time-input/date-time-input.ts index 43d7e5572..24c73383d 100644 --- a/src/components/date-time-input/date-time-input.ts +++ b/src/components/date-time-input/date-time-input.ts @@ -22,7 +22,7 @@ import { partMap } from '../common/part-map.js'; import type { IgcInputComponentEventMap } from '../input/input-base.js'; import { IgcMaskInputBaseComponent, - type MaskRange, + type MaskSelection, } from '../mask-input/mask-input-base.js'; import IgcValidationContainerComponent from '../validation-container/validation-container.js'; import { @@ -213,15 +213,6 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin< } } - @watch('prompt', { waitUntilFirstUpdate: true }) - protected promptChange(): void { - if (!this.prompt) { - this.prompt = this.parser.prompt; - } else { - this.parser.prompt = this.prompt; - } - } - protected get hasDateParts(): boolean { const parts = this._inputDateParts || @@ -250,11 +241,11 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin< private get targetDatePart(): DatePart | undefined { let result: DatePart | undefined; - if (this.focused) { + if (this._focused) { const partType = this._inputDateParts.find( (p) => - p.start <= this.inputSelection.start && - this.inputSelection.start <= p.end && + p.start <= this._inputSelection.start && + this._inputSelection.start <= p.end && p.type !== DateParts.Literal )?.type as string as DatePart; @@ -307,7 +298,7 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin< return; } - const { start, end } = this.inputSelection; + const { start, end } = this._inputSelection; const newValue = this.trySpinValue(targetPart, delta); this.value = newValue; this.updateComplete.then(() => this.input.setSelectionRange(start, end)); @@ -321,7 +312,7 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin< return; } - const { start, end } = this.inputSelection; + const { start, end } = this._inputSelection; const newValue = this.trySpinValue(targetPart, delta, true); this.value = newValue; this.updateComplete.then(() => this.input.setSelectionRange(start, end)); @@ -329,80 +320,79 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin< /** Clears the input element of user input. */ public clear(): void { - this.maskedValue = ''; + this._maskedValue = ''; this.value = null; } protected setToday() { this.value = new Date(); - this.handleInput(); + this._fireInputEvent(); } protected updateMask() { - if (this.focused) { - this.maskedValue = this.getMaskedValue(); + if (this._focused) { + this._maskedValue = this.getMaskedValue(); } else { if (!DateTimeUtil.isValidDate(this.value)) { - this.maskedValue = ''; + this._maskedValue = ''; return; } const format = this.displayFormat || this.inputFormat; if (this.displayFormat) { - this.maskedValue = DateTimeUtil.formatDate( + this._maskedValue = DateTimeUtil.formatDate( this.value, this.locale, format, true ); } else if (this.inputFormat) { - this.maskedValue = DateTimeUtil.formatDate( + this._maskedValue = DateTimeUtil.formatDate( this.value, this.locale, format ); } else { - this.maskedValue = this.value.toLocaleString(); + this._maskedValue = this.value.toLocaleString(); } } } - protected override handleInput() { + private _fireInputEvent(): void { this._setTouchedState(); this.emitEvent('igcInput', { detail: this.value?.toString() }); } protected handleDragLeave() { - if (!this.focused) { + if (!this._focused) { this.updateMask(); } } protected handleDragEnter() { - if (!this.focused) { - this.maskedValue = this.getMaskedValue(); + if (!this._focused) { + this._maskedValue = this.getMaskedValue(); } } - protected async updateInput(string: string, range: MaskRange) { - const { value, end } = this.parser.replace( - this.maskedValue, - string, - range.start, - range.end - ); + protected async _updateInput( + text: string, + { start, end }: MaskSelection + ): Promise { + const result = this._parser.replace(this._maskedValue, text, start, end); - this.maskedValue = value; + this._maskedValue = result.value; this.updateValue(); this.requestUpdate(); - if (range.start !== this.inputFormat.length) { - this.handleInput(); + if (start !== this.inputFormat.length) { + this._fireInputEvent(); } + await this.updateComplete; - this.input.setSelectionRange(end, end); + this.input.setSelectionRange(result.end, result.end); } private trySpinValue( @@ -451,7 +441,7 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin< (dp) => dp.type === DateParts.AmPm ); if (formatPart !== undefined) { - amPmFromMask = this.maskedValue.substring( + amPmFromMask = this._maskedValue.substring( formatPart!.start, formatPart!.end ); @@ -465,16 +455,16 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin< @eventOptions({ passive: false }) private async onWheel(event: WheelEvent) { - if (!this.focused || this.readOnly) { + if (!this._focused || this.readOnly) { return; } event.preventDefault(); event.stopPropagation(); - const { start, end } = this.inputSelection; + const { start, end } = this._inputSelection; event.deltaY > 0 ? this.stepDown() : this.stepUp(); - this.handleInput(); + this._fireInputEvent(); await this.updateComplete; this.setSelectionRange(start, end); @@ -496,12 +486,7 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin< '0' ); - this._mask = newMask.includes('tt') - ? newMask.replace(/tt/g, 'LL') - : newMask; - - this.parser.mask = this._mask; - this.parser.prompt = this.prompt; + this.mask = newMask.includes('tt') ? newMask.replace(/tt/g, 'LL') : newMask; if (!this.placeholder || oldFormat === this.placeholder) { this.placeholder = value; @@ -515,7 +500,7 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin< } private getMaskedValue(): string { - let mask = this.emptyMask; + let mask = this._parser.emptyMask; if (DateTimeUtil.isValidDate(this.value)) { for (const part of this._inputDateParts) { @@ -529,7 +514,7 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin< this.value ); - mask = this.parser.replace( + mask = this._parser.replace( mask, targetValue, part.start, @@ -539,16 +524,16 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin< return mask; } - return this.maskedValue === '' ? mask : this.maskedValue; + return this._maskedValue === '' ? mask : this._maskedValue; } private isComplete(): boolean { - return !this.maskedValue.includes(this.prompt); + return !this._maskedValue.includes(this.prompt); } private updateValue(): void { if (this.isComplete()) { - const parsedDate = this.parseDate(this.maskedValue); + const parsedDate = this.parseDate(this._maskedValue); this.value = DateTimeUtil.isValidDate(parsedDate) ? parsedDate : null; } else { this.value = null; @@ -560,7 +545,7 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin< } private getNewPosition(value: string, direction = 0): number { - const cursorPos = this.selection.start; + const cursorPos = this._maskSelection.start; if (!direction) { // Last literal before the current cursor position or start of input value @@ -578,7 +563,7 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin< } protected async handleFocus() { - this.focused = true; + this._focused = true; if (this.readOnly) { return; @@ -588,7 +573,7 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin< const areFormatsDifferent = this.displayFormat !== this.inputFormat; if (!this.value) { - this.maskedValue = this.emptyMask; + this._maskedValue = this._parser.emptyMask; await this.updateComplete; this.select(); } else if (areFormatsDifferent) { @@ -597,18 +582,18 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin< } protected handleBlur() { - const isEmptyMask = this.maskedValue === this.emptyMask; + const isEmptyMask = this._maskedValue === this._parser.emptyMask; - this.focused = false; + this._focused = false; if (!(this.isComplete() || isEmptyMask)) { - const parse = this.parseDate(this.maskedValue); + const parse = this.parseDate(this._maskedValue); if (parse) { this.value = parse; } else { this.value = null; - this.maskedValue = ''; + this._maskedValue = ''; } } else { this.updateMask(); @@ -630,9 +615,9 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin< protected async keyboardSpin(direction: 'up' | 'down') { direction === 'up' ? this.stepUp() : this.stepDown(); - this.handleInput(); + this._fireInputEvent(); await this.updateComplete; - this.setSelectionRange(this.selection.start, this.selection.end); + this.setSelectionRange(this._maskSelection.start, this._maskSelection.end); } protected override renderInput() { @@ -641,22 +626,22 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin< type="text" part=${partMap(this.resolvePartNames('input'))} name=${ifDefined(this.name)} - .value=${live(this.maskedValue)} - .placeholder=${live(this.placeholder || this.emptyMask)} + .value=${live(this._maskedValue)} + .placeholder=${this.placeholder || this._parser.emptyMask} ?readonly=${this.readOnly} ?disabled=${this.disabled} @blur=${this.handleBlur} @focus=${this.handleFocus} - @input=${super.handleInput} + @input=${this._handleInput} @wheel=${this.onWheel} - @keydown=${super.handleKeydown} - @click=${this.handleClick} - @cut=${this.handleCut} - @compositionstart=${this.handleCompositionStart} - @compositionend=${this.handleCompositionEnd} + @keydown=${this._setMaskSelection} + @click=${this._handleClick} + @cut=${this._setMaskSelection} + @compositionstart=${this._handleCompositionStart} + @compositionend=${this._handleCompositionEnd} @dragenter=${this.handleDragEnter} @dragleave=${this.handleDragLeave} - @dragstart=${this.handleDragStart} + @dragstart=${this._setMaskSelection} /> `; } diff --git a/src/components/date-time-input/date-util.ts b/src/components/date-time-input/date-util.ts index 53181570e..04672f142 100644 --- a/src/components/date-time-input/date-util.ts +++ b/src/components/date-time-input/date-util.ts @@ -1,5 +1,5 @@ import { parseISODate } from '../calendar/helpers.js'; -import { MaskParser } from '../mask-input/mask-parser.js'; +import { clamp } from '../common/util.js'; export enum FormatDesc { Numeric = 'numeric', @@ -60,7 +60,6 @@ export abstract class DateTimeUtil { 'long', 'full', ]); - private static _parser = new MaskParser(); public static parseValueFromMask( inputData: string, @@ -288,7 +287,7 @@ export abstract class DateTimeUtil { case DateParts.Hours: if (datePartInfo.format.indexOf('h') !== -1) { maskedValue = DateTimeUtil.prependValue( - DateTimeUtil.toTwelveHourFormat(_dateValue!.getHours().toString()), + DateTimeUtil.toTwelveHourFormat(_dateValue!.getHours()), partLength, '0' ); @@ -314,6 +313,27 @@ export abstract class DateTimeUtil { return maskedValue; } + private static _spinTimePart( + newDate: Date, + delta: number, + max: number, + min: number, + setter: (value: number) => number, + getter: () => number, + spinLoop: boolean + ): void { + const range = max - min + 1; + let newValue = getter.call(newDate) + delta; + + if (spinLoop) { + newValue = min + ((((newValue - min) % range) + range) % range); + } else { + newValue = clamp(newValue, min, max); + } + + setter.call(newDate, newValue); + } + public static spinYear(delta: number, newDate: Date): Date { const maxDate = DateTimeUtil.daysInMonth( newDate.getFullYear() + delta, @@ -377,16 +397,15 @@ export abstract class DateTimeUtil { newDate: Date, spinLoop: boolean ): void { - const maxHour = 23; - const minHour = 0; - let hours = newDate.getHours() + delta; - if (hours > maxHour) { - hours = spinLoop ? (hours % maxHour) - 1 : maxHour; - } else if (hours < minHour) { - hours = spinLoop ? maxHour + (hours % maxHour) + 1 : minHour; - } - - newDate.setHours(hours); + DateTimeUtil._spinTimePart( + newDate, + delta, + 23, + 0, + newDate.setHours, + newDate.getHours, + spinLoop + ); } public static spinMinutes( @@ -394,16 +413,15 @@ export abstract class DateTimeUtil { newDate: Date, spinLoop: boolean ): void { - const maxMinutes = 59; - const minMinutes = 0; - let minutes = newDate.getMinutes() + delta; - if (minutes > maxMinutes) { - minutes = spinLoop ? (minutes % maxMinutes) - 1 : maxMinutes; - } else if (minutes < minMinutes) { - minutes = spinLoop ? maxMinutes + (minutes % maxMinutes) + 1 : minMinutes; - } - - newDate.setMinutes(minutes); + DateTimeUtil._spinTimePart( + newDate, + delta, + 59, + 0, + newDate.setMinutes, + newDate.getMinutes, + spinLoop + ); } public static spinSeconds( @@ -411,16 +429,15 @@ export abstract class DateTimeUtil { newDate: Date, spinLoop: boolean ): void { - const maxSeconds = 59; - const minSeconds = 0; - let seconds = newDate.getSeconds() + delta; - if (seconds > maxSeconds) { - seconds = spinLoop ? (seconds % maxSeconds) - 1 : maxSeconds; - } else if (seconds < minSeconds) { - seconds = spinLoop ? maxSeconds + (seconds % maxSeconds) + 1 : minSeconds; - } - - newDate.setSeconds(seconds); + DateTimeUtil._spinTimePart( + newDate, + delta, + 59, + 0, + newDate.setSeconds, + newDate.getSeconds, + spinLoop + ); } public static spinAmPm( @@ -854,20 +871,8 @@ export abstract class DateTimeUtil { return (prependChar + value.toString()).slice(-partLength); } - private static toTwelveHourFormat(value: string): number { - let hour = Number.parseInt( - value.replace( - new RegExp(DateTimeUtil.escapeRegExp(DateTimeUtil._parser.prompt), 'g'), - '0' - ), - 10 - ); - if (hour > 12) { - hour -= 12; - } else if (hour === 0) { - hour = 12; - } - - return hour; + private static toTwelveHourFormat(value: number): number { + const hour12 = value % 12; + return hour12 === 0 ? 12 : hour12; } } diff --git a/src/components/mask-input/mask-input-base.ts b/src/components/mask-input/mask-input-base.ts index 4e4a73fdf..d8bb22f2e 100644 --- a/src/components/mask-input/mask-input-base.ts +++ b/src/components/mask-input/mask-input-base.ts @@ -1,141 +1,164 @@ import { property, state } from 'lit/decorators.js'; - import { blazorDeepImport } from '../common/decorators/blazorDeepImport.js'; import { IgcInputBaseComponent } from '../input/input-base.js'; import type { RangeTextSelectMode, SelectionRangeDirection } from '../types.js'; import { MaskParser } from './mask-parser.js'; -export type MaskRange = { +export type MaskSelection = { start: number; end: number; }; @blazorDeepImport export abstract class IgcMaskInputBaseComponent extends IgcInputBaseComponent { - protected parser = new MaskParser(); - protected selection: MaskRange = { start: 0, end: 0 }; - protected compositionStart = 0; + //#region Internal state and properties - @state() - protected focused = false; + protected readonly _parser = new MaskParser(); - @state() - protected maskedValue = ''; + protected _maskSelection: MaskSelection = { start: 0, end: 0 }; + protected compositionStart = 0; @state() - protected _mask = ''; + protected _focused = false; - /** The prompt symbol to use for unfilled parts of the mask. */ - @property() - public prompt!: string; + @state() + protected _maskedValue = ''; - protected get inputSelection(): MaskRange { + protected get _inputSelection(): MaskSelection { return { start: this.input.selectionStart || 0, end: this.input.selectionEnd || 0, }; } - protected get emptyMask(): string { - return this.parser.apply(); + //#endregion + + //#region Public attributes and properties + + /** + * The masked pattern of the component. + * + * @attr + * @default 'CCCCCCCCCC' + */ + @property() + public set mask(value: string) { + this._parser.mask = value; } - public override connectedCallback() { - super.connectedCallback(); + public get mask(): string { + return this._parser.mask; + } - this._mask = this._mask || this.parser.mask; - this.prompt = this.prompt || this.parser.prompt; + /** + * The prompt symbol to use for unfilled parts of the mask pattern. + * + * @attr + * @default '_' + */ + @property() + public set prompt(value: string) { + this._parser.prompt = value; } - /** Selects all text within the input. */ - public select() { - this.input.select(); + public get prompt(): string { + return this._parser.prompt; } - protected handleInput({ inputType, isComposing }: InputEvent) { - const EMPTY = ''; + //#endregion + + //#region Event handlers + + protected async _handleInput({ + inputType, + isComposing, + }: InputEvent): Promise { const value = this.input.value; - const { start, end } = this.selection; - const deleteEnd = this.parser.getNextNonLiteralPosition(end) + 1; + const { start, end } = this._maskSelection; + const deletePosition = this._parser.getNextNonLiteralPosition(end) + 1; this._setTouchedState(); switch (inputType) { case 'deleteContentForward': - this.updateInput(EMPTY, { start, end: deleteEnd }); - return this.updateComplete.then(() => - this.input.setSelectionRange(deleteEnd, deleteEnd) - ); + this._updateInput('', { start, end: deletePosition }); + await this.updateComplete; + return this.input.setSelectionRange(deletePosition, deletePosition); case 'deleteContentBackward': if (isComposing) return; - return this.updateInput(EMPTY, { - start: this.parser.getPreviousNonLiteralPosition( - this.inputSelection.start + return this._updateInput('', { + start: this._parser.getPreviousNonLiteralPosition( + this._inputSelection.start ), end, }); case 'deleteByCut': - return this.updateInput(EMPTY, this.selection); + return this._updateInput('', this._maskSelection); case 'insertText': - return this.updateInput( - value.substring(start, this.inputSelection.end), - this.selection + return this._updateInput( + value.substring(start, this._inputSelection.end), + this._maskSelection ); case 'insertFromPaste': - return this.updateInput( - value.substring(start, this.inputSelection.end), + return this._updateInput( + value.substring(start, this._inputSelection.end), { start, - end: this.inputSelection.start, + end: this._inputSelection.start, } ); case 'insertFromDrop': - return this.updateInput( - value.substring(this.inputSelection.start, this.inputSelection.end), - { ...this.inputSelection } + return this._updateInput( + value.substring(this._inputSelection.start, this._inputSelection.end), + { ...this._inputSelection } ); } } - protected handleKeydown({ key }: KeyboardEvent) { - if (!key) { - return; - } - this.selection = this.inputSelection; - } - - protected handleCut() { - this.selection = this.inputSelection; - } - - protected handleDragStart() { - this.selection = this.inputSelection; + protected _setMaskSelection(): void { + this._maskSelection = this._inputSelection; } - protected handleCompositionStart() { - this.compositionStart = this.inputSelection.start; + protected _handleCompositionStart(): void { + this.compositionStart = this._inputSelection.start; } - protected handleCompositionEnd({ data }: CompositionEvent) { - this.updateInput(data, { + protected _handleCompositionEnd({ data }: CompositionEvent): void { + this._updateInput(data, { start: this.compositionStart, - end: this.inputSelection.end, + end: this._inputSelection.end, }); } - protected handleClick() { + protected _handleClick(): void { const { selectionStart: start, selectionEnd: end } = this.input; // Clicking at the end of the input field will select the entire mask - if (start === end && start === this.maskedValue.length) { + if (start === end && start === this._maskedValue.length) { this.select(); } } + //#endregion + + //#region Internal methods + + protected abstract _updateSetRangeTextValue(): void; + protected abstract _updateInput(text: string, range: MaskSelection): void; + + //#endregion + + //#region Public methods + + /** Selects all text within the input. */ + public select(): void { + this.input.select(); + } + /* blazorSuppress */ public override setSelectionRange( start: number, @@ -143,7 +166,7 @@ export abstract class IgcMaskInputBaseComponent extends IgcInputBaseComponent { direction?: SelectionRangeDirection ): void { super.setSelectionRange(start, end, direction); - this.selection = { start, end }; + this._maskSelection = { start, end }; } /* blazorSuppress */ @@ -154,17 +177,17 @@ export abstract class IgcMaskInputBaseComponent extends IgcInputBaseComponent { end?: number, selectMode?: RangeTextSelectMode ) { - const current = this.inputSelection; + const current = this._inputSelection; const _start = start ?? current.start; const _end = end ?? current.end; - const result = this.parser.replace( - this.maskedValue || this.emptyMask, + const result = this._parser.replace( + this._maskedValue || this._parser.emptyMask, replacement, _start, _end ); - this.maskedValue = this.parser.apply(this.parser.parse(result.value)); + this._maskedValue = this._parser.apply(this._parser.parse(result.value)); this._updateSetRangeTextValue(); this.updateComplete.then(() => { @@ -184,6 +207,5 @@ export abstract class IgcMaskInputBaseComponent extends IgcInputBaseComponent { }); } - protected abstract _updateSetRangeTextValue(): void; - protected abstract updateInput(string: string, range: MaskRange): void; + //#endregion } diff --git a/src/components/mask-input/mask-input.ts b/src/components/mask-input/mask-input.ts index 519473ea0..db7384e91 100644 --- a/src/components/mask-input/mask-input.ts +++ b/src/components/mask-input/mask-input.ts @@ -2,8 +2,6 @@ import { html, nothing } from 'lit'; import { property } from 'lit/decorators.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import { live } from 'lit/directives/live.js'; - -import { watch } from '../common/decorators/watch.js'; import { registerComponent } from '../common/definitions/register.js'; import { createFormValueState } from '../common/mixins/forms/form-value.js'; import { partMap } from '../common/part-map.js'; @@ -12,7 +10,7 @@ import type { MaskInputValueMode } from '../types.js'; import IgcValidationContainerComponent from '../validation-container/validation-container.js'; import { IgcMaskInputBaseComponent, - type MaskRange, + type MaskSelection, } from './mask-input-base.js'; import { maskValidators } from './validators.js'; @@ -44,10 +42,12 @@ export default class IgcMaskInputComponent extends IgcMaskInputBaseComponent { public static readonly tagName = 'igc-mask-input'; /* blazorSuppress */ - public static register() { + public static register(): void { registerComponent(IgcMaskInputComponent, IgcValidationContainerComponent); } + //#region Internal attributes and properties + protected override get __validators() { return maskValidators; } @@ -56,116 +56,109 @@ export default class IgcMaskInputComponent extends IgcMaskInputBaseComponent { initialValue: '', transformers: { setFormValue: (value) => - this._isRawMode ? value || null : this.maskedValue || null, + this._isRawMode ? value || null : this._maskedValue || null, }, }); - protected get _isRawMode() { + protected get _isRawMode(): boolean { return this.valueMode === 'raw'; } + //#endregion + + //#region Public attributes and properties + /** * Dictates the behavior when retrieving the value of the control: * * - `raw` will return the clean user input. * - `withFormatting` will return the value with all literals and prompts. + * * @attr value-mode + * @default 'raw' */ @property({ attribute: 'value-mode' }) public valueMode: MaskInputValueMode = 'raw'; + /* @tsTwoWayProperty(true, "igcChange", "detail", false) */ /** * The value of the input. * * Regardless of the currently set `value-mode`, an empty value will return an empty string. + * * @attr */ + @property() + public set value(string: string) { + const value = string ?? ''; + this._maskedValue = this._parser.apply(value); + this._updateMaskedValue(); + this._formValue.setValueAndFormState(value); + } + public get value(): string { const value = this._formValue.value; if (this._isRawMode) { return value; } - return value ? this.maskedValue : value; - } - - /* @tsTwoWayProperty(true, "igcChange", "detail", false) */ - @property() - public set value(string: string) { - const value = string ?? ''; - this.maskedValue = this.parser.apply(value); - this.updateMaskedValue(); - this._formValue.setValueAndFormState(value); + return value ? this._maskedValue : value; } /** - * The mask pattern to apply on the input. + * The masked pattern of the component. + * * @attr + * @default 'CCCCCCCCCC' */ @property() - public get mask(): string { - return this._mask; - } - - /** The mask pattern to apply on the input. */ - public set mask(value: string) { - this._mask = value; - this.parser.mask = value; + public override set mask(value: string) { + super.mask = value; if (this.value) { - this.maskedValue = this.parser.apply(this._formValue.value); + this._maskedValue = this._parser.apply(this._formValue.value); } } - @watch('prompt') - protected promptChange() { - this.parser.prompt = this.prompt; + public override get mask(): string { + return super.mask; + } + + /** + * The prompt symbol to use for unfilled parts of the mask pattern. + * + * @attr + * @default '_' + */ + @property() + public override set prompt(value: string) { + super.prompt = value; if (this.value) { - this.maskedValue = this.parser.apply(this._formValue.value); + this._maskedValue = this._parser.apply(this._formValue.value); } } - protected override _restoreDefaultValue(): void { - const value = this.defaultValue as string; - - this.maskedValue = this.parser.apply(value); - this.updateMaskedValue(); - this._formValue.setValueAndFormState(value); + public override get prompt(): string { + return super.prompt; } - protected async updateInput(string: string, range: MaskRange) { - const { value, end } = this.parser.replace( - this.maskedValue, - string, - range.start, - range.end - ); + //#endregion - this.maskedValue = value; - this._formValue.setValueAndFormState(this.parser.parse(value)); - this.requestUpdate(); + //#region Event handlers - if (range.start !== this.mask.length) { - this.emitEvent('igcInput', { detail: this.value }); + protected _handleDragEnter(): void { + if (!this._focused && !this._formValue.value) { + this._maskedValue = this._parser.emptyMask; } - await this.updateComplete; - - this.input.setSelectionRange(end, end); } - protected handleDragEnter() { - if (!this.focused && !this._formValue.value) { - this.maskedValue = this.emptyMask; + protected _handleDragLeave(): void { + if (!this._focused) { + this._updateMaskedValue(); } } - protected handleDragLeave() { - if (!this.focused) { - this.updateMaskedValue(); - } - } - - protected async handleFocus() { - this.focused = true; + protected async _handleFocus(): Promise { + this._focused = true; if (this.readOnly) { return; @@ -173,59 +166,91 @@ export default class IgcMaskInputComponent extends IgcMaskInputBaseComponent { if (!this._formValue.value) { // In case of empty value, select the whole mask - this.maskedValue = this.emptyMask; + this._maskedValue = this._parser.emptyMask; await this.updateComplete; this.select(); } } - protected override _handleBlur() { - this.focused = false; - this.updateMaskedValue(); + protected override _handleBlur(): void { + this._focused = false; + this._updateMaskedValue(); super._handleBlur(); } - protected handleChange() { + protected _handleChange(): void { this._setTouchedState(); this.emitEvent('igcChange', { detail: this.value }); } - protected updateMaskedValue() { - if (this.maskedValue === this.emptyMask) { - this.maskedValue = ''; + //#endregion + + //#region Internal methods + + protected override _restoreDefaultValue(): void { + const value = this.defaultValue as string; + + this._maskedValue = this._parser.apply(value); + this._updateMaskedValue(); + this._formValue.setValueAndFormState(value); + } + + protected async _updateInput( + text: string, + { start, end }: MaskSelection + ): Promise { + const result = this._parser.replace(this._maskedValue, text, start, end); + + this._maskedValue = result.value; + this._formValue.setValueAndFormState(this._parser.parse(this._maskedValue)); + this.requestUpdate(); + + if (start !== this.mask.length) { + this.emitEvent('igcInput', { detail: this.value }); } + + await this.updateComplete; + this.input.setSelectionRange(result.end, result.end); } - protected override _updateSetRangeTextValue() { - this.value = this.parser.parse(this.maskedValue); + protected override _updateSetRangeTextValue(): void { + this.value = this._parser.parse(this._maskedValue); } + private _updateMaskedValue(): void { + if (this._maskedValue === this._parser.emptyMask) { + this._maskedValue = ''; + } + } + + //#endregion + protected override renderInput() { return html` `; } diff --git a/src/components/mask-input/mask-parser.ts b/src/components/mask-input/mask-parser.ts index baf5fe7fe..17d862a65 100644 --- a/src/components/mask-input/mask-parser.ts +++ b/src/components/mask-input/mask-parser.ts @@ -103,6 +103,13 @@ export class MaskParser { return this._escapedMask; } + /** + * Returns the result of applying an empty string over the mask pattern. + */ + public get emptyMask(): string { + return this.apply(); + } + /** * Gets the unescaped mask string (the original format string). * If the mask has no escape sequences, then `mask === escapedMask`. diff --git a/src/components/mask-input/validators.ts b/src/components/mask-input/validators.ts index ee6858547..599af3b13 100644 --- a/src/components/mask-input/validators.ts +++ b/src/components/mask-input/validators.ts @@ -8,6 +8,6 @@ export const maskValidators: Validator[] = [ key: 'badInput', message: messages.mask, // @ts-expect-error - protected access - isValid: ({ parser, maskedValue }) => parser.isValidString(maskedValue), + isValid: ({ _parser, _maskedValue }) => _parser.isValidString(_maskedValue), }, ];