diff --git a/packages/devextreme/js/__internal/core/localization/intl/number.ts b/packages/devextreme/js/__internal/core/localization/intl/number.ts index 19aa32beaea6..7b103817ad00 100644 --- a/packages/devextreme/js/__internal/core/localization/intl/number.ts +++ b/packages/devextreme/js/__internal/core/localization/intl/number.ts @@ -3,6 +3,7 @@ import accountingFormats from '@ts/core/localization/cldr-data/accounting_format import localizationCoreUtils from '@ts/core/localization/core'; import type { FormatConfig as BaseFormatConfig, LocalizationFormat } from '@ts/core/localization/number'; import openXmlCurrencyFormat from '@ts/core/localization/open_xml_currency_format'; +import { getGlobalFormatByDataType } from '@ts/core/m_global_format_config'; interface CurrencySymbolInfo { position: 'before' | 'after'; @@ -106,6 +107,13 @@ export default { return value; } + const globalNumberFormat = getGlobalFormatByDataType('number'); + + if (!format && globalNumberFormat) { + // eslint-disable-next-line no-param-reassign + format = globalNumberFormat as LocalizationFormat; + } + // eslint-disable-next-line no-param-reassign format = this._normalizeFormat(format) as FormatConfig; diff --git a/packages/devextreme/js/__internal/core/localization/number.ts b/packages/devextreme/js/__internal/core/localization/number.ts index 2b4dd0d7f6ca..7aad251c4f39 100644 --- a/packages/devextreme/js/__internal/core/localization/number.ts +++ b/packages/devextreme/js/__internal/core/localization/number.ts @@ -6,6 +6,7 @@ import currencyLocalization from '@ts/core/localization/currency'; import intlNumberLocalization from '@ts/core/localization/intl/number'; import { getFormatter } from '@ts/core/localization/ldml/number'; import { toFixed } from '@ts/core/localization/utils'; +import { getGlobalFormatByDataType } from '@ts/core/m_global_format_config'; import { escapeRegExp } from '@ts/core/utils/m_common'; import { injector as dependencyInjector } from '@ts/core/utils/m_dependency_injector'; import { each } from '@ts/core/utils/m_iterator'; @@ -321,6 +322,12 @@ const numberLocalization = dependencyInjector({ return value; } + const globalNumberFormat = getGlobalFormatByDataType('number'); + + if (!format && globalNumberFormat) { + format = globalNumberFormat as LocalizationFormat; + } + // @ts-expect-error format = format?.formatter || format; diff --git a/packages/devextreme/js/__internal/core/m_config.ts b/packages/devextreme/js/__internal/core/m_config.ts index 9f556bad406b..19bf01257a21 100644 --- a/packages/devextreme/js/__internal/core/m_config.ts +++ b/packages/devextreme/js/__internal/core/m_config.ts @@ -19,6 +19,11 @@ const config = { useLegacyVisibleIndex: false, versionAssertions: [], copyStylesToShadowDom: true, + dateFormat: undefined, + timeFormat: undefined, + dateTimeFormat: undefined, + numberFormat: undefined, + dateTimeFormatPresets: undefined, floatingActionButtonConfig: { icon: 'add', diff --git a/packages/devextreme/js/__internal/core/m_global_format_config.js b/packages/devextreme/js/__internal/core/m_global_format_config.js new file mode 100644 index 000000000000..de854f58aa58 --- /dev/null +++ b/packages/devextreme/js/__internal/core/m_global_format_config.js @@ -0,0 +1,62 @@ +import config from '@js/core/config'; +import coreLocalization from '@js/common/core/localization/core'; +import parentLocales from '@ts/core/localization/cldr-data/parent_locales'; +import getParentLocale from '@ts/core/localization/parentLocale'; +import { isFunction, isPlainObject, isString } from '@js/core/utils/type'; + +const hasOwn = Object.prototype.hasOwnProperty; + +const resolveByLocaleMap = (localeMap) => { + let currentLocale = coreLocalization.locale(); + + while(currentLocale) { + if(hasOwn.call(localeMap, currentLocale) && localeMap[currentLocale] !== undefined) { + return localeMap[currentLocale]; + } + + currentLocale = getParentLocale(parentLocales, currentLocale); + } + + if(hasOwn.call(localeMap, 'default')) { + return localeMap.default; + } + + return undefined; +}; + +const resolveGlobalFormat = (optionName) => { + const optionValue = config()[optionName]; + + if(optionValue === undefined) { + return undefined; + } + + if(isString(optionValue) || isFunction(optionValue)) { + return optionValue; + } + + if(isPlainObject(optionValue)) { + return resolveByLocaleMap(optionValue); + } + + return undefined; +}; + +export const getGlobalFormatByDataType = (dataType) => { + switch(dataType) { + case 'date': + return resolveGlobalFormat('dateFormat'); + case 'datetime': + return resolveGlobalFormat('dateTimeFormat'); + case 'time': + return resolveGlobalFormat('timeFormat'); + case 'number': + return resolveGlobalFormat('numberFormat'); + default: + return undefined; + } +}; + +export default { + getGlobalFormatByDataType, +}; diff --git a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts index e61cbd284ab3..c822bfd1eb72 100644 --- a/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts +++ b/packages/devextreme/js/__internal/grids/grid_core/m_utils.ts @@ -1,6 +1,7 @@ // @ts-check import eventsEngine from '@js/common/core/events/core/events_engine'; +import dateLocalization from '@js/common/core/localization/date'; import DataSource from '@js/common/data/data_source'; import { normalizeDataSourceOptions } from '@js/common/data/data_source/utils'; import { normalizeSortingInfo as normalizeSortingInfoUtility } from '@js/common/data/utils'; @@ -19,6 +20,7 @@ import { getWindow } from '@js/core/utils/window'; import formatHelper from '@js/format_helper'; import LoadPanel from '@js/ui/load_panel'; import sharedFiltering from '@js/ui/shared/filtering'; +import { getGlobalFormatByDataType } from '@ts/core/m_global_format_config'; import { isNumeric } from '@ts/core/utils/m_type'; import type { Column } from '@ts/grids/grid_core/columns_controller/types'; import type { ColumnPoint } from '@ts/grids/grid_core/m_types'; @@ -83,6 +85,21 @@ function isDateType(dataType) { return dataType === 'date' || dataType === 'datetime'; } +const getGlobalFormat = (dataType) => { + const globalFormat = getGlobalFormatByDataType(dataType); + + if (!globalFormat) { + return undefined; + } + + return isString(globalFormat) + ? (value) => { + const dateValue = value instanceof Date ? value : new Date(value); + return isNaN(dateValue.getTime()) ? '' : dateLocalization.format(dateValue, globalFormat); + } + : globalFormat; +}; + const setEmptyText = function ($container) { $container.get(0).textContent = '\u00A0'; }; @@ -389,9 +406,9 @@ export default { getFormatByDataType(dataType) { switch (dataType) { case 'date': - return 'shortDate'; + return getGlobalFormat('date') || 'shortDate'; case 'datetime': - return 'shortDateShortTime'; + return getGlobalFormat('datetime') || 'shortDateShortTime'; default: return undefined; } diff --git a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/utils.ts b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/utils.ts index 32a1ecfc4fa8..d2108c00605c 100644 --- a/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/utils.ts +++ b/packages/devextreme/js/__internal/grids/new/grid_core/columns_controller/utils.ts @@ -1,10 +1,12 @@ import type { DataType, Format } from '@js/common'; +import dateLocalization from '@js/common/core/localization/date'; import { compileGetter, getPathParts } from '@js/core/utils/data'; import { captionize } from '@js/core/utils/inflector'; import { isDefined, isString, type, } from '@js/core/utils/type'; +import { getGlobalFormatByDataType } from '@ts/core/m_global_format_config'; import { getTreeNodeByPath, setTreeNodeByPath } from '@ts/grids/new/grid_core/utils/tree/index'; import type { ComponentType } from 'inferno'; @@ -222,6 +224,30 @@ export const getValueDataType = ( : dataType as DataType; }; +const getGlobalFormat = ( + dataType: 'date' | 'datetime', +): Format | undefined => { + const globalFormat = getGlobalFormatByDataType(dataType); + + if (!globalFormat) { + return undefined; + } + + if (isString(globalFormat)) { + return ( + (value: Date | string | number) => { + const dateValue = value instanceof Date ? value : new Date(value); + + return isNaN(dateValue.getTime()) + ? '' + : dateLocalization.format(dateValue, globalFormat) as string; + } + ) as unknown as Format; + } + + return globalFormat as Format; +}; + export const getColumnFormat = ( column: Partial>, ): Format | undefined => { @@ -229,8 +255,12 @@ export const getColumnFormat = ( return column.format; } - if (column.dataType === 'date' || column.dataType === 'datetime') { - return 'shortDate'; + if (column.dataType === 'date') { + return getGlobalFormat('date') || 'shortDate'; + } + + if (column.dataType === 'datetime') { + return getGlobalFormat('datetime') || 'shortDateShortTime'; } return undefined; diff --git a/packages/devextreme/js/__internal/scheduler/a11y_status/a11y_status_text.ts b/packages/devextreme/js/__internal/scheduler/a11y_status/a11y_status_text.ts index a6a2e090ed18..4fa4f666fc6d 100644 --- a/packages/devextreme/js/__internal/scheduler/a11y_status/a11y_status_text.ts +++ b/packages/devextreme/js/__internal/scheduler/a11y_status/a11y_status_text.ts @@ -1,7 +1,7 @@ -import dateLocalization from '@js/common/core/localization/date'; import messageLocalization from '@js/common/core/localization/message'; import type { ViewType } from '@js/ui/scheduler'; +import { formatImplicitSchedulerDate, formatImplicitSchedulerMonth } from '../utils/global_formats'; import type { NormalizedView } from '../utils/options/types'; const KEYS = { @@ -22,8 +22,8 @@ const viewTypeLocalization: Record = { timelineWorkWeek: 'dxScheduler-switcherTimelineWorkWeek', }; -const localizeMonth = (date: Date): string => String(dateLocalization.format(date, 'monthAndYear')); -const localizeDate = (date: Date): string => `${dateLocalization.format(date, 'monthAndDay')}, ${dateLocalization.format(date, 'year')}`; +const localizeMonth = (date: Date): string => formatImplicitSchedulerMonth(date); +const localizeDate = (date: Date): string => formatImplicitSchedulerDate(date); const localizeCurrentIndicator = ( date: Date, startDate: Date, diff --git a/packages/devextreme/js/__internal/scheduler/appointments/appointment/text_utils.ts b/packages/devextreme/js/__internal/scheduler/appointments/appointment/text_utils.ts index 610a7112ea29..29d867aa2a81 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments/appointment/text_utils.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments/appointment/text_utils.ts @@ -1,12 +1,11 @@ -import dateLocalization from '@js/common/core/localization/date'; import messageLocalization from '@js/common/core/localization/message'; import { isDefined } from '@js/core/utils/type'; +import { formatImplicitSchedulerDate, formatImplicitSchedulerTime } from '../../utils/global_formats'; import type { AppointmentProperties } from './m_types'; -const localizeDate = (date: Date): string => `${dateLocalization.format(date, 'monthAndDay')}, ${dateLocalization.format(date, 'year')}`; - -const localizeTime = (date: Date): string => `${dateLocalization.format(date, 'shorttime')}`; +const localizeDate = (date: Date): string => formatImplicitSchedulerDate(date); +const localizeTime = (date: Date): string => formatImplicitSchedulerTime(date); const getDate = (options: AppointmentProperties, propName: 'endDate' | 'startDate'): Date => { const result = options.dataAccessors.get(propName, options.data); diff --git a/packages/devextreme/js/__internal/scheduler/appointments_new/utils/get_date_text.ts b/packages/devextreme/js/__internal/scheduler/appointments_new/utils/get_date_text.ts index 4e0d893da903..00f42d025920 100644 --- a/packages/devextreme/js/__internal/scheduler/appointments_new/utils/get_date_text.ts +++ b/packages/devextreme/js/__internal/scheduler/appointments_new/utils/get_date_text.ts @@ -1,8 +1,29 @@ import dateLocalization from '@js/common/core/localization/date'; import dateUtils from '@js/core/utils/date'; +import { getGlobalFormatByDataType } from '@ts/core/m_global_format_config'; import type { TargetedAppointment, ViewType } from '../../types'; +const formatTooltipDatePart = (date: Date): string => { + const globalFormat = getGlobalFormatByDataType('date'); + + if (globalFormat) { + return dateLocalization.format(date, globalFormat) as string; + } + + return String(dateLocalization.format(date, 'monthandday')); +}; + +const formatTooltipTimePart = (date: Date): string => { + const globalFormat = getGlobalFormatByDataType('time'); + + if (globalFormat) { + return dateLocalization.format(date, globalFormat) as string; + } + + return String(dateLocalization.format(date, 'shorttime')); +}; + export enum DateFormatType { DATETIME = 'DATETIME', TIME = 'TIME', @@ -25,24 +46,17 @@ export const getDateFormatType = ( }; export const getDateText = (startDate: Date, endDate: Date, formatType: DateFormatType): string => { - const dateFormat = 'monthandday'; - const timeFormat = 'shorttime'; const isSameDate = dateUtils.sameDate(startDate, endDate); switch (formatType) { case DateFormatType.DATETIME: - return [ - dateLocalization.format(startDate, dateFormat), - ' ', - dateLocalization.format(startDate, timeFormat), - ' - ', - isSameDate ? '' : `${dateLocalization.format(endDate, dateFormat)} `, - dateLocalization.format(endDate, timeFormat), - ].join(''); + return isSameDate + ? `${formatTooltipDatePart(startDate)} ${formatTooltipTimePart(startDate)} - ${formatTooltipTimePart(endDate)}` + : `${formatTooltipDatePart(startDate)} ${formatTooltipTimePart(startDate)} - ${formatTooltipDatePart(endDate)} ${formatTooltipTimePart(endDate)}`; case DateFormatType.TIME: - return `${dateLocalization.format(startDate, timeFormat)} - ${dateLocalization.format(endDate, timeFormat)}`; + return `${formatTooltipTimePart(startDate)} - ${formatTooltipTimePart(endDate)}`; case DateFormatType.DATE: - return `${dateLocalization.format(startDate, dateFormat)}${isSameDate ? '' : ` - ${dateLocalization.format(endDate, dateFormat)}`}`; + return `${formatTooltipDatePart(startDate)}${isSameDate ? '' : ` - ${formatTooltipDatePart(endDate)}`}`; default: return ''; } diff --git a/packages/devextreme/js/__internal/scheduler/m_compact_appointments_helper.ts b/packages/devextreme/js/__internal/scheduler/m_compact_appointments_helper.ts index 5948d999ba90..fd2a38162d4b 100644 --- a/packages/devextreme/js/__internal/scheduler/m_compact_appointments_helper.ts +++ b/packages/devextreme/js/__internal/scheduler/m_compact_appointments_helper.ts @@ -1,5 +1,4 @@ import { locate, move } from '@js/common/core/animation/translator'; -import dateLocalization from '@js/common/core/localization/date'; import messageLocalization from '@js/common/core/localization/message'; import $, { type dxElementWrapper } from '@js/core/renderer'; import { FunctionTemplate } from '@js/core/templates/function_template'; @@ -9,6 +8,7 @@ import type { Appointment } from '@js/ui/scheduler'; import { APPOINTMENT_SETTINGS_KEY, LIST_ITEM_CLASS, LIST_ITEM_DATA_KEY } from './constants'; import type Scheduler from './m_scheduler'; import type { AppointmentTooltipItem, CompactAppointmentOptions, TargetedAppointment } from './types'; +import { formatImplicitSchedulerDate } from './utils/global_formats'; const APPOINTMENT_COLLECTOR_CLASS = 'dx-scheduler-appointment-collector'; const COMPACT_APPOINTMENT_COLLECTOR_CLASS = `${APPOINTMENT_COLLECTOR_CLASS}-compact`; @@ -185,7 +185,7 @@ export class CompactAppointmentsHelper { } private localizeDate(date) { - return `${dateLocalization.format(date, 'monthAndDay')}, ${dateLocalization.format(date, 'year')}`; + return formatImplicitSchedulerDate(date); } private getDateText( diff --git a/packages/devextreme/js/__internal/scheduler/r1/utils/week.ts b/packages/devextreme/js/__internal/scheduler/r1/utils/week.ts index 240f50a05abe..96601f02c80a 100644 --- a/packages/devextreme/js/__internal/scheduler/r1/utils/week.ts +++ b/packages/devextreme/js/__internal/scheduler/r1/utils/week.ts @@ -2,6 +2,7 @@ import dateLocalization from '@js/common/core/localization/date'; import dateUtils from '@js/core/utils/date'; import type { CalculateStartViewDate } from '../../types'; +import { formatImplicitSchedulerTime } from '../../utils/global_formats'; import { getCalculatedFirstDayOfWeek, getValidCellDateForLocalTimeFormat, @@ -29,7 +30,7 @@ export const getTimePanelCellText = ( viewOffset, }); - return dateLocalization.format(validTimeDate, 'shorttime') as string; + return formatImplicitSchedulerTime(validTimeDate); }; export const getIntervalDuration = ( diff --git a/packages/devextreme/js/__internal/scheduler/utils/global_formats.ts b/packages/devextreme/js/__internal/scheduler/utils/global_formats.ts new file mode 100644 index 000000000000..6bd2ae7d8bbc --- /dev/null +++ b/packages/devextreme/js/__internal/scheduler/utils/global_formats.ts @@ -0,0 +1,28 @@ +import dateLocalization from '@js/common/core/localization/date'; +import { getGlobalFormatByDataType } from '@ts/core/m_global_format_config'; + +export const formatImplicitSchedulerDate = (date: Date): string => { + const globalDateFormat = getGlobalFormatByDataType('date'); + + if (globalDateFormat) { + return dateLocalization.format(date, globalDateFormat) as string; + } + + return `${dateLocalization.format(date, 'monthAndDay')}, ${dateLocalization.format(date, 'year')}`; +}; + +export const formatImplicitSchedulerMonth = (date: Date): string => { + const globalDateFormat = getGlobalFormatByDataType('date'); + + if (globalDateFormat) { + return dateLocalization.format(date, globalDateFormat) as string; + } + + return String(dateLocalization.format(date, 'monthAndYear')); +}; + +export const formatImplicitSchedulerTime = (date: Date): string => { + const globalTimeFormat = getGlobalFormatByDataType('time'); + + return dateLocalization.format(date, globalTimeFormat || 'shorttime') as string; +}; diff --git a/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.calendar.ts b/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.calendar.ts index ac4e3e9ae70b..1a59b21f0b56 100644 --- a/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.calendar.ts +++ b/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.calendar.ts @@ -9,6 +9,7 @@ import type { ClickEvent } from '@js/ui/button'; import type { ValueChangedEvent } from '@js/ui/calendar'; import type { ToolbarItem } from '@js/ui/popup'; import { current, isMaterial } from '@js/ui/themes'; +import { getGlobalFormatByDataType } from '@ts/core/m_global_format_config'; import { splitPair } from '@ts/core/utils/m_common'; import Calendar from '@ts/ui/calendar/calendar'; @@ -94,8 +95,9 @@ class CalendarStrategy extends DateBoxStrategy { } getDisplayFormat(displayFormat?: Format): Format { + const globalDateFormat = getGlobalFormatByDataType('date'); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - return displayFormat || 'shortdate'; + return displayFormat || globalDateFormat || 'shortdate'; } _closeDropDownByEnter(): boolean { diff --git a/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.calendar_with_time.ts b/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.calendar_with_time.ts index 55b1b83f7e5d..54310d0cffc1 100644 --- a/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.calendar_with_time.ts +++ b/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.calendar_with_time.ts @@ -7,6 +7,7 @@ import { getWidth } from '@js/core/utils/size'; import { getWindow } from '@js/core/utils/window'; import type { DxEvent } from '@js/events'; import type { Format } from '@js/localization'; +import { getGlobalFormatByDataType } from '@ts/core/m_global_format_config'; import type { BoxItemData } from '@ts/ui/box'; import Box from '@ts/ui/box'; import TimeView from '@ts/ui/date_box/time_view'; @@ -56,8 +57,9 @@ class CalendarWithTimeStrategy extends CalendarStrategy { } getDisplayFormat(displayFormat?: Format): Format { + const globalDateTimeFormat = getGlobalFormatByDataType('datetime'); // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - return displayFormat || 'shortdateshorttime'; + return displayFormat || globalDateTimeFormat || 'shortdateshorttime'; } _is24HourFormat(): boolean { diff --git a/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.date_view.ts b/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.date_view.ts index a240d201e5ff..0bc4d0ebca69 100644 --- a/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.date_view.ts +++ b/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.date_view.ts @@ -5,6 +5,7 @@ import $ from '@js/core/renderer'; import { inputType } from '@js/core/utils/support'; import { getWindow } from '@js/core/utils/window'; import type { Format } from '@js/localization'; +import { getGlobalFormatByDataType } from '@ts/core/m_global_format_config'; import type { PopupProperties } from '../popup/m_popup'; import type DateBox from './date_box.base'; @@ -39,9 +40,12 @@ class DateViewStrategy extends DateBoxStrategy { getDisplayFormat(displayFormat: Format): Format { const { type = 'date' } = this.dateBox.option(); + const globalFormat = type === 'date' || type === 'datetime' + ? getGlobalFormatByDataType(type) + : undefined; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - return displayFormat || dateUtils.FORMATS_MAP[type]; + return displayFormat || globalFormat || dateUtils.FORMATS_MAP[type]; } popupConfig(config: PopupProperties): PopupProperties { diff --git a/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.native.ts b/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.native.ts index c6a53b4ae64f..11fb6cabe325 100644 --- a/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.native.ts +++ b/packages/devextreme/js/__internal/ui/date_box/m_date_box.strategy.native.ts @@ -5,6 +5,7 @@ import dateSerialization from '@js/core/utils/date_serialization'; import { inputType } from '@js/core/utils/support'; import type { Format } from '@js/localization'; import type { TextBoxType } from '@js/ui/text_box'; +import { getGlobalFormatByDataType } from '@ts/core/m_global_format_config'; import type { PopupProperties } from '../popup/m_popup'; import type { DateBoxBaseProperties } from './date_box.base'; @@ -72,9 +73,12 @@ class NativeStrategy extends DateBoxStrategy { getDisplayFormat(displayFormat?: Format): Format { const type = this._getDateBoxType(); + const globalFormat = type === 'date' || type === 'datetime' || type === 'datetime-local' + ? getGlobalFormatByDataType(type === 'datetime-local' ? 'datetime' : type) + : undefined; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - return displayFormat || dateUtils.FORMATS_MAP[type] as string; + return displayFormat || globalFormat || dateUtils.FORMATS_MAP[type] as string; } renderInputMinMax($input?: dxElementWrapper): void { diff --git a/packages/devextreme/js/common.d.ts b/packages/devextreme/js/common.d.ts index 2ac75ae9b8a8..4580f174ea25 100644 --- a/packages/devextreme/js/common.d.ts +++ b/packages/devextreme/js/common.d.ts @@ -1,4 +1,5 @@ import { PositionConfig } from './common/core/animation'; +import type { Format as LocalizationFormat } from './common/core/localization'; import type { OmitInternal, } from './core'; @@ -284,6 +285,11 @@ export type VersionAssertion = { */ export type GlobalConfig = { versionAssertions?: VersionAssertion[]; + dateFormat?: LocalizationFormat | Record; + timeFormat?: LocalizationFormat | Record; + dateTimeFormat?: LocalizationFormat | Record; + numberFormat?: LocalizationFormat | Record; + dateTimeFormatPresets?: Record>; /** * @docid * @default "." diff --git a/packages/devextreme/js/format_helper.js b/packages/devextreme/js/format_helper.js index c748112c1b69..51fdd4e0dba8 100644 --- a/packages/devextreme/js/format_helper.js +++ b/packages/devextreme/js/format_helper.js @@ -9,6 +9,7 @@ import { import dateUtils from './core/utils/date'; import numberLocalization from './common/core/localization/number'; import dateLocalization from './common/core/localization/date'; +import { getGlobalFormatByDataType } from './__internal/core/m_global_format_config'; import dependencyInjector from './core/utils/dependency_injector'; import './common/core/localization/currency'; @@ -18,7 +19,18 @@ export default dependencyInjector({ const formatIsValid = isString(format) && format !== '' || isPlainObject(format) || isFunction(format); const valueIsValid = isNumeric(value) || (isDate(value) && !isNaN(value.getTime())); - if(!formatIsValid || !valueIsValid) { + if(!valueIsValid) { + return isDefined(value) ? value.toString() : ''; + } + + if(!formatIsValid && isNumeric(value)) { + const globalNumberFormat = getGlobalFormatByDataType('number'); + if(globalNumberFormat) { + return numberLocalization.format(value, globalNumberFormat); + } + } + + if(!formatIsValid) { return isDefined(value) ? value.toString() : ''; } diff --git a/packages/devextreme/testing/tests/DevExpress.localization/localization.intl.tests.js b/packages/devextreme/testing/tests/DevExpress.localization/localization.intl.tests.js index 67bfb491acee..b4543cc11a42 100644 --- a/packages/devextreme/testing/tests/DevExpress.localization/localization.intl.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.localization/localization.intl.tests.js @@ -774,6 +774,76 @@ QUnit.module('Intl localization', { }); }); +QUnit.module('Global formatting config (spec, intl)', () => { + const saveGlobalFormats = () => { + const globalConfig = config(); + + return { + dateFormat: globalConfig.dateFormat, + timeFormat: globalConfig.timeFormat, + dateTimeFormat: globalConfig.dateTimeFormat, + numberFormat: globalConfig.numberFormat, + }; + }; + const restoreGlobalFormats = (saved) => { + const globalConfig = config(); + + Object.keys(saved).forEach((key) => { + const value = saved[key]; + if(value === undefined) { + delete globalConfig[key]; + } else { + globalConfig[key] = value; + } + }); + }; + + QUnit.test('global dateFormat supports formatter function values', function(assert) { + const saved = saveGlobalFormats(); + + try { + config({ + ...config(), + dateFormat: (date) => `${date.getDate()}-${date.getMonth() + 1}-${date.getFullYear()}`, + }); + + assert.strictEqual(dateLocalization.format(new Date(2020, 0, 2), config().dateFormat), '2-1-2020'); + } finally { + restoreGlobalFormats(saved); + } + }); + + QUnit.test('global dateTimeFormat supports formatter function values', function(assert) { + const saved = saveGlobalFormats(); + + try { + config({ + ...config(), + dateTimeFormat: (date) => `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()} ${date.getHours()}:${date.getMinutes()}`, + }); + + assert.strictEqual(dateLocalization.format(new Date(2020, 0, 2, 14, 5), config().dateTimeFormat), '2/1/2020 14:5'); + } finally { + restoreGlobalFormats(saved); + } + }); + + QUnit.test('global numberFormat supports formatter function values', function(assert) { + const saved = saveGlobalFormats(); + + try { + config({ + ...config(), + numberFormat: (value) => `#${value.toFixed(2)}`, + }); + + assert.strictEqual(numberLocalization.format(12.3), '#12.30'); + } finally { + restoreGlobalFormats(saved); + } + }); +}); + QUnit.module('Exceljs format', () => { ExcelJSLocalizationFormatTests.runCurrencyTests([ { value: 'USD', expected: '$#,##0_);\\($#,##0\\)' }, diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataGrid.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataGrid.tests.js index 701c8d3a919e..bf1515fb97c3 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataGrid.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.dataGrid/dataGrid.tests.js @@ -5525,3 +5525,86 @@ QUnit.module('Formatting', baseModuleConfig, () => { }), '216 rub'); }); }); + +QUnit.module('Global formatting config (spec)', baseModuleConfig, () => { + const saveGlobalFormats = () => { + const globalConfig = config(); + + return { + dateFormat: globalConfig.dateFormat, + timeFormat: globalConfig.timeFormat, + dateTimeFormat: globalConfig.dateTimeFormat, + numberFormat: globalConfig.numberFormat, + }; + }; + const restoreGlobalFormats = (saved) => { + const globalConfig = config(); + + Object.keys(saved).forEach((key) => { + const value = saved[key]; + if(value === undefined) { + delete globalConfig[key]; + } else { + globalConfig[key] = value; + } + }); + }; + + QUnit.test('implicit date format uses global dateFormat', function(assert) { + const saved = saveGlobalFormats(); + + try { + config({ + ...config(), + dateFormat: 'dd/MM/yyyy', + }); + + const format = gridCore.getFormatByDataType('date'); + const dateText = gridCore.formatValue(new Date(2020, 0, 2), { format }); + + assert.strictEqual(dateText, '02/01/2020', 'global date format is applied for implicit DataGrid date format'); + } finally { + restoreGlobalFormats(saved); + } + }); + + QUnit.test('implicit datetime format uses global dateTimeFormat', function(assert) { + const saved = saveGlobalFormats(); + + try { + config({ + ...config(), + dateTimeFormat: 'dd/MM/yyyy, HH:mm', + }); + + const format = gridCore.getFormatByDataType('datetime'); + const dateText = gridCore.formatValue(new Date(2020, 0, 2, 14, 5), { format }); + + assert.strictEqual(dateText, '02/01/2020, 14:05', 'global datetime format is applied for implicit DataGrid datetime format'); + } finally { + restoreGlobalFormats(saved); + } + }); + + QUnit.test('explicit column.format keeps priority over global format', function(assert) { + const saved = saveGlobalFormats(); + + try { + config({ + ...config(), + dateFormat: 'dd/MM/yyyy', + }); + + const dataGrid = createDataGrid({ + dataSource: [{ createdAt: new Date(2020, 0, 2) }], + columns: [{ dataField: 'createdAt', dataType: 'date', format: 'shortDate' }], + }); + this.clock.tick(10); + + const dateText = $(dataGrid.getCellElement(0, 0)).text().trim(); + assert.strictEqual(dateText, '1/2/2020', 'explicit preset format is not replaced by global dateFormat'); + } finally { + restoreGlobalFormats(saved); + } + }); +}); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js index bf32b07356d7..28f06e7d3286 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.editors/datebox.tests.js @@ -3373,3 +3373,58 @@ QUnit.module('validation', { assert.strictEqual(this.$dateBox.hasClass(SHOW_INVALID_BADGE_CLASS), false, 'validation icon is be hidden'); }); }); + +QUnit.module('Global formatting config (spec)', { + beforeEach: function() { + this.originalConfig = config(); + }, + afterEach: function() { + config(this.originalConfig); + }, +}, () => { + QUnit.test('implicit date displayFormat uses global dateFormat', function(assert) { + config({ + ...this.originalConfig, + dateFormat: 'dd/MM/yyyy', + }); + + const instance = $('#dateBox').dxDateBox({ + type: 'date', + value: new Date(2020, 0, 2), + pickerType: 'calendar', + }).dxDateBox('instance'); + + assert.strictEqual(instance.option('text'), '02/01/2020'); + }); + + QUnit.test('implicit datetime displayFormat uses global dateTimeFormat', function(assert) { + config({ + ...this.originalConfig, + dateTimeFormat: 'dd/MM/yyyy, HH:mm', + }); + + const instance = $('#dateBox').dxDateBox({ + type: 'datetime', + value: new Date(2020, 0, 2, 14, 5), + pickerType: 'calendar', + }).dxDateBox('instance'); + + assert.strictEqual(instance.option('text'), '02/01/2020, 14:05'); + }); + + QUnit.test('explicit displayFormat keeps priority over global dateFormat', function(assert) { + config({ + ...this.originalConfig, + dateFormat: 'dd/MM/yyyy', + }); + + const instance = $('#dateBox').dxDateBox({ + type: 'date', + value: new Date(2020, 0, 2), + displayFormat: 'shortDate', + pickerType: 'calendar', + }).dxDateBox('instance'); + + assert.strictEqual(instance.option('text'), '1/2/2020'); + }); +}); diff --git a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentTooltip.tests.js b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentTooltip.tests.js index 52af727e9ab8..4d8efba7357e 100644 --- a/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentTooltip.tests.js +++ b/packages/devextreme/testing/tests/DevExpress.ui.widgets.scheduler/integration.appointmentTooltip.tests.js @@ -1,5 +1,6 @@ import $ from 'jquery'; import dateSerialization from 'core/utils/date_serialization'; +import config from 'core/config'; import Tooltip from 'ui/tooltip'; import { hide } from '__internal/ui/tooltip/m_tooltip'; import resizeCallbacks from 'core/utils/resize_callbacks'; @@ -35,6 +36,87 @@ const moduleConfig = { } }; +module('Global formatting config (spec): Scheduler tooltip', { + beforeEach() { + fx.off = true; + const globalConfig = config(); + this.savedGlobalFormats = { + dateFormat: globalConfig.dateFormat, + timeFormat: globalConfig.timeFormat, + dateTimeFormat: globalConfig.dateTimeFormat, + numberFormat: globalConfig.numberFormat, + }; + }, + afterEach() { + fx.off = false; + hide(); + const globalConfig = config(); + Object.keys(this.savedGlobalFormats).forEach((key) => { + const value = this.savedGlobalFormats[key]; + if(value === undefined) { + delete globalConfig[key]; + } else { + globalConfig[key] = value; + } + }); + } +}, () => { + const createScheduler = (options) => createWrapper($.extend({ + currentView: 'day', + currentDate: new Date(2015, 1, 9), + height: 600, + dataSource: [{ + text: 'Task 1', + startDate: new Date(2015, 1, 9, 11, 0), + endDate: new Date(2015, 1, 9, 12, 0), + }], + }, options)); + + test('implicit Scheduler tooltip time format uses global timeFormat', async function(assert) { + config({ + ...config(), + timeFormat: (date) => `T${date.getHours()}`, + }); + + const scheduler = await createScheduler(); + const clock = sinon.useFakeTimers(); + await scheduler.appointments.click(0, clock); + clock.restore(); + + assert.strictEqual(scheduler.tooltip.getDateText(), 'T11 - T12'); + }); + + test('implicit Scheduler tooltip uses built-in format when global timeFormat is not set', async function(assert) { + const scheduler = await createScheduler(); + const clock = sinon.useFakeTimers(); + await scheduler.appointments.click(0, clock); + clock.restore(); + + assert.strictEqual(scheduler.tooltip.getDateText(), '11:00 AM - 12:00 PM'); + }); + + test('implicit Scheduler tooltip date/time use global dateFormat and timeFormat', async function(assert) { + config({ + ...config(), + dateFormat: (date) => `Date${date.getDate()}`, + timeFormat: (date) => `Time${date.getHours()}`, + }); + + const scheduler = await createScheduler({ + dataSource: [{ + text: 'Task 1', + startDate: new Date(2015, 1, 9, 23, 0), + endDate: new Date(2015, 1, 10, 1, 0), + }], + }); + const clock = sinon.useFakeTimers(); + await scheduler.appointments.click(0, clock); + clock.restore(); + + assert.strictEqual(scheduler.tooltip.getDateText(), 'Date9 Time23 - Date10 Time1'); + }); +}); + module('Integration: Appointment tooltip', moduleConfig, () => { const createScheduler = (options) => createWrapper($.extend(options, { height: 600 })); const getDeltaTz = (schedulerTz, date) => schedulerTz * 3600000 + date.getTimezoneOffset() * 60000;