feat(DateTimeFields): implement free typing time input with dropdown

- Replace default TimePicker field with custom EditableTimeField
- Allow free typing with multiple formats (HH:mm, HHmm, H, etc.)
- Show dropdown on click while maintaining typing capability
- Center-align time input text
This commit is contained in:
lenhanphung
2025-12-10 11:47:05 +07:00
committed by Benoit TELLIER
parent 12776d36a4
commit 627e9bf44b
13 changed files with 605 additions and 223 deletions
@@ -1,5 +1,8 @@
import moment from "moment-timezone";
import dayjs, { Dayjs } from "dayjs";
import customParseFormat from "dayjs/plugin/customParseFormat";
dayjs.extend(customParseFormat);
/**
* Helper functions for date/time string manipulation
@@ -9,6 +12,8 @@ export const DATETIME_WITH_SECONDS_LENGTH = 19;
export const DATETIME_FORMAT_WITH_SECONDS = "YYYY-MM-DDTHH:mm:ss";
export const DATETIME_FORMAT_WITHOUT_SECONDS = "YYYY-MM-DDTHH:mm";
export const TIME_PARSE_FORMATS = ["HH:mm", "H:mm", "HHmm", "Hmm", "HH", "H"];
/**
* Detect datetime format based on string length
* @param datetime - Datetime string to analyze
@@ -83,3 +88,35 @@ export const dtDate = (d: Dayjs) => d.format("YYYY-MM-DD");
/** Extract time “HH:mm” */
export const dtTime = (d: Dayjs) => d.format("HH:mm");
/**
* Parse flexible time string input into Dayjs object
* Handles formats like: "HH:mm", "HHmm", "Hmm", "HH", "H"
* Also handles 3-digit input normalization (e.g. "830" -> "08:30")
*/
export function parseTimeInput(
value: string,
currentDate: Dayjs | null
): Dayjs | null {
let trimmed = value.trim();
if (!trimmed) {
return null;
}
// Normalize 3-digit input (e.g. "830" → "0830" → 08:30, "123" → "0123" → 01:23)
if (/^\d{3}$/.test(trimmed)) {
trimmed = trimmed.padStart(4, "0");
}
const parsed = dayjs(trimmed, TIME_PARSE_FORMATS, true);
if (parsed.isValid()) {
const baseDate = currentDate || dayjs();
return baseDate
.hour(parsed.hour())
.minute(parsed.minute())
.second(0)
.millisecond(0);
}
return null;
}
+5 -25
View File
@@ -45,9 +45,12 @@ export function validateEventForm(params: ValidationParams): ValidationResult {
let dateTimeError = "";
// Determine which fields are visible based on UI mode
const showFullFields = showMore || hasEndDateChanged;
const showFullFields =
showMore ||
allday ||
hasEndDateChanged ||
(!showMore && !allday && startDate !== endDate);
const showTimeOnly = !allday && !showFullFields;
const showDateOnly = allday;
// Validate start date
if (!startDate || startDate.trim() === "") {
@@ -123,29 +126,6 @@ export function validateEventForm(params: ValidationParams): ValidationResult {
dateTimeError = "Invalid time format";
}
}
} else if (showDateOnly) {
// 2 fields mode: validate date only
if (!endDate || endDate.trim() === "") {
isDateTimeValid = false;
dateTimeError = "End date is required";
} else {
const toLocalDate = (ymd: string) => {
const [y, m, d] = ymd.split("-").map((v) => parseInt(v, 10));
if (!y || !m || !d) return new Date(NaN);
return new Date(y, m - 1, d);
};
const startOnly = toLocalDate(startDate);
const endOnly = toLocalDate(endDate);
if (isNaN(startOnly.getTime()) || isNaN(endOnly.getTime())) {
isDateTimeValid = false;
dateTimeError = "Invalid date";
} else if (endOnly < startOnly) {
isDateTimeValid = false;
dateTimeError = "End date must be on or after start date";
}
}
}
const isValid = isDateTimeValid;