From b99cb8c4a0ee9e3bb94bc5b930563f34975313f6 Mon Sep 17 00:00:00 2001 From: Nebojsa Date: Fri, 13 Dec 2024 17:06:23 +0100 Subject: [PATCH 1/3] use invariant date parsing in date based components to avoid time zone issues (cherry picked from commit bdd46e21ce4ada29c6b158db9b7b057aa69bd148) --- litmus/features/date/invariantParseDate.js | 31 ++++ litmus/index.js | 3 +- packages/cx/src/charts/axis/TimeAxis.js | 13 +- packages/cx/src/ui/Format.js | 122 ++++++-------- packages/cx/src/util/Format.js | 159 ++++++++---------- packages/cx/src/util/date/index.d.ts | 19 ++- packages/cx/src/util/date/index.js | 19 ++- .../cx/src/util/date/parseDateInvariant.d.ts | 3 + .../cx/src/util/date/parseDateInvariant.js | 20 +++ packages/cx/src/widgets/form/Calendar.js | 53 +++--- packages/cx/src/widgets/form/DateTimeField.js | 17 +- .../cx/src/widgets/form/DateTimePicker.js | 17 +- packages/cx/src/widgets/form/MonthField.js | 20 ++- packages/cx/src/widgets/form/MonthPicker.js | 33 ++-- 14 files changed, 283 insertions(+), 246 deletions(-) create mode 100644 litmus/features/date/invariantParseDate.js create mode 100644 packages/cx/src/util/date/parseDateInvariant.d.ts create mode 100644 packages/cx/src/util/date/parseDateInvariant.js diff --git a/litmus/features/date/invariantParseDate.js b/litmus/features/date/invariantParseDate.js new file mode 100644 index 000000000..0ed82d13d --- /dev/null +++ b/litmus/features/date/invariantParseDate.js @@ -0,0 +1,31 @@ +import { Calendar, DateField, DateTimeField, MonthField, PureContainer, TimeField } from "cx/widgets"; + +export default ( + + +
+ + + + + + + + +
+
+
+ + +); diff --git a/litmus/index.js b/litmus/index.js index 6bdd34df6..efd8a84c5 100644 --- a/litmus/index.js +++ b/litmus/index.js @@ -108,7 +108,8 @@ import "./index.scss"; // import Demo from "./features/calendar"; //import Demo from "./features/slider/SliderPreventDefault"; //import Demo from "./features/validator/index"; -import Demo from "./bugs/1075-complex-column-resizing"; +// import Demo from "./bugs/1075-complex-column-resizing"; +import Demo from "./features/date/invariantParseDate"; let store = (window.store = new Store()); diff --git a/packages/cx/src/charts/axis/TimeAxis.js b/packages/cx/src/charts/axis/TimeAxis.js index db8cf5564..5a296c0ef 100644 --- a/packages/cx/src/charts/axis/TimeAxis.js +++ b/packages/cx/src/charts/axis/TimeAxis.js @@ -4,12 +4,13 @@ import { Stack } from "./Stack"; import { Format } from "../../ui/Format"; import { isNumber } from "../../util/isNumber"; import { zeroTime } from "../../util/date/zeroTime"; +import { parseDateInvariant } from "../../util"; Format.registerFactory("yearOrMonth", (format) => { let year = Format.parse("datetime;yyyy"); let month = Format.parse("datetime;MMM"); return function (date) { - let d = new Date(date); + let d = parseDateInvariant(date); if (d.getMonth() == 0) return year(d); else return month(d); }; @@ -19,7 +20,7 @@ Format.registerFactory("monthOrDay", (format) => { let month = Format.parse("datetime;MMM"); let day = Format.parse("datetime;dd"); return function (date) { - let d = new Date(date); + let d = parseDateInvariant(date); if (d.getDate() == 1) return month(d); else return day(d); }; @@ -69,7 +70,7 @@ export class TimeAxis extends Axis { this.minTickUnit, lowerDeadZone, upperDeadZone, - this.decode + this.decode, ); } @@ -149,7 +150,7 @@ class TimeScale { minTickUnit, lowerDeadZone, upperDeadZone, - decode + decode, ) { this.dateCache = {}; this.min = min != null ? this.decodeValue(min) : null; @@ -180,12 +181,12 @@ class TimeScale { let v = this.dateCache[date]; if (!v) { if (this.decode) date = this.decode(date); - v = this.dateCache[date] = Date.parse(date); + v = this.dateCache[date] = parseDateInvariant(date).getTime(); } return v; case "number": - return date; + return parseDateInvariant(date).getTime(); } } diff --git a/packages/cx/src/ui/Format.js b/packages/cx/src/ui/Format.js index 900d00a5a..fb69ee5f5 100644 --- a/packages/cx/src/ui/Format.js +++ b/packages/cx/src/ui/Format.js @@ -1,89 +1,73 @@ -import {Culture} from "./Culture"; -import {Format as Fmt, resolveMinMaxFractionDigits} from "../util/Format"; -import {GlobalCacheIdentifier} from '../util/GlobalCacheIdentifier'; +import { parseDateInvariant } from "../util"; +import { Format as Fmt, resolveMinMaxFractionDigits } from "../util/Format"; +import { GlobalCacheIdentifier } from "../util/GlobalCacheIdentifier"; +import { Culture } from "./Culture"; export const Format = Fmt; let cultureSensitiveFormatsRegistered = false; export function enableCultureSensitiveFormatting() { - - if (cultureSensitiveFormatsRegistered) - return; + if (cultureSensitiveFormatsRegistered) return; cultureSensitiveFormatsRegistered = true; - Fmt.registerFactory( - ['number', 'n'], - (format, minimumFractionDigits, maximumFractionDigits) => { - let culture = Culture.getNumberCulture(); - - let formatter = culture.getFormatter(resolveMinMaxFractionDigits(minimumFractionDigits, maximumFractionDigits)); - - return value => formatter.format(value); - } - ); - - Fmt.registerFactory('currency', - (format, currency, minimumFractionDigits, maximumFractionDigits) => { - let culture = Culture.getNumberCulture(); - currency = currency || Culture.defaultCurrency; - - let formatter = culture.getFormatter({ - style: 'currency', - currency: currency, - ...resolveMinMaxFractionDigits(minimumFractionDigits, maximumFractionDigits) - }); - - return value => formatter.format(value); - } - ); - - Fmt.registerFactory( - ['percentage', 'p', '%'], - (format, minimumFractionDigits, maximumFractionDigits) => { - let culture = Culture.getNumberCulture(); - let formatter = culture.getFormatter({ - style: 'percent', - ...resolveMinMaxFractionDigits(minimumFractionDigits, maximumFractionDigits) - }); - return value => formatter.format(value); - } - ); - - Fmt.registerFactory( - ['percentSign', 'ps'], - (format, minimumFractionDigits, maximumFractionDigits) => { - let culture = Culture.getNumberCulture(); - let formatter = culture.getFormatter({ - style: 'percent', - ...resolveMinMaxFractionDigits(minimumFractionDigits, maximumFractionDigits) - }); - return value => formatter.format(value / 100); - } - ); - - Fmt.registerFactory(['date', 'd'], (fmt, format = 'yyyyMMdd') => { + Fmt.registerFactory(["number", "n"], (format, minimumFractionDigits, maximumFractionDigits) => { + let culture = Culture.getNumberCulture(); + + let formatter = culture.getFormatter(resolveMinMaxFractionDigits(minimumFractionDigits, maximumFractionDigits)); + + return (value) => formatter.format(value); + }); + + Fmt.registerFactory("currency", (format, currency, minimumFractionDigits, maximumFractionDigits) => { + let culture = Culture.getNumberCulture(); + currency = currency || Culture.defaultCurrency; + + let formatter = culture.getFormatter({ + style: "currency", + currency: currency, + ...resolveMinMaxFractionDigits(minimumFractionDigits, maximumFractionDigits), + }); + + return (value) => formatter.format(value); + }); + + Fmt.registerFactory(["percentage", "p", "%"], (format, minimumFractionDigits, maximumFractionDigits) => { + let culture = Culture.getNumberCulture(); + let formatter = culture.getFormatter({ + style: "percent", + ...resolveMinMaxFractionDigits(minimumFractionDigits, maximumFractionDigits), + }); + return (value) => formatter.format(value); + }); + + Fmt.registerFactory(["percentSign", "ps"], (format, minimumFractionDigits, maximumFractionDigits) => { + let culture = Culture.getNumberCulture(); + let formatter = culture.getFormatter({ + style: "percent", + ...resolveMinMaxFractionDigits(minimumFractionDigits, maximumFractionDigits), + }); + return (value) => formatter.format(value / 100); + }); + + Fmt.registerFactory(["date", "d"], (fmt, format = "yyyyMMdd") => { let culture = Culture.getDateTimeCulture(); let formatter = culture.getFormatter(format); - return value => formatter.format(new Date(value)); + return (value) => formatter.format(parseDateInvariant(value)); }); - - Fmt.registerFactory(['time', 't'], (fmt, format = 'hhmmss') => { + Fmt.registerFactory(["time", "t"], (fmt, format = "hhmmss") => { let culture = Culture.getDateTimeCulture(); let formatter = culture.getFormatter(format); - return value => formatter.format(new Date(value)); + return (value) => formatter.format(parseDateInvariant(value)); }); - Fmt.registerFactory( - ['datetime', 'dt'], - (fmt, format = 'yyyyMd hhmm') => { - let culture = Culture.getDateTimeCulture(); - let formatter = culture.getFormatter(format); - return value => formatter.format(new Date(value)); - } - ); + Fmt.registerFactory(["datetime", "dt"], (fmt, format = "yyyyMd hhmm") => { + let culture = Culture.getDateTimeCulture(); + let formatter = culture.getFormatter(format); + return (value) => formatter.format(new Date(value)); + }); GlobalCacheIdentifier.change(); } diff --git a/packages/cx/src/util/Format.js b/packages/cx/src/util/Format.js index 1613781e8..446773c54 100644 --- a/packages/cx/src/util/Format.js +++ b/packages/cx/src/util/Format.js @@ -1,121 +1,116 @@ -import {debug} from "./Debug"; -import {GlobalCacheIdentifier} from './GlobalCacheIdentifier'; -import {isNumber} from '../util/isNumber'; -import {isUndefined} from '../util/isUndefined'; -import {isArray} from '../util/isArray'; +import { isArray } from "../util/isArray"; +import { isNumber } from "../util/isNumber"; +import { isUndefined } from "../util/isUndefined"; +import { parseDateInvariant } from "./date"; +import { debug } from "./Debug"; +import { GlobalCacheIdentifier } from "./GlobalCacheIdentifier"; //Culture dependent formatters are defined in the ui package. -const defaultFormatter = v => v.toString(); +const defaultFormatter = (v) => v.toString(); let formatFactory = { - - string: function() { - return defaultFormatter + string: function () { + return defaultFormatter; }, - wrap: function(part0, prefix, suffix) { - if (!prefix) - prefix = ''; + wrap: function (part0, prefix, suffix) { + if (!prefix) prefix = ""; - if (!suffix) - suffix = ''; + if (!suffix) suffix = ""; - return value => prefix + value.toString() + suffix; + return (value) => prefix + value.toString() + suffix; }, - fixed: function(part0, digits) { - return value => value.toFixed(digits) + fixed: function (part0, digits) { + return (value) => value.toFixed(digits); }, - prefix: function(part0, prefix) { - if (!prefix) - prefix = ''; + prefix: function (part0, prefix) { + if (!prefix) prefix = ""; - return value => prefix + value.toString(); + return (value) => prefix + value.toString(); }, - suffix: function(part0, suffix) { - if (!suffix) - suffix = ''; + suffix: function (part0, suffix) { + if (!suffix) suffix = ""; - return value => value.toString() + suffix; + return (value) => value.toString() + suffix; }, - uppercase: function() { - return value => value.toString().toUpperCase(); + uppercase: function () { + return (value) => value.toString().toUpperCase(); }, - lowercase: function() { - return value => value.toString().toLowerCase(); + lowercase: function () { + return (value) => value.toString().toLowerCase(); }, - urlencode: function() { - return value => encodeURIComponent(value); + urlencode: function () { + return (value) => encodeURIComponent(value); }, - + number: function (part0, minFractionDigits, maxFractionDigits) { - let {minimumFractionDigits, maximumFractionDigits} = resolveMinMaxFractionDigits(minFractionDigits, maxFractionDigits); + let { minimumFractionDigits, maximumFractionDigits } = resolveMinMaxFractionDigits( + minFractionDigits, + maxFractionDigits, + ); let trimmable = maximumFractionDigits - minimumFractionDigits; if (trimmable > 0) { - if (minimumFractionDigits == 0) - ++trimmable; - return value => trimFractionZeros(value.toFixed(maximumFractionDigits), trimmable); + if (minimumFractionDigits == 0) ++trimmable; + return (value) => trimFractionZeros(value.toFixed(maximumFractionDigits), trimmable); } - return value => value.toFixed(maximumFractionDigits); + return (value) => value.toFixed(maximumFractionDigits); }, percentage: function (part0, minFractionDigits, maxFractionDigits) { let numberFormatter = formatFactory.number(part0, minFractionDigits, maxFractionDigits); - return value => numberFormatter(value * 100) + '%'; + return (value) => numberFormatter(value * 100) + "%"; }, percentageSign: function (part0, minFractionDigits, maxFractionDigits) { let numberFormatter = formatFactory.number(part0, minFractionDigits, maxFractionDigits); - return value => numberFormatter(value) + '%'; + return (value) => numberFormatter(value) + "%"; }, date: function () { - return value => { - let date = new Date(value); + return (value) => { + let date = parseDateInvariant(value); return `${date.getMonth() + 1}/${date.getDate()}/${date.getFullYear()}`; - } + }; }, time: function () { - return value => { - let date = new Date(value); - let h = date.getHours() >= 10 ? date.getHours() : '0' + date.getHours(); - let m = date.getMinutes() >= 10 ? date.getMinutes() : '0' + date.getMinutes(); + return (value) => { + let date = parseDateInvariant(value); + let h = date.getHours() >= 10 ? date.getHours() : "0" + date.getHours(); + let m = date.getMinutes() >= 10 ? date.getMinutes() : "0" + date.getMinutes(); return `${h}:${m}`; - } + }; }, datetime: function () { let date = formatFactory.date(); let time = formatFactory.time(); - return value => date(value) + ' ' + time(value); + return (value) => date(value) + " " + time(value); }, ellipsis: function (part0, length, where) { length = Number(length); - if (!(length > 3)) - length = 10; + if (!(length > 3)) length = 10; switch (where) { default: case "end": return (value) => { let s = String(value); - if (s.length > length) - return s.substring(0, length - 3) + "..."; + if (s.length > length) return s.substring(0, length - 3) + "..."; return s; }; case "start": return (value) => { let s = String(value); - if (s.length > length) - return "..." + s.substring(s.length - length + 3); + if (s.length > length) return "..." + s.substring(s.length - length + 3); return s; }; @@ -129,7 +124,7 @@ let formatFactory = { return s; }; } - } + }, }; formatFactory.s = formatFactory.str = formatFactory.string; @@ -142,26 +137,25 @@ formatFactory.t = formatFactory.time; formatFactory.dt = formatFactory.datetime; function buildFormatter(format) { - let formatter = defaultFormatter, nullText = ''; + let formatter = defaultFormatter, + nullText = ""; if (format) { - let pipeParts = format.split('|'); - nullText = pipeParts[1] || ''; - let colonSepParts = pipeParts[0].split(':'); + let pipeParts = format.split("|"); + nullText = pipeParts[1] || ""; + let colonSepParts = pipeParts[0].split(":"); for (let i = 0; i < colonSepParts.length; i++) { - let parts = colonSepParts[i].split(';'); + let parts = colonSepParts[i].split(";"); let factory = formatFactory[parts[0]]; - if (!factory) - debug('Unknown string format: ' + format); - else if (i == 0) - formatter = factory(...parts); + if (!factory) debug("Unknown string format: " + format); + else if (i == 0) formatter = factory(...parts); else { let outerFmt = factory(...parts); let innerFmt = formatter; - formatter = v => outerFmt(innerFmt(v)); + formatter = (v) => outerFmt(innerFmt(v)); } } } - return v => (v == null || v === '') ? nullText : formatter(v); + return (v) => (v == null || v === "" ? nullText : formatter(v)); } let format = { @@ -172,25 +166,22 @@ function getFormatCache() { if (format.cacheIdentifier != GlobalCacheIdentifier.get()) { format = { cache: {}, - cacheIdentifier: GlobalCacheIdentifier.get() + cacheIdentifier: GlobalCacheIdentifier.get(), }; } return format.cache; } function getFormatter(format) { - if (!format) - format = ''; + if (!format) format = ""; let formatCache = getFormatCache(); let formatter = formatCache[format]; - if (!formatter) - formatter = formatCache[format] = buildFormatter(format); + if (!formatter) formatter = formatCache[format] = buildFormatter(format); return formatter; } export class Format { - static value(v, format) { let formatter = getFormatter(format); return formatter(v); @@ -205,10 +196,8 @@ export class Format { } static registerFactory(format, factory) { - if (isArray(format)) - format.forEach(f => this.registerFactory(f, factory)); - else - formatFactory[format] = factory; + if (isArray(format)) format.forEach((f) => this.registerFactory(f, factory)); + else formatFactory[format] = factory; } } @@ -217,26 +206,24 @@ export function resolveMinMaxFractionDigits(minimumFractionDigits, maximumFracti maximumFractionDigits = maximumFractionDigits != null ? Number(maximumFractionDigits) : maximumFractionDigits; if (isNumber(minimumFractionDigits)) { - if (isUndefined(maximumFractionDigits)) - maximumFractionDigits = minimumFractionDigits; + if (isUndefined(maximumFractionDigits)) maximumFractionDigits = minimumFractionDigits; else if (isNumber(maximumFractionDigits) && maximumFractionDigits < minimumFractionDigits) maximumFractionDigits = minimumFractionDigits; - } - else if (minimumFractionDigits == null && maximumFractionDigits == null) { + } else if (minimumFractionDigits == null && maximumFractionDigits == null) { minimumFractionDigits = 0; maximumFractionDigits = 18; } return { minimumFractionDigits, - maximumFractionDigits - } + maximumFractionDigits, + }; } export function trimFractionZeros(str, max) { - let cnt = 0, l = str.length; - while (cnt < max && (str[l - 1 - cnt] === '0' || str[l - 1 - cnt] === '.')) - cnt++; + let cnt = 0, + l = str.length; + while (cnt < max && (str[l - 1 - cnt] === "0" || str[l - 1 - cnt] === ".")) cnt++; return cnt > 0 ? str.substring(0, l - cnt) : str; -} \ No newline at end of file +} diff --git a/packages/cx/src/util/date/index.d.ts b/packages/cx/src/util/date/index.d.ts index 5a110eae5..13c07b2b1 100644 --- a/packages/cx/src/util/date/index.d.ts +++ b/packages/cx/src/util/date/index.d.ts @@ -1,9 +1,10 @@ -export * from './dateDiff'; -export * from './zeroTime'; -export * from './monthStart'; -export * from './lowerBoundCheck'; -export * from './upperBoundCheck'; -export * from './maxDate'; -export * from './minDate'; -export * from './sameDate'; -export * from './encodeDateWithTimezoneOffset'; \ No newline at end of file +export * from "./dateDiff"; +export * from "./zeroTime"; +export * from "./monthStart"; +export * from "./lowerBoundCheck"; +export * from "./upperBoundCheck"; +export * from "./maxDate"; +export * from "./minDate"; +export * from "./sameDate"; +export * from "./encodeDateWithTimezoneOffset"; +export * from "./parseDateInvariant"; diff --git a/packages/cx/src/util/date/index.js b/packages/cx/src/util/date/index.js index 186e81508..13c07b2b1 100644 --- a/packages/cx/src/util/date/index.js +++ b/packages/cx/src/util/date/index.js @@ -1,9 +1,10 @@ -export * from './dateDiff'; -export * from './zeroTime'; -export * from './monthStart'; -export * from './lowerBoundCheck'; -export * from './upperBoundCheck'; -export * from './maxDate'; -export * from './minDate'; -export * from './sameDate'; -export * from './encodeDateWithTimezoneOffset'; +export * from "./dateDiff"; +export * from "./zeroTime"; +export * from "./monthStart"; +export * from "./lowerBoundCheck"; +export * from "./upperBoundCheck"; +export * from "./maxDate"; +export * from "./minDate"; +export * from "./sameDate"; +export * from "./encodeDateWithTimezoneOffset"; +export * from "./parseDateInvariant"; diff --git a/packages/cx/src/util/date/parseDateInvariant.d.ts b/packages/cx/src/util/date/parseDateInvariant.d.ts new file mode 100644 index 000000000..c94e1d82e --- /dev/null +++ b/packages/cx/src/util/date/parseDateInvariant.d.ts @@ -0,0 +1,3 @@ +export function parseDateInvariant(input: string | number | Date): Date; + +export function overrideParseDateInvariant(newImpl: (input: string | number | Date) => Date): void; diff --git a/packages/cx/src/util/date/parseDateInvariant.js b/packages/cx/src/util/date/parseDateInvariant.js new file mode 100644 index 000000000..ff8c8826e --- /dev/null +++ b/packages/cx/src/util/date/parseDateInvariant.js @@ -0,0 +1,20 @@ +// This module addresses a common issue when handling date strings in the format "yyyy-MM-dd" usually returned by backends. +// In time zones earlier than UTC, creating a Date object from such a string can result in the date being shifted one day earlier. +// This happens because "yyyy-MM-dd" is interpreted as a UTC date at 00:00, and when the browser displays it in local time, it adjusts backward. +// To resolve this, the default implementation (`defaultInvariantParseDate`) appends " 00:00" to the date string, +// explicitly indicating local time. Custom parsing logic can also be registered dynamically using `registerInvariantParseDateImpl` +// to accommodate other formats or requirements. +function defaultParseDateInvariant(input) { + if (typeof input == "string" && input.length == 10 && input[4] == "-" && input[7] == "-") + return new Date(`${input} 00:00`); + return new Date(input); +} +let impl = defaultParseDateInvariant; + +export function parseDateInvariant(input) { + return impl(input); +} + +export function overrideParseDateInvariant(newImpl) { + impl = newImpl; +} diff --git a/packages/cx/src/widgets/form/Calendar.js b/packages/cx/src/widgets/form/Calendar.js index 30a4aa388..8c9a8cfe1 100644 --- a/packages/cx/src/widgets/form/Calendar.js +++ b/packages/cx/src/widgets/form/Calendar.js @@ -1,26 +1,27 @@ -import { Widget, VDOM } from "../../ui/Widget"; -import { Field, getFieldTooltip } from "./Field"; -import { Culture } from "../../ui/Culture"; -import { FocusManager, oneFocusOut, offFocusOut } from "../../ui/FocusManager"; import { StringTemplate } from "../../data/StringTemplate"; -import { zeroTime } from "../../util/date/zeroTime"; +import { Culture } from "../../ui/Culture"; +import { FocusManager, offFocusOut, oneFocusOut } from "../../ui/FocusManager"; +import "../../ui/Format"; +import { Localization } from "../../ui/Localization"; +import { VDOM, Widget } from "../../ui/Widget"; +import { parseDateInvariant } from "../../util"; +import { KeyCode } from "../../util/KeyCode"; import { dateDiff } from "../../util/date/dateDiff"; import { lowerBoundCheck } from "../../util/date/lowerBoundCheck"; -import { upperBoundCheck } from "../../util/date/upperBoundCheck"; +import { monthStart } from "../../util/date/monthStart"; import { sameDate } from "../../util/date/sameDate"; +import { upperBoundCheck } from "../../util/date/upperBoundCheck"; +import { zeroTime } from "../../util/date/zeroTime"; +import DropdownIcon from "../icons/drop-down"; +import ForwardIcon from "../icons/forward"; import { - tooltipParentWillReceiveProps, - tooltipParentWillUnmount, - tooltipMouseMove, tooltipMouseLeave, + tooltipMouseMove, tooltipParentDidMount, + tooltipParentWillReceiveProps, + tooltipParentWillUnmount, } from "../overlay/tooltip-ops"; -import { KeyCode } from "../../util/KeyCode"; -import { Localization } from "../../ui/Localization"; -import ForwardIcon from "../icons/forward"; -import DropdownIcon from "../icons/drop-down"; -import "../../ui/Format"; -import { monthStart } from "../../util/date/monthStart"; +import { Field, getFieldTooltip } from "./Field"; export class Calendar extends Field { declareData() { @@ -37,7 +38,7 @@ export class Calendar extends Field { focusable: undefined, dayData: undefined, }, - ...arguments + ...arguments, ); } @@ -53,17 +54,17 @@ export class Calendar extends Field { }; if (data.value) { - let d = new Date(data.value); + let d = parseDateInvariant(data.value); if (!isNaN(d.getTime())) { data.date = zeroTime(d); } } - if (data.refDate) data.refDate = zeroTime(new Date(data.refDate)); + if (data.refDate) data.refDate = zeroTime(parseDateInvariant(data.refDate)); - if (data.maxValue) data.maxValue = zeroTime(new Date(data.maxValue)); + if (data.maxValue) data.maxValue = zeroTime(parseDateInvariant(data.maxValue)); - if (data.minValue) data.minValue = zeroTime(new Date(data.minValue)); + if (data.minValue) data.minValue = zeroTime(parseDateInvariant(data.minValue)); super.prepareData(...arguments); } @@ -92,7 +93,7 @@ export class Calendar extends Field { } if (data.dayData) { - let date = new Date(data.value); + let date = parseDateInvariant(data.value); let info = data.dayData[date.toDateString()]; if (info && info.disabled) data.error = this.disabledDaysOfWeekErrorText; } @@ -117,7 +118,7 @@ export class Calendar extends Field { if (this.onBeforeSelect && instance.invoke("onBeforeSelect", e, instance, date) === false) return; if (widget.partial) { - let mixed = new Date(data.value); + let mixed = parseDateInvariant(data.value); if (data.value && !isNaN(mixed)) { mixed.setFullYear(date.getFullYear()); mixed.setMonth(date.getMonth()); @@ -176,7 +177,7 @@ export class CalendarCmp extends VDOM.Component { focus: false, cursor: zeroTime(data.date || refDate), }, - this.getPage(refDate) + this.getPage(refDate), ); this.handleMouseMove = this.handleMouseMove.bind(this); @@ -414,7 +415,7 @@ export class CalendarCmp extends VDOM.Component { today: widget.highlightToday && sameDate(date, today), }), dayInfo.className, - CSS.mod(dayInfo.mod) + CSS.mod(dayInfo.mod), ); let dateInst = new Date(date); days.push( @@ -429,7 +430,7 @@ export class CalendarCmp extends VDOM.Component { onMouseDown={unselectable ? null : this.handleMouseDown} > {date.getDate()} - + , ); date.setDate(date.getDate() + 1); } @@ -438,7 +439,7 @@ export class CalendarCmp extends VDOM.Component { {days} - + , ); } diff --git a/packages/cx/src/widgets/form/DateTimeField.js b/packages/cx/src/widgets/form/DateTimeField.js index 6db9948aa..3bb4709f9 100644 --- a/packages/cx/src/widgets/form/DateTimeField.js +++ b/packages/cx/src/widgets/form/DateTimeField.js @@ -26,6 +26,7 @@ import { stopPropagation } from "../../util/eventCallbacks"; import { Format } from "../../util/Format"; import { TimeList } from "./TimeList"; import { autoFocus } from "../autoFocus"; +import { parseDateInvariant } from "../../util"; export class DateTimeField extends Field { declareData() { @@ -45,7 +46,7 @@ export class DateTimeField extends Field { icon: undefined, autoOpen: undefined, }, - ...arguments + ...arguments, ); } @@ -76,7 +77,9 @@ export class DateTimeField extends Field { let { data } = instance; if (data.value) { - let date = new Date(data.value); + let date = parseDateInvariant(data.value); + // let date = new Date(data.value); + if (isNaN(date.getTime())) data.formatted = String(data.value); else { // handle utc edge cases @@ -86,11 +89,11 @@ export class DateTimeField extends Field { data.date = date; } else data.formatted = ""; - if (data.refDate) data.refDate = zeroTime(new Date(data.refDate)); + if (data.refDate) data.refDate = zeroTime(parseDateInvariant(data.refDate)); - if (data.maxValue) data.maxValue = new Date(data.maxValue); + if (data.maxValue) data.maxValue = parseDateInvariant(data.maxValue); - if (data.minValue) data.minValue = new Date(data.minValue); + if (data.minValue) data.minValue = parseDateInvariant(data.minValue); if (this.segment == "date") { if (data.minValue) data.minValue = zeroTime(data.minValue); @@ -345,7 +348,7 @@ class DateTimeInput extends VDOM.Component { icon: !!icon, empty: empty && !data.placeholder, error: data.error && (state.visited || !suppressErrorsUntilVisited || !empty), - }) + }), )} style={data.style} onMouseDown={this.onMouseDown.bind(this)} @@ -539,7 +542,7 @@ class DateTimeInput extends VDOM.Component { }); if (!isNaN(date)) { - let mixed = new Date(baseValue); + let mixed = parseDateInvariant(baseValue); if (date && baseValue && !isNaN(mixed) && widget.partial) { switch (widget.segment) { case "date": diff --git a/packages/cx/src/widgets/form/DateTimePicker.js b/packages/cx/src/widgets/form/DateTimePicker.js index 2111da995..cdc0cadb0 100644 --- a/packages/cx/src/widgets/form/DateTimePicker.js +++ b/packages/cx/src/widgets/form/DateTimePicker.js @@ -5,6 +5,7 @@ import { WheelComponent } from "./Wheel"; import { oneFocusOut, offFocusOut } from "../../ui/FocusManager"; import { enableCultureSensitiveFormatting } from "../../ui/Format"; +import { parseDateInvariant } from "../../util"; enableCultureSensitiveFormatting(); export class DateTimePicker extends Widget { @@ -37,7 +38,7 @@ DateTimePicker.prototype.showSeconds = false; class DateTimePickerComponent extends VDOM.Component { constructor(props) { super(props); - let date = props.data.value ? new Date(props.data.value) : new Date(); + let date = props.data.value ? parseDateInvariant(props.data.value) : new Date(); if (isNaN(date.getTime())) date = new Date(); this.state = { date: date, @@ -67,7 +68,7 @@ class DateTimePickerComponent extends VDOM.Component { } UNSAFE_componentWillReceiveProps(props) { - let date = props.data.value ? new Date(props.data.value) : new Date(); + let date = props.data.value ? parseDateInvariant(props.data.value) : new Date(); if (isNaN(date.getTime())) date = new Date(); this.setState({ date }); } @@ -160,7 +161,7 @@ class DateTimePickerComponent extends VDOM.Component { (state) => ({ date: this.setDateComponent(this.state.date, "year", newIndex + 1970), }), - this.handleChange + this.handleChange, ); }} onPipeKeyDown={(kd) => { @@ -186,7 +187,7 @@ class DateTimePickerComponent extends VDOM.Component { (state) => ({ date: this.setDateComponent(this.state.date, "month", newIndex), }), - this.handleChange + this.handleChange, ); }} onPipeKeyDown={(kd) => { @@ -214,7 +215,7 @@ class DateTimePickerComponent extends VDOM.Component { (state) => ({ date: this.setDateComponent(this.state.date, "date", newIndex + 1), }), - this.handleChange + this.handleChange, ); }} onPipeKeyDown={(kd) => { @@ -240,7 +241,7 @@ class DateTimePickerComponent extends VDOM.Component { (state) => ({ date: this.setDateComponent(this.state.date, "hours", newIndex), }), - this.handleChange + this.handleChange, ); }} onPipeKeyDown={(kd) => { @@ -266,7 +267,7 @@ class DateTimePickerComponent extends VDOM.Component { (state) => ({ date: this.setDateComponent(this.state.date, "minutes", newIndex), }), - this.handleChange + this.handleChange, ); }} onPipeKeyDown={(kd) => { @@ -292,7 +293,7 @@ class DateTimePickerComponent extends VDOM.Component { (state) => ({ date: this.setDateComponent(this.state.date, "seconds", newIndex), }), - this.handleChange + this.handleChange, ); }} onPipeKeyDown={(kd) => { diff --git a/packages/cx/src/widgets/form/MonthField.js b/packages/cx/src/widgets/form/MonthField.js index 72d4c77ce..7c0c1dc02 100644 --- a/packages/cx/src/widgets/form/MonthField.js +++ b/packages/cx/src/widgets/form/MonthField.js @@ -18,7 +18,6 @@ import { } from "../overlay/tooltip-ops"; import { stopPropagation } from "../../util/eventCallbacks"; import { Icon } from "../Icon"; -import CalendarIcon from "../icons/calendar"; import DropdownIcon from "../icons/drop-down"; import ClearIcon from "../icons/clear"; import { KeyCode } from "../../util/KeyCode"; @@ -27,6 +26,9 @@ import { isTouchDevice } from "../../util/isTouchDevice"; import { Localization } from "../../ui/Localization"; import { isDefined } from "../../util/isDefined"; import { autoFocus } from "../autoFocus"; +import { Field, getFieldTooltip } from "./Field"; +import { MonthPicker } from "./MonthPicker"; +import { parseDateInvariant } from "../../util"; export class MonthField extends Field { declareData() { @@ -63,7 +65,7 @@ export class MonthField extends Field { maxExclusive: undefined, icon: undefined, }, - ...arguments + ...arguments, ); } @@ -92,11 +94,11 @@ export class MonthField extends Field { }; if (!this.range && data.value) { - data.date = new Date(data.value); + data.date = parseDateInvariant(data.value); data.formatted = this.culture.format(data.date, formatOptions); } else if (this.range && data.from && data.to) { - data.from = new Date(data.from); - data.to = new Date(data.to); + data.from = parseDateInvariant(data.from); + data.to = parseDateInvariant(data.to); data.to.setDate(data.to.getDate() - 1); let fromStr = this.culture.format(data.from, formatOptions); let toStr = this.culture.format(data.to, formatOptions); @@ -104,11 +106,11 @@ export class MonthField extends Field { else data.formatted = fromStr; } - if (data.refDate) data.refDate = monthStart(new Date(data.refDate)); + if (data.refDate) data.refDate = monthStart(parseDateInvariant(data.refDate)); - if (data.maxValue) data.maxValue = monthStart(new Date(data.maxValue)); + if (data.maxValue) data.maxValue = monthStart(parseDateInvariant(data.maxValue)); - if (data.minValue) data.minValue = monthStart(new Date(data.minValue)); + if (data.minValue) data.minValue = monthStart(parseDateInvariant(data.minValue)); instance.lastDropdown = context.lastDropdown; } @@ -322,7 +324,7 @@ class MonthInput extends VDOM.Component { icon: !!icon, empty: empty && !data.placeholder, error: data.error && (state.visited || !suppressErrorsUntilVisited || !empty), - }) + }), )} style={data.style} onMouseDown={this.onMouseDown.bind(this)} diff --git a/packages/cx/src/widgets/form/MonthPicker.js b/packages/cx/src/widgets/form/MonthPicker.js index 47c03bd80..a45a0dbd3 100644 --- a/packages/cx/src/widgets/form/MonthPicker.js +++ b/packages/cx/src/widgets/form/MonthPicker.js @@ -26,6 +26,7 @@ import { isTouchEvent } from "../../util/isTouchEvent"; import { getCursorPos } from "../overlay/captureMouse"; import { enableCultureSensitiveFormatting } from "../../ui/Format"; +import { parseDateInvariant } from "../../util"; enableCultureSensitiveFormatting(); export class MonthPicker extends Field { @@ -59,7 +60,7 @@ export class MonthPicker extends Field { maxValue: undefined, maxExclusive: undefined, }, - ...arguments + ...arguments, ); } @@ -72,19 +73,19 @@ export class MonthPicker extends Field { disabled: data.disabled, }; - if (!this.range && data.value) data.date = monthStart(new Date(data.value)); + if (!this.range && data.value) data.date = monthStart(parseDateInvariant(data.value)); if (this.range) { - if (data.from) data.from = monthStart(new Date(data.from)); + if (data.from) data.from = monthStart(parseDateInvariant(data.from)); - if (data.to) data.to = monthStart(new Date(data.to)); + if (data.to) data.to = monthStart(parseDateInvariant(data.to)); } - if (data.refDate) data.refDate = monthStart(new Date(data.refDate)); + if (data.refDate) data.refDate = monthStart(parseDateInvariant(data.refDate)); - if (data.maxValue) data.maxValue = monthStart(new Date(data.maxValue)); + if (data.maxValue) data.maxValue = monthStart(parseDateInvariant(data.maxValue)); - if (data.minValue) data.minValue = monthStart(new Date(data.minValue)); + if (data.minValue) data.minValue = monthStart(parseDateInvariant(data.minValue)); super.prepareData(...arguments); } @@ -271,7 +272,7 @@ export class MonthPickerComponent extends VDOM.Component { cursorQuarter: (cursorQuarter + 3) % 4, cursorYear: cursorQuarter == 0 ? cursorYear - 1 : cursorYear, }, - { ensureVisible: true } + { ensureVisible: true }, ); else if (column == "M") if (cursorMonth > 3) this.moveCursor(e, { cursorMonth: cursorMonth - 3 }, { ensureVisible: true }); @@ -279,7 +280,7 @@ export class MonthPickerComponent extends VDOM.Component { this.moveCursor( e, { cursorMonth: cursorMonth + 9, cursorYear: cursorYear - 1 }, - { ensureVisible: true } + { ensureVisible: true }, ); break; @@ -292,7 +293,7 @@ export class MonthPickerComponent extends VDOM.Component { cursorQuarter: (cursorQuarter + 1) % 4, cursorYear: cursorQuarter == 3 ? cursorYear + 1 : cursorYear, }, - { ensureVisible: true } + { ensureVisible: true }, ); else if (column == "M") if (cursorMonth < 10) this.moveCursor(e, { cursorMonth: cursorMonth + 3 }, { ensureVisible: true }); @@ -300,7 +301,7 @@ export class MonthPickerComponent extends VDOM.Component { this.moveCursor( e, { cursorMonth: cursorMonth - 9, cursorYear: cursorYear + 1 }, - { ensureVisible: true } + { ensureVisible: true }, ); break; @@ -469,7 +470,7 @@ export class MonthPickerComponent extends VDOM.Component { onMouseUp={this.handleMouseUp} > {y} - + , ); for (let i = 0; i < 3; i++) { @@ -499,7 +500,7 @@ export class MonthPickerComponent extends VDOM.Component { onTouchEnd={this.handleMouseUp} > {monthNames[m - 1].substr(0, 3)} - + , ); } row.push( @@ -518,7 +519,7 @@ export class MonthPickerComponent extends VDOM.Component { onMouseUp={this.handleMouseUp} > {`Q${q + 1}`} - + , ); rows.push(row); } @@ -567,7 +568,7 @@ export class MonthPickerComponent extends VDOM.Component { let visibleItems = ceil5(Math.ceil(this.dom.el.offsetHeight / this.state.yearHeight)); let start = Math.max( startYear, - startYear + floor5(Math.floor(this.dom.el.scrollTop / this.state.yearHeight)) - visibleItems + startYear + floor5(Math.floor(this.dom.el.scrollTop / this.state.yearHeight)) - visibleItems, ); if (start != this.state.start && start + bufferSize <= endYear) { this.setState({ @@ -605,7 +606,7 @@ export class MonthPickerComponent extends VDOM.Component { this.dom.el.scrollTop = (this.state.cursorYear - startYear + yearCount / 2) * this.state.yearHeight - this.dom.el.offsetHeight / 2; - } + }, ); } From d1e93b3ebf1b59a1ddd9cc2f9f69de363c74205a Mon Sep 17 00:00:00 2001 From: Nebojsa Date: Mon, 16 Dec 2024 15:22:11 +0100 Subject: [PATCH 2/3] resolve imports --- packages/cx/src/widgets/form/MonthField.js | 42 +++++++++++----------- 1 file changed, 20 insertions(+), 22 deletions(-) diff --git a/packages/cx/src/widgets/form/MonthField.js b/packages/cx/src/widgets/form/MonthField.js index 7c0c1dc02..b5052880b 100644 --- a/packages/cx/src/widgets/form/MonthField.js +++ b/packages/cx/src/widgets/form/MonthField.js @@ -1,34 +1,32 @@ -import { Widget, VDOM, getContent } from "../../ui/Widget"; -import { Cx } from "../../ui/Cx"; -import { Field, getFieldTooltip } from "./Field"; -import { MonthPicker } from "./MonthPicker"; import { DateTimeCulture } from "intl-io"; -import { Format } from "../../util/Format"; -import { Dropdown } from "../overlay/Dropdown"; -import { Console } from "../../util/Console"; import { StringTemplate } from "../../data/StringTemplate"; -import { monthStart } from "../../util/date/monthStart"; +import { Cx } from "../../ui/Cx"; +import { Localization } from "../../ui/Localization"; +import { VDOM, Widget, getContent } from "../../ui/Widget"; +import { parseDateInvariant } from "../../util"; +import { Console } from "../../util/Console"; +import { Format } from "../../util/Format"; +import { KeyCode } from "../../util/KeyCode"; import { dateDiff } from "../../util/date/dateDiff"; +import { monthStart } from "../../util/date/monthStart"; +import { stopPropagation } from "../../util/eventCallbacks"; +import { isDefined } from "../../util/isDefined"; +import { isTouchDevice } from "../../util/isTouchDevice"; +import { isTouchEvent } from "../../util/isTouchEvent"; +import { Icon } from "../Icon"; +import { autoFocus } from "../autoFocus"; +import ClearIcon from "../icons/clear"; +import DropdownIcon from "../icons/drop-down"; +import { Dropdown } from "../overlay/Dropdown"; import { - tooltipParentWillReceiveProps, - tooltipParentWillUnmount, - tooltipMouseMove, tooltipMouseLeave, + tooltipMouseMove, tooltipParentDidMount, + tooltipParentWillReceiveProps, + tooltipParentWillUnmount, } from "../overlay/tooltip-ops"; -import { stopPropagation } from "../../util/eventCallbacks"; -import { Icon } from "../Icon"; -import DropdownIcon from "../icons/drop-down"; -import ClearIcon from "../icons/clear"; -import { KeyCode } from "../../util/KeyCode"; -import { isTouchEvent } from "../../util/isTouchEvent"; -import { isTouchDevice } from "../../util/isTouchDevice"; -import { Localization } from "../../ui/Localization"; -import { isDefined } from "../../util/isDefined"; -import { autoFocus } from "../autoFocus"; import { Field, getFieldTooltip } from "./Field"; import { MonthPicker } from "./MonthPicker"; -import { parseDateInvariant } from "../../util"; export class MonthField extends Field { declareData() { From b28d08c0a7780635fec42ee69bba27d6b97a8c0a Mon Sep 17 00:00:00 2001 From: Nebojsa Date: Mon, 16 Dec 2024 15:33:34 +0100 Subject: [PATCH 3/3] set branch patch version --- packages/cx/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cx/package.json b/packages/cx/package.json index 89498849a..eaed63cfb 100644 --- a/packages/cx/package.json +++ b/packages/cx/package.json @@ -1,6 +1,6 @@ { "name": "cx", - "version": "23.5.0", + "version": "23.5.3", "description": "Advanced JavaScript UI framework for admin and dashboard applications with ready to use grid, form and chart components.", "main": "index.js", "jsnext:main": "src/index.js",