diff --git a/src/components/calendar/calendar.ts b/src/components/calendar/calendar.ts index d35c9247b..0cfeec53e 100644 --- a/src/components/calendar/calendar.ts +++ b/src/components/calendar/calendar.ts @@ -1,3 +1,4 @@ +import { getDateFormatter } from 'igniteui-i18n-core'; import { html, nothing } from 'lit'; import { property, query, queryAll, state } from 'lit/decorators.js'; import { choose } from 'lit/directives/choose.js'; @@ -15,9 +16,7 @@ import { pageUpKey, shiftKey, } from '../common/controllers/key-bindings.js'; -import { watch } from '../common/decorators/watch.js'; import { registerComponent } from '../common/definitions/register.js'; -import { createDateTimeFormatters } from '../common/localization/intl-formatters.js'; import type { Constructor } from '../common/mixins/constructor.js'; import { EventEmitterMixin } from '../common/mixins/event-emitter.js'; import { partMap } from '../common/part-map.js'; @@ -219,30 +218,21 @@ export default class IgcCalendarComponent extends EventEmitterMixin< public formatOptions: Pick = { month: 'long', weekday: 'narrow' }; - private _intl = createDateTimeFormatters(this.locale, { - month: { - month: this.formatOptions.month, - }, - monthLabel: { month: 'long' }, - weekday: { weekday: 'short' }, - monthDay: { month: 'short', day: 'numeric' }, - yearLabel: { month: 'long', year: 'numeric' }, - }); - - @watch('locale') - protected localeChange() { - this._intl.locale = this.locale; - } - - @watch('formatOptions') - protected formatOptionsChange() { - this._intl.update({ - month: { - month: this.formatOptions.month, - }, - }); + private get _monthOptions(): Intl.DateTimeFormatOptions { + return { month: this.formatOptions.month }; } + private _monthLabelOptions: Intl.DateTimeFormatOptions = { month: 'long' }; + private _weekdayOptions: Intl.DateTimeFormatOptions = { weekday: 'short' }; + private _monthDayOptions: Intl.DateTimeFormatOptions = { + month: 'short', + day: 'numeric', + }; + private _yearLabelOptions: Intl.DateTimeFormatOptions = { + month: 'long', + year: 'numeric', + }; + /** @private @hidden @internal */ public async [focusActiveDate](options?: FocusOptions) { await this.updateComplete; @@ -462,9 +452,17 @@ export default class IgcCalendarComponent extends EventEmitterMixin< active: CalendarDay, viewIndex: number ) { - const labelFmt = this._intl.get('monthLabel').format; - const valueFmt = this._intl.get('month').format; - const ariaLabel = `${labelFmt(active.native)}, ${this.resourceStrings.selectMonth}`; + const label = getDateFormatter().formatDateTime( + active.native, + this.locale, + this._monthLabelOptions + ); + const value = getDateFormatter().formatDateTime( + active.native, + this.locale, + this._monthOptions + ); + const ariaLabel = `${label}, ${this.resourceStrings.selectMonth}`; return html` `; } protected renderYearButtonNavigation(active: CalendarDay, viewIndex: number) { - const fmt = this._intl.get('yearLabel').format; + const fmt = getDateFormatter().getIntlFormatter( + this.locale, + this._yearLabelOptions + ).format; const ariaLabel = `${active.year}, ${this.resourceStrings.selectYear}`; const ariaSkip = this._isDayView ? fmt(active.native) : active.year; @@ -548,19 +549,30 @@ export default class IgcCalendarComponent extends EventEmitterMixin< protected renderHeaderDateSingle() { const date = this.value ?? CalendarDay.today.native; - const day = this._intl.get('weekday').format(date); - const month = this._intl.get('monthDay').format(date); + const weekday = getDateFormatter().formatDateTime( + date, + this.locale, + this._weekdayOptions + ); + const monthDay = getDateFormatter().formatDateTime( + date, + this.locale, + this._monthDayOptions + ); const separator = this.headerOrientation === 'vertical' ? html`
` : ' '; - const formatted = html`${day},${separator}${month}`; + const formatted = html`${weekday},${separator}${monthDay}`; return html`${formatted}`; } protected renderHeaderDateRange() { const values = this.values; - const fmt = this._intl.get('monthDay').format; + const fmt = getDateFormatter().getIntlFormatter( + this.locale, + this._monthDayOptions + ).format; const { startDate, endDate } = this.resourceStrings; const start = this._hasValues ? fmt(first(values)) : startDate; diff --git a/src/components/calendar/days-view/days-view.ts b/src/components/calendar/days-view/days-view.ts index 05c8b1ee9..6917de923 100644 --- a/src/components/calendar/days-view/days-view.ts +++ b/src/components/calendar/days-view/days-view.ts @@ -1,13 +1,12 @@ +import { getDateFormatter } from 'igniteui-i18n-core'; import { html, nothing } from 'lit'; import { property, query, state } from 'lit/decorators.js'; - import { addThemingController } from '../../../theming/theming-controller.js'; import { addKeybindings } from '../../common/controllers/key-bindings.js'; import { blazorIndirectRender } from '../../common/decorators/blazorIndirectRender.js'; import { blazorSuppressComponent } from '../../common/decorators/blazorSuppressComponent.js'; import { watch } from '../../common/decorators/watch.js'; import { registerComponent } from '../../common/definitions/register.js'; -import { createDateTimeFormatters } from '../../common/localization/intl-formatters.js'; import type { Constructor } from '../../common/mixins/constructor.js'; import { EventEmitterMixin } from '../../common/mixins/event-emitter.js'; import { partMap } from '../../common/part-map.js'; @@ -107,21 +106,17 @@ export default class IgcDaysViewComponent extends EventEmitterMixin< @property({ attribute: 'week-day-format' }) public weekDayFormat: 'long' | 'short' | 'narrow' = 'narrow'; - private _intl = createDateTimeFormatters(this.locale, { - weekday: { weekday: this.weekDayFormat }, - label: { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }, - ariaWeekday: { weekday: 'long' }, - }); - - @watch('locale') - protected localeChange() { - this._intl.locale = this.locale; + private get _weekdayOptions(): Intl.DateTimeFormatOptions { + return { weekday: this.weekDayFormat }; } - @watch('weekDayFormat') - protected weekDayFormatChange() { - this._intl.update({ weekday: { weekday: this.weekDayFormat } }); - } + private _labelOptions: Intl.DateTimeFormatOptions = { + weekday: 'long', + year: 'numeric', + month: 'long', + day: 'numeric', + }; + private _ariaWeekdayOptions: Intl.DateTimeFormatOptions = { weekday: 'long' }; @watch('weekStart') @watch('activeDate') @@ -336,7 +331,10 @@ export default class IgcDaysViewComponent extends EventEmitterMixin< } private intlFormatDay(day: CalendarDay) { - const fmt = this._intl.get('label'); + const fmt = getDateFormatter().getIntlFormatter( + this.locale, + this._labelOptions + ); // Range selection in progress if (this._rangePreviewDate?.equalTo(day)) { @@ -444,8 +442,14 @@ export default class IgcDaysViewComponent extends EventEmitterMixin< } protected renderHeaders() { - const label = this._intl.get('weekday'); - const aria = this._intl.get('ariaWeekday'); + const label = getDateFormatter().getIntlFormatter( + this.locale, + this._weekdayOptions + ); + const aria = getDateFormatter().getIntlFormatter( + this.locale, + this._ariaWeekdayOptions + ); const days = take( generateMonth(this._activeDate, this._firstDayOfWeek), daysInWeek diff --git a/src/components/calendar/months-view/months-view.ts b/src/components/calendar/months-view/months-view.ts index 04fc46968..bf8a77d57 100644 --- a/src/components/calendar/months-view/months-view.ts +++ b/src/components/calendar/months-view/months-view.ts @@ -1,14 +1,12 @@ +import { getDateFormatter } from 'igniteui-i18n-core'; import { html, LitElement } from 'lit'; import { property, query, state } from 'lit/decorators.js'; import { range } from 'lit/directives/range.js'; - import { addThemingController } from '../../../theming/theming-controller.js'; import { addKeybindings } from '../../common/controllers/key-bindings.js'; import { blazorIndirectRender } from '../../common/decorators/blazorIndirectRender.js'; import { blazorSuppressComponent } from '../../common/decorators/blazorSuppressComponent.js'; -import { watch } from '../../common/decorators/watch.js'; import { registerComponent } from '../../common/definitions/register.js'; -import { createDateTimeFormatters } from '../../common/localization/intl-formatters.js'; import type { Constructor } from '../../common/mixins/constructor.js'; import { EventEmitterMixin } from '../../common/mixins/event-emitter.js'; import { partMap } from '../../common/part-map.js'; @@ -67,20 +65,14 @@ export default class IgcMonthsViewComponent extends EventEmitterMixin< public monthFormat: 'numeric' | '2-digit' | 'long' | 'short' | 'narrow' = 'long'; - private _intl = createDateTimeFormatters(this.locale, { - month: { month: this.monthFormat }, - ariaMonth: { month: 'long', year: 'numeric' }, - }); - - @watch('locale') - protected localeChange() { - this._intl.locale = this.locale; + private get _monthOptions(): Intl.DateTimeFormatOptions { + return { month: this.monthFormat }; } - @watch('monthFormat') - protected formatChange() { - this._intl.update({ month: { month: this.monthFormat } }); - } + private _ariaMonthOptions: Intl.DateTimeFormatOptions = { + month: 'long', + year: 'numeric', + }; constructor() { super(); @@ -110,8 +102,16 @@ export default class IgcMonthsViewComponent extends EventEmitterMixin< } protected renderMonth(entry: CalendarDay, now: CalendarDay) { - const ariaLabel = this._intl.get('ariaMonth').format(entry.native); - const value = this._intl.get('month').format(entry.native); + const ariaLabel = getDateFormatter().formatDateTime( + entry.native, + this.locale, + this._ariaMonthOptions + ); + const value = getDateFormatter().formatDateTime( + entry.native, + this.locale, + this._monthOptions + ); const active = areSameMonth(this._value, entry); const current = areSameMonth(now, entry); diff --git a/src/components/common/localization/intl-formatters.ts b/src/components/common/localization/intl-formatters.ts deleted file mode 100644 index a05119d22..000000000 --- a/src/components/common/localization/intl-formatters.ts +++ /dev/null @@ -1,66 +0,0 @@ -type DateTimeFormatterConfig = { - [name: string]: Intl.DateTimeFormatOptions; -}; - -const _cache = new Map(); - -function stringifyOptions(options: Intl.DateTimeFormatOptions) { - return Object.entries(options) - .map(([k, v]) => `${k}:${v}`) - .join('::'); -} - -function getFormatter(locale: string, options: Intl.DateTimeFormatOptions) { - const key = `${locale}#${stringifyOptions(options)}`; - - if (!_cache.has(key)) { - _cache.set(key, new Intl.DateTimeFormat(locale, options)); - } - - return _cache.get(key)!; -} - -class DateTimeFormatters { - private _formatters = new Map(); - - private _update(configuration: T) { - for (const [key, value] of Object.entries(configuration)) { - this._formatters.set(key, getFormatter(this._locale, value)); - } - } - - constructor( - private _locale: string, - private _configuration: T - ) { - this._update(_configuration); - } - - public get(name: keyof T) { - return this._formatters.get(name)!; - } - - public get configuration() { - return { ...this._configuration }; - } - - public get locale() { - return this._locale; - } - - public set locale(value: string) { - this._locale = value; - this._update(this._configuration); - } - - public update(configuration: Partial) { - this._update(Object.assign(this._configuration, configuration)); - } -} - -export function createDateTimeFormatters( - locale: string, - configuration: T -) { - return new DateTimeFormatters(locale, configuration); -} diff --git a/src/components/date-picker/date-picker.ts b/src/components/date-picker/date-picker.ts index 3665999ca..05b98a9d8 100644 --- a/src/components/date-picker/date-picker.ts +++ b/src/components/date-picker/date-picker.ts @@ -186,7 +186,6 @@ export default class IgcDatePickerComponent extends FormAssociatedRequiredMixin( private _max: Date | null = null; private _disabledDates?: DateRangeDescriptor[]; private _dateConstraints?: DateRangeDescriptor[]; - private _displayFormat?: string; private _inputFormat?: string; protected override readonly _formValue = createFormValueState(this, { @@ -391,19 +390,21 @@ export default class IgcDatePickerComponent extends FormAssociatedRequiredMixin( @property({ type: Boolean, reflect: true, attribute: 'show-week-numbers' }) public showWeekNumbers = false; + /** + * Sets to always show leading zero regardless of the displayFormat applied or one based on locale. + * Leading zero is applied during edit for the inputFormat always, regardless of this option. + * @attr + */ + @property({ type: Boolean, attribute: 'always-leading-zero' }) + public alwaysLeadingZero = false; + /** * Format to display the value in when not editing. - * Defaults to the input format if not set. + * Defaults to the locale format if not set. * @attr display-format */ @property({ attribute: 'display-format' }) - public set displayFormat(value: string) { - this._displayFormat = value; - } - - public get displayFormat(): string { - return this._displayFormat ?? this.inputFormat; - } + public displayFormat?: string; /** * The date format to apply on the input. @@ -794,7 +795,7 @@ export default class IgcDatePickerComponent extends FormAssociatedRequiredMixin( protected _renderInput(id: string) { const format = DateTimeUtil.predefinedToDateDisplayFormat( - this._displayFormat! + this.displayFormat ); // Dialog mode is always readonly, rest depends on configuration diff --git a/src/components/date-range-picker/date-range-picker.ts b/src/components/date-range-picker/date-range-picker.ts index fccc1957a..b6e31c0cc 100644 --- a/src/components/date-range-picker/date-range-picker.ts +++ b/src/components/date-range-picker/date-range-picker.ts @@ -441,9 +441,17 @@ export default class IgcDateRangePickerComponent extends FormAssociatedRequiredM @property() public prompt = '_'; + /** + * Sets to always show leading zero regardless of the displayFormat applied or one based on locale. + * Leading zero is applied during edit for the inputFormat always, regardless of this option. + * @attr + */ + @property({ type: Boolean, attribute: 'always-leading-zero' }) + public alwaysLeadingZero = false; + /** * Format to display the value in when not editing. - * Defaults to the input format if not set. + * Defaults to the locale format if not set. * @attr display-format */ @property({ attribute: 'display-format' }) @@ -452,8 +460,8 @@ export default class IgcDateRangePickerComponent extends FormAssociatedRequiredM this._updateMaskedRangeValue(); } - public get displayFormat(): string { - return this._displayFormat ?? this.inputFormat; + public get displayFormat(): string | undefined { + return this._displayFormat; } /** @@ -657,7 +665,7 @@ export default class IgcDateRangePickerComponent extends FormAssociatedRequiredM @watch('locale') protected _updateDefaultMask(): void { - this._defaultMask = DateTimeUtil.getDefaultMask(this.locale); + this._defaultMask = DateTimeUtil.getDefaultInputMask(this.locale); this._updateMaskedRangeValue(); } @@ -888,17 +896,26 @@ export default class IgcDateRangePickerComponent extends FormAssociatedRequiredM return; } - const { formatDate, predefinedToDateDisplayFormat } = DateTimeUtil; - + const { formatDisplayDate, predefinedToDateDisplayFormat } = DateTimeUtil; const { start, end } = this.value; - const format = - predefinedToDateDisplayFormat(this._displayFormat) ?? - this._displayFormat ?? - this.inputFormat; + const displayFormat = predefinedToDateDisplayFormat(this._displayFormat); - this._maskedRangeValue = format - ? `${formatDate(start, this.locale, format)} - ${formatDate(end, this.locale, format)}` - : `${start.toLocaleDateString()} - ${end.toLocaleDateString()}`; + const startValue = formatDisplayDate( + start, + this.locale, + displayFormat, + this.alwaysLeadingZero + ); + const endValue = formatDisplayDate( + end, + this.locale, + displayFormat, + this.alwaysLeadingZero + ); + this._maskedRangeValue = + displayFormat || this.inputFormat + ? `${startValue} - ${endValue}` + : `${start.toLocaleDateString()} - ${end.toLocaleDateString()}`; } private _setCalendarRangeValues() { @@ -1122,7 +1139,7 @@ export default class IgcDateRangePickerComponent extends FormAssociatedRequiredM picker === 'start' ? this.placeholderStart : this.placeholderEnd; const label = picker === 'start' ? this.labelStart : this.labelEnd; const format = DateTimeUtil.predefinedToDateDisplayFormat( - this._displayFormat! + this._displayFormat ); const value = picker === 'start' ? this.value?.start : this.value?.end; diff --git a/src/components/date-range-picker/date-range-picker.utils.spec.ts b/src/components/date-range-picker/date-range-picker.utils.spec.ts index e0dbed229..4f3871f09 100644 --- a/src/components/date-range-picker/date-range-picker.utils.spec.ts +++ b/src/components/date-range-picker/date-range-picker.utils.spec.ts @@ -52,17 +52,17 @@ export const checkSelectedRange = ( } else { const input = picker.renderRoot.querySelector(IgcInputComponent.tagName)!; const start = expectedValue?.start - ? DateTimeUtil.formatDate( + ? DateTimeUtil.formatDisplayDate( expectedValue.start, picker.locale, - picker.displayFormat || picker.inputFormat + picker.displayFormat ) : ''; const end = expectedValue?.end - ? DateTimeUtil.formatDate( + ? DateTimeUtil.formatDisplayDate( expectedValue.end, picker.locale, - picker.displayFormat || picker.inputFormat + picker.displayFormat ) : ''; expect(input.value).to.equal(`${start} - ${end}`); diff --git a/src/components/date-time-input/date-time-input.ts b/src/components/date-time-input/date-time-input.ts index 43d7e5572..9f6c6be05 100644 --- a/src/components/date-time-input/date-time-input.ts +++ b/src/components/date-time-input/date-time-input.ts @@ -1,8 +1,8 @@ +import { getCurrentI18n } from 'igniteui-i18n-core'; import { html } from 'lit'; import { eventOptions, property } from 'lit/decorators.js'; import { ifDefined } from 'lit/directives/if-defined.js'; import { live } from 'lit/directives/live.js'; - import { convertToDate } from '../calendar/helpers.js'; import { addKeybindings, @@ -88,6 +88,7 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin< }); protected _defaultMask!: string; + private _locale?: string; private _oldValue: Date | null = null; private _min: Date | null = null; private _max: Date | null = null; @@ -165,13 +166,21 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin< return this._max; } + /** + * Sets to always show leading zero regardless of the displayFormat applied or one based on locale. + * Leading zero is applied during edit for the inputFormat always, regardless of this option. + * @attr + */ + @property({ type: Boolean, attribute: 'always-leading-zero' }) + public alwaysLeadingZero = false; + /** * Format to display the value in when not editing. - * Defaults to the input format if not set. + * Defaults to the locale format if not set. * @attr display-format */ @property({ attribute: 'display-format' }) - public displayFormat!: string; + public displayFormat?: string; /** * Delta values used to increment or decrement each date part on step actions. @@ -192,7 +201,13 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin< * @attr */ @property() - public locale = 'en'; + public set locale(value: string) { + this._locale = value; + } + + public get locale() { + return this._locale ?? getCurrentI18n(); + } @watch('locale', { waitUntilFirstUpdate: true }) protected setDefaultMask(): void { @@ -206,6 +221,13 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin< } } + @watch('alwaysLeadingZero', { waitUntilFirstUpdate: true }) + protected setAlwaysLeadingZero(): void { + if (this.value) { + this.updateMask(); + } + } + @watch('displayFormat', { waitUntilFirstUpdate: true }) protected setDisplayFormat(): void { if (this.value) { @@ -347,24 +369,12 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin< return; } - const format = this.displayFormat || this.inputFormat; - - if (this.displayFormat) { - this.maskedValue = DateTimeUtil.formatDate( - this.value, - this.locale, - format, - true - ); - } else if (this.inputFormat) { - this.maskedValue = DateTimeUtil.formatDate( - this.value, - this.locale, - format - ); - } else { - this.maskedValue = this.value.toLocaleString(); - } + this.maskedValue = DateTimeUtil.formatDisplayDate( + this.value, + this.locale, + this.displayFormat, + this.alwaysLeadingZero + ); } } @@ -481,12 +491,12 @@ export default class IgcDateTimeInputComponent extends EventEmitterMixin< } private updateDefaultMask(): void { - this._defaultMask = DateTimeUtil.getDefaultMask(this.locale); + this._defaultMask = DateTimeUtil.getDefaultInputMask(this.locale); } private setMask(string: string): void { const oldFormat = this._inputDateParts?.map((p) => p.format).join(''); - this._inputDateParts = DateTimeUtil.parseDateTimeFormat(string); + this._inputDateParts = DateTimeUtil.parseDateTimeFormat(string, true); const value = this._inputDateParts.map((p) => p.format).join(''); this._defaultMask = value; diff --git a/src/components/date-time-input/date-util.spec.ts b/src/components/date-time-input/date-util.spec.ts index 7014369ae..4fd09d87a 100644 --- a/src/components/date-time-input/date-util.spec.ts +++ b/src/components/date-time-input/date-util.spec.ts @@ -9,10 +9,12 @@ describe('Date Util', () => { const DEFAULT_PROMPT = '_'; it('locale default mask', () => { - expect(DateTimeUtil.getDefaultMask('')).to.equal('MM/dd/yyyy'); - expect(DateTimeUtil.getDefaultMask(DEFAULT_LOCALE)).to.equal('MM/dd/yyyy'); - expect(DateTimeUtil.getDefaultMask('no')).to.equal('dd.MM.yyyy'); - expect(DateTimeUtil.getDefaultMask('bg').normalize('NFKC')).to.equal( + expect(DateTimeUtil.getDefaultInputMask('')).to.equal('MM/dd/yyyy'); + expect(DateTimeUtil.getDefaultInputMask(DEFAULT_LOCALE)).to.equal( + 'MM/dd/yyyy' + ); + expect(DateTimeUtil.getDefaultInputMask('no')).to.equal('dd.MM.yyyy'); + expect(DateTimeUtil.getDefaultInputMask('bg').normalize('NFKC')).to.equal( 'dd.MM.yyyy г.' ); }); diff --git a/src/components/date-time-input/date-util.ts b/src/components/date-time-input/date-util.ts index 53181570e..b0907e804 100644 --- a/src/components/date-time-input/date-util.ts +++ b/src/components/date-time-input/date-util.ts @@ -1,3 +1,4 @@ +import { getDateFormatter } from 'igniteui-i18n-core'; import { parseISODate } from '../calendar/helpers.js'; import { MaskParser } from '../mask-input/mask-parser.js'; @@ -52,8 +53,6 @@ function isDate(value: unknown): value is Date { export abstract class DateTimeUtil { public static readonly DEFAULT_INPUT_FORMAT = 'MM/dd/yyyy'; public static readonly DEFAULT_TIME_INPUT_FORMAT = 'hh:mm tt'; - private static readonly SEPARATOR = 'literal'; - private static readonly DEFAULT_LOCALE = 'en'; private static readonly PREDEFINED_FORMATS = new Set([ 'short', 'medium', @@ -128,32 +127,16 @@ export abstract class DateTimeUtil { ); } - public static getDefaultMask(locale: string): string { - const parts = DateTimeUtil.getDefaultLocaleMask( - locale || DateTimeUtil.DEFAULT_LOCALE - ); - - if (parts !== undefined) { - parts.forEach((p: any) => { - if (p.type !== DateParts.Year && p.type !== DateTimeUtil.SEPARATOR) { - p.formatType = FormatDesc.TwoDigits; - } - }); - - return DateTimeUtil.getMask(parts); - } - - return ''; + public static getDefaultInputMask(locale: string): string { + return getDateFormatter().getLocaleDateTimeFormat(locale, true); } public static parseDateTimeFormat( mask: string, - locale: string = DateTimeUtil.DEFAULT_LOCALE, - noLeadingZero = false + leadingZero = false ): DatePartInfo[] { - const format = mask || DateTimeUtil.getDefaultMask(locale); const dateTimeParts: DatePartInfo[] = []; - const formatArray = Array.from(format); + const formatArray = Array.from(mask); let currentPart: DatePartInfo | null = null; let position = 0; @@ -167,7 +150,7 @@ export abstract class DateTimeUtil { } } - DateTimeUtil.addCurrentPart(currentPart, dateTimeParts, noLeadingZero); + DateTimeUtil.addCurrentPart(currentPart, dateTimeParts, leadingZero); position = currentPart.end; } @@ -184,7 +167,7 @@ export abstract class DateTimeUtil { !dateTimeParts.filter((p) => p.format.includes(currentPart!.format)) .length ) { - DateTimeUtil.addCurrentPart(currentPart!, dateTimeParts, noLeadingZero); + DateTimeUtil.addCurrentPart(currentPart!, dateTimeParts, leadingZero); } // formats like "y" or "yyy" are treated like "yyyy" while editing const yearPart = dateTimeParts.filter((p) => p.type === DateParts.Year)[0]; @@ -208,54 +191,62 @@ export abstract class DateTimeUtil { return false; } - public static formatDate( + /** + * Format date for display. + * @param value Date value + * @param locale Locale of the component + * @param displayFormat Display format specified by the user. Can be undefined. + * @param inputFormat Input format, so it is not calculated again and used for leading zero format. + * @param leadingZero Should leading zero be applied? + * @returns + */ + public static formatDisplayDate( value: Date, locale: string, - format: string, - noLeadingZero = false + displayFormat: string | undefined, + leadingZero = false ): string { - const options: any = {}; - let formattedDate = ''; - - switch (format) { + let options: Intl.DateTimeFormatOptions = {}; + switch (displayFormat) { case 'short': case 'long': case 'medium': case 'full': - options.dateStyle = format; - options.timeStyle = format; + options.dateStyle = displayFormat; + options.timeStyle = displayFormat; break; case 'shortDate': case 'longDate': case 'mediumDate': case 'fullDate': - options.dateStyle = format.toLowerCase().split('date')[0]; + options.dateStyle = displayFormat.toLowerCase().split('date')[0] as any; break; case 'shortTime': case 'longTime': case 'mediumTime': case 'fullTime': - options.timeStyle = format.toLowerCase().split('time')[0]; + options.timeStyle = displayFormat.toLowerCase().split('time')[0] as any; break; default: - return DateTimeUtil.setDisplayFormatOptions( - value, - format, - locale, - noLeadingZero - ); + if (displayFormat) { + return getDateFormatter().formatDateCustomFormat( + value, + locale, + displayFormat, + leadingZero + ); + } } - let formatter: Intl.DateTimeFormat; - try { - formatter = new Intl.DateTimeFormat(locale, options); - } catch { - formatter = new Intl.DateTimeFormat(DateTimeUtil.DEFAULT_LOCALE, options); + if (leadingZero) { + options = { + day: '2-digit', + month: '2-digit', + year: 'numeric', + } as Intl.DateTimeFormatOptions; } - formattedDate = formatter.format(value); - - return formattedDate; + return getDateFormatter().formatDateTime(value, locale, options); } public static getPartValue( @@ -553,160 +544,24 @@ export abstract class DateTimeUtil { : format; } - private static setDisplayFormatOptions( - value: Date, - format: string, - locale: string, - noLeadingZero = false - ) { - const options: any = {}; - const parts = DateTimeUtil.parseDateTimeFormat( - format, - locale, - noLeadingZero - ); - - const datePartFormatOptionMap = new Map([ - [DateParts.Date, 'day'], - [DateParts.Month, 'month'], - [DateParts.Year, 'year'], - [DateParts.Hours, 'hour'], - [DateParts.Minutes, 'minute'], - [DateParts.Seconds, 'second'], - [DateParts.AmPm, 'dayPeriod'], - ]); - - const dateFormatMap = new Map([ - ['d', 'numeric'], - ['dd', '2-digit'], - ['M', 'numeric'], - ['MM', '2-digit'], - ['MMM', 'short'], - ['MMMM', 'long'], - ['MMMMM', 'narrow'], - ['y', 'numeric'], - ['yy', '2-digit'], - ['yyy', 'numeric'], - ['yyyy', 'numeric'], - ['h', 'numeric'], - ['hh', '2-digit'], - ['H', 'numeric'], - ['HH', '2-digit'], - ['m', 'numeric'], - ['mm', '2-digit'], - ['s', 'numeric'], - ['ss', '2-digit'], - ['ttt', 'short'], - ['tttt', 'long'], - ['ttttt', 'narrow'], - ]); - - for (const part of parts) { - if (part.type !== DateParts.Literal) { - const option = datePartFormatOptionMap.get(part.type); - const format = - dateFormatMap.get(part.format) || - dateFormatMap.get(part.format.substring(0, 2)); - - if (option && format) { - options[option] = format; - - if (part.type === DateParts.Hours) { - if (part.format.charAt(0) === 'h') { - options.hourCycle = 'h12'; - } else { - options.hourCycle = 'h23'; - } - } - } - - // Need to be set if we have 't' or 'tt'. - if (part.type === DateParts.AmPm && part.format.length <= 2) { - options.hour = '2-digit'; - options.hourCycle = 'h12'; - } - } - } - - let formatter: Intl.DateTimeFormat; - try { - formatter = new Intl.DateTimeFormat( - locale, - options as Intl.DateTimeFormatOptions - ); - } catch { - formatter = new Intl.DateTimeFormat(DateTimeUtil.DEFAULT_LOCALE, options); - } - - const formattedParts = formatter.formatToParts(value); - - let result = ''; - - for (const part of parts) { - if (part.type === DateParts.Literal) { - result += part.format; - continue; - } - - const option = datePartFormatOptionMap.get(part.type)!; - result += formattedParts.filter((p) => p.type === option)[0]?.value || ''; - } - - return result; - } - - private static getMask(dateStruct: any[]): string { - const mask = []; - - for (const part of dateStruct) { - switch (part.formatType) { - case FormatDesc.Numeric: { - if (part.type === DateParts.Day) { - mask.push('d'); - } else if (part.type === DateParts.Month) { - mask.push('M'); - } else { - mask.push('yyyy'); - } - break; - } - case FormatDesc.TwoDigits: { - if (part.type === DateParts.Day) { - mask.push('dd'); - } else if (part.type === DateParts.Month) { - mask.push('MM'); - } else { - mask.push('yy'); - } - } - } - - if (part.type === DateTimeUtil.SEPARATOR) { - mask.push(part.value); - } - } - - return mask.join(''); - } - private static addCurrentPart( currentPart: DatePartInfo, dateTimeParts: DatePartInfo[], - noLeadingZero = false + leadingZero = false ): void { - DateTimeUtil.ensureLeadingZero(currentPart, noLeadingZero); + DateTimeUtil.ensureLeadingZero(currentPart, leadingZero); currentPart.end = currentPart.start + currentPart.format.length; dateTimeParts.push(currentPart); } - private static ensureLeadingZero(part: DatePartInfo, noLeadingZero = false) { + private static ensureLeadingZero(part: DatePartInfo, leadingZero = false) { switch (part.type) { case DateParts.Date: case DateParts.Month: case DateParts.Hours: case DateParts.Minutes: case DateParts.Seconds: - if (part.format.length === 1 && !noLeadingZero) { + if (part.format.length === 1 && leadingZero) { part.format = part.format.repeat(2); } break; @@ -739,86 +594,6 @@ export abstract class DateTimeUtil { } } - private static getDefaultLocaleMask(locale: string) { - const dateStruct: any = []; - let formatter: Intl.DateTimeFormat; - try { - formatter = new Intl.DateTimeFormat(locale); - } catch { - return; - } - - const formatToParts = formatter.formatToParts(new Date()); - - for (const part of formatToParts) { - if (part.type === DateTimeUtil.SEPARATOR) { - dateStruct.push({ - type: DateTimeUtil.SEPARATOR, - value: part.value, - }); - } else { - dateStruct.push({ - type: part.type, - }); - } - } - - const formatterOptions = formatter.resolvedOptions(); - - for (const part of dateStruct) { - switch (part.type) { - case DateParts.Day: { - part.formatType = formatterOptions.day; - break; - } - case DateParts.Month: { - part.formatType = formatterOptions.month; - break; - } - case DateParts.Year: { - part.formatType = formatterOptions.year; - break; - } - } - } - - DateTimeUtil.fillDatePartsPositions(dateStruct); - return dateStruct; - } - - private static fillDatePartsPositions(dateArray: any[]): void { - let currentPos = 0; - - for (const part of dateArray) { - // Day|Month part positions - if (part.type === DateParts.Day || part.type === DateParts.Month) { - // Offset 2 positions for number - part.position = [currentPos, currentPos + 2]; - currentPos += 2; - } else if (part.type === DateParts.Year) { - // Year part positions - switch (part.formatType) { - case FormatDesc.Numeric: { - // Offset 4 positions for full year - part.position = [currentPos, currentPos + 4]; - currentPos += 4; - break; - } - case FormatDesc.TwoDigits: { - // Offset 2 positions for short year - part.position = [currentPos, currentPos + 2]; - currentPos += 2; - break; - } - } - } else if (part.type === DateTimeUtil.SEPARATOR) { - // Separator positions - part.position = [currentPos, currentPos + 1]; - currentPos++; - } - } - } - private static getCleanVal( inputData: string, datePart: DatePartInfo,