diff --git a/litmus-ts/bugs/DateTimePickerInfinite.tsx b/litmus-ts/bugs/DateTimePickerInfinite.tsx new file mode 100644 index 000000000..48178df2d --- /dev/null +++ b/litmus-ts/bugs/DateTimePickerInfinite.tsx @@ -0,0 +1,18 @@ +import { bind, createFunctionalComponent } from "cx/ui"; +import { + DateField, + DateTimeField, + DateTimePicker, + TimeField, +} from "cx/widgets"; + +export default createFunctionalComponent(() => { + return ( + + + + + + + ); +}); diff --git a/litmus-ts/index.tsx b/litmus-ts/index.tsx index 1dbaaf725..7aa1dde69 100644 --- a/litmus-ts/index.tsx +++ b/litmus-ts/index.tsx @@ -1,10 +1,15 @@ import { Store } from "cx/data"; import "cx/locale/de-de.js"; -import { History, Widget } from "cx/ui"; +import { + Culture, + enableCultureSensitiveFormatting, + History, + Widget, +} from "cx/ui"; import { startHotAppLoop } from "cx/ui/app/startHotAppLoop.js"; import { Debug, Timing } from "cx/util"; import { enableMsgBoxAlerts, enableTooltips } from "cx/widgets"; -import Demo from "./bugs/GridOnFetchRecords"; +import test from "./bugs/DateTimePickerInfinite"; let store = new Store(); @@ -14,18 +19,14 @@ Widget.resetCounter(); //Timing.enable('vdom-render'); Timing.enable("app-loop"); Debug.enable("app-data"); +enableCultureSensitiveFormatting(); enableTooltips(); enableMsgBoxAlerts(); History.connect(store, "url"); -startHotAppLoop( - module, - document.getElementById("app"), - store, - +Culture.setCulture("sr-latn"); - - , -); +// @ts-expect-error +startHotAppLoop(module, document.getElementById("app"), store, test); diff --git a/packages/cx/src/widgets/form/DateTimePicker.tsx b/packages/cx/src/widgets/form/DateTimePicker.tsx index c0eacc8ab..9af3b0f8e 100644 --- a/packages/cx/src/widgets/form/DateTimePicker.tsx +++ b/packages/cx/src/widgets/form/DateTimePicker.tsx @@ -12,32 +12,38 @@ import { WheelComponent } from "./Wheel"; enableCultureSensitiveFormatting(); export class DateTimePicker extends Widget { - declare public size: number; - declare public segment: string; - declare public autoFocus?: boolean; - declare public showSeconds?: boolean; - declare public encoding?: (date: Date) => string; - declare public onFocusOut?: string | ((instance: Instance) => void); - declare public onSelect?: string | ((e: React.KeyboardEvent, instance: Instance, date: Date) => void); - declare baseClass: string; - - declareData(...args: Record[]): void { - return super.declareData(...args, { - value: undefined, - }); - } - - render(context: RenderingContext, instance: Instance, key: string): React.ReactNode { - return ( - - ); - } + declare public size: number; + declare public segment: string; + declare public autoFocus?: boolean; + declare public showSeconds?: boolean; + declare public encoding?: (date: Date) => string; + declare public onFocusOut?: string | ((instance: Instance) => void); + declare public onSelect?: + | string + | ((e: React.KeyboardEvent, instance: Instance, date: Date) => void); + declare baseClass: string; + + declareData(...args: Record[]): void { + return super.declareData(...args, { + value: undefined, + }); + } + + render( + context: RenderingContext, + instance: Instance, + key: string, + ): React.ReactNode { + return ( + + ); + } } DateTimePicker.prototype.baseClass = "datetimepicker"; @@ -46,384 +52,504 @@ DateTimePicker.prototype.size = 3; DateTimePicker.prototype.autoFocus = false; DateTimePicker.prototype.segment = "datetime"; DateTimePicker.prototype.showSeconds = false; - interface DateTimePickerComponentProps { - instance: Instance; - data: Record; - size: number; - segment: string; + instance: Instance; + data: Record; + size: number; + segment: string; } interface DateTimePickerComponentState { - date: Date; - activeWheel: string | null; + date: Date; + activeWheel: string | null; + daysResetKey: number; + hoursResetKey: number; + minutesResetKey: number; + secondsResetKey: number; } -class DateTimePickerComponent extends VDOM.Component { - el!: HTMLDivElement; - declare wheels: Record; - keyDownPipes: Record void>; - - constructor(props: DateTimePickerComponentProps) { - super(props); - let date = props.data.value ? parseDateInvariant(props.data.value as string | number | Date) : new Date(); - if (isNaN(date.getTime())) date = new Date(); - this.state = { - date: date, - activeWheel: null, - }; - - let { widget } = props.instance; - let pickerWidget = widget as DateTimePicker; - - this.handleChange = this.handleChange.bind(this); - this.onFocus = this.onFocus.bind(this); - this.onBlur = this.onBlur.bind(this); - this.onKeyDown = this.onKeyDown.bind(this); - - let showDate = props.segment.indexOf("date") !== -1; - let showTime = props.segment.indexOf("time") !== -1; - - this.wheels = { - year: showDate, - month: showDate, - date: showDate, - hours: showTime, - minutes: showTime, - seconds: showTime && !!pickerWidget.showSeconds, - }; - - this.keyDownPipes = {}; - } - - UNSAFE_componentWillReceiveProps(props: DateTimePickerComponentProps): void { - let date = props.data.value ? parseDateInvariant(props.data.value as string | number | Date) : new Date(); - if (isNaN(date.getTime())) date = new Date(); - this.setState({ date }); - } - - setDateComponent(date: Date, component: string, value: number): Date { - let v = new Date(date); - switch (component) { - case "year": - v.setFullYear(value); - break; - - case "month": - v.setMonth(value); - break; - - case "date": - v.setDate(value); - break; - - case "hours": - v.setHours(value); - break; - - case "minutes": - v.setMinutes(value); - break; - - case "seconds": - v.setSeconds(value); - break; - } - return v; - } - - handleChange(): void { - let { widget } = this.props.instance; - let pickerWidget = widget as DateTimePicker; - let encode = pickerWidget.encoding || Culture.getDefaultDateEncoding(); - this.props.instance.set("value", encode!(this.state.date)); - } - - render(): React.ReactNode { - let { instance, data, size } = this.props; - let { widget } = instance; - let { CSS, baseClass } = widget; - let pickerWidget = widget as DateTimePicker; - let date = this.state.date; - - let culture = Culture.getDateTimeCulture(); - let monthNames = culture.getMonthNames("short"); - - let years = []; - for (let y = 1970; y <= 2050; y++) years.push({y}); - - let days = []; - let start = new Date(date.getFullYear(), date.getMonth(), 1); - while (start.getMonth() === date.getMonth()) { - let day = start.getDate(); - days.push({day < 10 ? "0" + day : day}); - start.setDate(start.getDate() + 1); - } - - let hours = []; - for (let h = 0; h < 24; h++) { - hours.push({h < 10 ? "0" + h : h}); - } - - let minutes = []; - for (let m = 0; m < 60; m++) { - minutes.push({m < 10 ? "0" + m : m}); - } - - return ( -
{ - this.el = el!; - }} - className={data.classNames as string} - onFocus={this.onFocus} - onBlur={this.onBlur} - onKeyDown={this.onKeyDown} - > - {this.wheels.year && ( - { - this.setState( - (state) => ({ - date: this.setDateComponent(this.state.date, "year", newIndex + 1970), - }), - this.handleChange, - ); - }} - onPipeKeyDown={(kd) => { - this.keyDownPipes["year"] = kd; - }} - onMouseDown={() => { - this.setState({ activeWheel: "year" }); - }} - > - {years} - - )} - {this.wheels.year && this.wheels.month && -} - {this.wheels.month && ( - { - this.setState( - (state) => ({ - date: this.setDateComponent(this.state.date, "month", newIndex), - }), - this.handleChange, - ); - }} - onPipeKeyDown={(kd) => { - this.keyDownPipes["month"] = kd; - }} - onMouseDown={() => { - this.setState({ activeWheel: "month" }); - }} - > - {monthNames.map((m: string, i: number) => ( - {m} - ))} - - )} - {this.wheels.month && this.wheels.date && -} - {this.wheels.date && ( - { - this.setState( - (state) => ({ - date: this.setDateComponent(this.state.date, "date", newIndex + 1), - }), - this.handleChange, - ); - }} - onPipeKeyDown={(kd) => { - this.keyDownPipes["date"] = kd; - }} - onMouseDown={() => { - this.setState({ activeWheel: "date" }); - }} - > - {days} - - )} - {this.wheels.hours && this.wheels.year && } - {this.wheels.hours && ( - { - this.setState( - (state) => ({ - date: this.setDateComponent(this.state.date, "hours", newIndex), - }), - this.handleChange, - ); - }} - onPipeKeyDown={(kd) => { - this.keyDownPipes["hours"] = kd; - }} - onMouseDown={() => { - this.setState({ activeWheel: "hours" }); - }} - > - {hours} - - )} - {this.wheels.hours && this.wheels.minutes && :} - {this.wheels.minutes && ( - { - this.setState( - (state) => ({ - date: this.setDateComponent(this.state.date, "minutes", newIndex), - }), - this.handleChange, - ); - }} - onPipeKeyDown={(kd) => { - this.keyDownPipes["minutes"] = kd; - }} - onMouseDown={() => { - this.setState({ activeWheel: "minutes" }); - }} - > - {minutes} - - )} - {this.wheels.minutes && this.wheels.seconds && :} - {this.wheels.seconds && ( - { - this.setState( - (state) => ({ - date: this.setDateComponent(this.state.date, "seconds", newIndex), - }), - this.handleChange, - ); - }} - onPipeKeyDown={(kd) => { - this.keyDownPipes["seconds"] = kd; - }} - onMouseDown={() => { - this.setState({ activeWheel: "seconds" }); - }} - > - {minutes} - - )} -
+class DateTimePickerComponent extends VDOM.Component< + DateTimePickerComponentProps, + DateTimePickerComponentState +> { + el!: HTMLDivElement; + declare wheels: Record; + keyDownPipes: Record void>; + declare years: any[]; + declare days: any[]; + declare hours: any[]; + declare minutes: any[]; + declare century: number; + declare numberOfDaysInMonth: number; + + constructor(props: DateTimePickerComponentProps) { + super(props); + let date = props.data.value + ? parseDateInvariant(props.data.value as string | number | Date) + : new Date(); + if (isNaN(date.getTime())) date = new Date(); + this.state = { + date: date, + activeWheel: null, + daysResetKey: 0, + hoursResetKey: 0, + minutesResetKey: 0, + secondsResetKey: 0, + }; + this.century = (date.getFullYear() / 100) | 0; + + let { widget } = props.instance; + let pickerWidget = widget as DateTimePicker; + + this.handleChange = this.handleChange.bind(this); + this.onFocus = this.onFocus.bind(this); + this.onBlur = this.onBlur.bind(this); + this.onKeyDown = this.onKeyDown.bind(this); + + let showDate = props.segment.indexOf("date") !== -1; + let showTime = props.segment.indexOf("time") !== -1; + + this.wheels = { + year: showDate, + month: showDate, + date: showDate, + hours: showTime, + minutes: showTime, + seconds: showTime && !!pickerWidget.showSeconds, + }; + + this.keyDownPipes = {}; + } + + UNSAFE_componentWillReceiveProps(props: DateTimePickerComponentProps): void { + let date = props.data.value + ? parseDateInvariant(props.data.value as string | number | Date) + : new Date(); + if (isNaN(date.getTime())) date = new Date(); + this.setState({ date }); + } + + setDateComponent(date: Date, component: string, value: number): Date { + let v = new Date(date); + switch (component) { + case "year": + v.setFullYear(value); + break; + + case "month": + v.setMonth(value); + break; + + case "date": + v.setDate(value); + break; + + case "hours": + v.setHours(value); + break; + + case "minutes": + v.setMinutes(value); + break; + + case "seconds": + v.setSeconds(value); + break; + } + return v; + } + + handleChange(): void { + let { widget } = this.props.instance; + let pickerWidget = widget as DateTimePicker; + let encode = pickerWidget.encoding || Culture.getDefaultDateEncoding(); + this.props.instance.set("value", encode!(this.state.date)); + } + + render(): React.ReactNode { + let { instance, data, size } = this.props; + let { widget } = instance; + let { CSS, baseClass } = widget; + let pickerWidget = widget as DateTimePicker; + let date = this.state.date; + + let culture = Culture.getDateTimeCulture(); + let monthNames = culture.getMonthNames("short"); + + let years = []; + if (!this.years || this.century !== ((date.getFullYear() / 100) | 0)) { + this.century = (date.getFullYear() / 100) | 0; + + for ( + let y = this.century * 100 - 3; + y <= (this.century + 1) * 100 + 5; + y++ + ) + years.push({y}); + this.years = years; + } else { + years = this.years; + } + + let days = []; + const daysInThisMonth = new Date( + date.getFullYear(), + date.getMonth() + 1, + 0, + ).getDate(); + this.numberOfDaysInMonth ??= daysInThisMonth; + + if (!this.days || this.numberOfDaysInMonth !== daysInThisMonth) { + days = Array.from({ length: 5 }, (_, d) => ( + + {daysInThisMonth - 4 + d < 10 + ? "0" + (daysInThisMonth - 4 + d) + : daysInThisMonth - 4 + d} + + )); + days.push( + ...Array.from({ length: 36 }, (_, d) => ( + + {(d % daysInThisMonth) + 1 < 10 + ? "0" + ((d % daysInThisMonth) + 1) + : (d % daysInThisMonth) + 1} + + )), ); - } - - componentDidMount(): void { - let { widget } = this.props.instance; - let pickerWidget = widget as DateTimePicker; - if (pickerWidget.autoFocus) this.el.focus(); - } - - componentWillUnmount(): void { - offFocusOut(this); - } - - onFocus(): void { - oneFocusOut(this, this.el, this.onFocusOut.bind(this)); - - if (!this.state.activeWheel) { - let firstWheel: string | null = null; - for (let wheel in this.wheels) { - if (this.wheels[wheel]) { - firstWheel = wheel; - break; - } - } - this.setState({ - activeWheel: firstWheel, - }); + this.days = days; + this.numberOfDaysInMonth = daysInThisMonth; + } else { + days = this.days; + } + + let hours = []; + if (!this.hours) { + hours = Array.from({ length: 52 }, (_, h) => ( + {h % 24 < 10 ? "0" + (h % 24) : h % 24} + )); + this.hours = hours; + } else { + hours = this.hours; + } + + let minutes = []; + if (!this.minutes) { + minutes = Array.from({ length: 130 }, (_, h) => ( + {h % 60 < 10 ? "0" + (h % 60) : h % 60} + )); + this.minutes = minutes; + } else { + minutes = this.minutes; + } + + return ( +
{ + this.el = el!; + }} + className={data.classNames as string} + onFocus={this.onFocus} + onBlur={this.onBlur} + onKeyDown={this.onKeyDown} + > + {this.wheels.year && ( + { + this.setState( + (state) => ({ + date: this.setDateComponent( + this.state.date, + "year", + newIndex + Number(this.years[0].key), + ), + }), + this.handleChange, + ); + }} + onPipeKeyDown={(kd) => { + this.keyDownPipes["year"] = kd; + }} + onMouseDown={() => { + this.setState({ activeWheel: "year" }); + }} + > + {years} + + )} + {this.wheels.year && this.wheels.month && -} + {this.wheels.month && ( + { + this.setState( + (state) => ({ + date: this.setDateComponent( + this.state.date, + "month", + newIndex, + ), + }), + this.handleChange, + ); + }} + onPipeKeyDown={(kd) => { + this.keyDownPipes["month"] = kd; + }} + onMouseDown={() => { + this.setState({ activeWheel: "month" }); + }} + > + {monthNames.map((m: string, i: number) => ( + {m} + ))} + + )} + {this.wheels.month && this.wheels.date && -} + {this.wheels.date && ( + { + newDate -= 5; + if (newDate < 0) { + newDate += this.numberOfDaysInMonth; + } else { + newDate = newDate % this.numberOfDaysInMonth; + } + + this.setState( + (state) => ({ + date: this.setDateComponent(state.date, "date", newDate + 1), + daysResetKey: + ((state.date.getDate() - 1) ^ newDate) === + this.numberOfDaysInMonth - 1 + ? state.daysResetKey + 1 + : state.daysResetKey, + }), + this.handleChange, + ); + }} + onPipeKeyDown={(kd) => { + this.keyDownPipes["date"] = kd; + }} + onMouseDown={() => { + this.setState({ activeWheel: "date" }); + }} + > + {days} + + )} + {this.wheels.hours && this.wheels.year && ( + + )} + {this.wheels.hours && ( + { + const newHour = newIndex % 24; + + this.setState( + (s) => ({ + date: this.setDateComponent(s.date, "hours", newHour), + hoursResetKey: + (s.date.getHours() ^ newHour) == 23 + ? s.hoursResetKey + 1 + : s.hoursResetKey, + }), + this.handleChange, + ); + }} + onPipeKeyDown={(kd) => { + this.keyDownPipes["hours"] = kd; + }} + onMouseDown={() => { + this.setState({ activeWheel: "hours" }); + }} + > + {hours} + + )} + {this.wheels.hours && this.wheels.minutes && :} + {this.wheels.minutes && ( + { + const newMinutes = newIndex % 60; + this.setState( + (state) => ({ + date: this.setDateComponent( + state.date, + "minutes", + newMinutes, + ), + minutesResetKey: + (state.date.getMinutes() ^ newMinutes) == 59 + ? state.minutesResetKey + 1 + : state.minutesResetKey, + }), + this.handleChange, + ); + }} + onPipeKeyDown={(kd) => { + this.keyDownPipes["minutes"] = kd; + }} + onMouseDown={() => { + this.setState({ activeWheel: "minutes" }); + }} + > + {minutes} + + )} + {this.wheels.minutes && this.wheels.seconds && :} + {this.wheels.seconds && ( + { + const newSeconds = newIndex % 60; + this.setState( + (state) => ({ + date: this.setDateComponent( + state.date, + "seconds", + newSeconds, + ), + secondsResetKey: + (state.date.getSeconds() ^ newSeconds) == 59 + ? state.secondsResetKey + 1 + : state.secondsResetKey, + }), + this.handleChange, + ); + }} + onPipeKeyDown={(kd) => { + this.keyDownPipes["seconds"] = kd; + }} + onMouseDown={() => { + this.setState({ activeWheel: "seconds" }); + }} + > + {minutes} + + )} +
+ ); + } + + componentDidMount(): void { + let { widget } = this.props.instance; + let pickerWidget = widget as DateTimePicker; + if (pickerWidget.autoFocus) this.el.focus(); + } + + componentWillUnmount(): void { + offFocusOut(this); + } + + onFocus(): void { + oneFocusOut(this, this.el, this.onFocusOut.bind(this)); + + if (!this.state.activeWheel) { + let firstWheel: string | null = null; + for (let wheel in this.wheels) { + if (this.wheels[wheel]) { + firstWheel = wheel; + break; + } } - } - onFocusOut(): void { - let { instance } = this.props; - let { widget } = instance; - let pickerWidget = widget as DateTimePicker; - if (pickerWidget.onFocusOut) instance.invoke("onFocusOut", null, instance); - } - - onBlur(): void { this.setState({ - activeWheel: null, + activeWheel: firstWheel, }); - } - - onKeyDown(e: React.KeyboardEvent): void { - let tmp: string | null = null; - let { instance } = this.props; - let { widget } = instance; - let pickerWidget = widget as DateTimePicker; - - switch (e.keyCode) { - case KeyCode.right: - e.preventDefault(); - for (let wheel in this.wheels) { - if (this.wheels[wheel]) { - if (tmp === this.state.activeWheel) { - this.setState({ activeWheel: wheel }); - break; - } - tmp = wheel; - } + } + } + + onFocusOut(): void { + let { instance } = this.props; + let { widget } = instance; + let pickerWidget = widget as DateTimePicker; + if (pickerWidget.onFocusOut) instance.invoke("onFocusOut", null, instance); + } + + onBlur(): void { + this.setState({ + activeWheel: null, + }); + } + + onKeyDown(e: React.KeyboardEvent): void { + let tmp: string | null = null; + let { instance } = this.props; + let { widget } = instance; + let pickerWidget = widget as DateTimePicker; + + switch (e.keyCode) { + case KeyCode.right: + e.preventDefault(); + for (let wheel in this.wheels) { + if (this.wheels[wheel]) { + if (tmp === this.state.activeWheel) { + this.setState({ activeWheel: wheel }); + break; } - break; - - case KeyCode.left: - e.preventDefault(); - for (let wheel in this.wheels) { - if (this.wheels[wheel]) { - if (wheel === this.state.activeWheel && tmp) { - this.setState({ activeWheel: tmp }); - break; - } - tmp = wheel; - } + tmp = wheel; + } + } + break; + + case KeyCode.left: + e.preventDefault(); + for (let wheel in this.wheels) { + if (this.wheels[wheel]) { + if (wheel === this.state.activeWheel && tmp) { + this.setState({ activeWheel: tmp }); + break; } - break; - - case KeyCode.enter: - e.preventDefault(); - if (pickerWidget.onSelect) instance.invoke("onSelect", e, instance, this.state.date); - break; - - default: let kdp = this.keyDownPipes[this.state.activeWheel!]; - if (kdp) kdp(e); - break; - } - } + tmp = wheel; + } + } + break; + + case KeyCode.enter: + e.preventDefault(); + if (pickerWidget.onSelect) + instance.invoke("onSelect", e, instance, this.state.date); + break; + + default: + let kdp = this.keyDownPipes[this.state.activeWheel!]; + if (kdp) kdp(e); + break; + } + } }