mirror of
https://github.com/wavetermdev/waveterm.git
synced 2025-01-18 21:02:00 +01:00
Datepicker with dark and light theme support (#397)
* datepicker. add electron-rebuild sa dev dependency * comments * use <If> component for conditional renders * invoke callback on month change * individual inputs for date parts * keyboard support for input * more on keyboard support * dynamic setting of input width based on their text value * remove error if fromts is out of range * format is optional * undo electron-rebuild
This commit is contained in:
parent
631b9b89b9
commit
a32bc10981
@ -223,4 +223,12 @@
|
|||||||
--modal-header-bottom-border-color: rgba(241, 246, 243, 0.15);
|
--modal-header-bottom-border-color: rgba(241, 246, 243, 0.15);
|
||||||
|
|
||||||
--logo-button-hover-bg-color: var(--app-accent-bg-color);
|
--logo-button-hover-bg-color: var(--app-accent-bg-color);
|
||||||
|
|
||||||
|
--xterm-viewport-border-color: rgba(241, 246, 243, 0.15);
|
||||||
|
|
||||||
|
--datepicker-cell-hover-bg-color: rgba(255, 255, 255, 0.06);
|
||||||
|
--datepicker-cell-other-text-color: rgba(255, 255, 255, 0.3);
|
||||||
|
--datepicker-header-fade-color: rgba(255, 255, 255, 0.4);
|
||||||
|
--datepicker-year-header-bg-color: rgba(255, 255, 255, 0.2); /* Light grey background */
|
||||||
|
--datepicker-year-header-border-color: rgba(241, 246, 243, 0.15); /* Light grey border */
|
||||||
}
|
}
|
||||||
|
@ -71,6 +71,16 @@
|
|||||||
--line-actions-inactive-color: rgba(0, 0, 0, 0.3);
|
--line-actions-inactive-color: rgba(0, 0, 0, 0.3);
|
||||||
--line-actions-active-color: rgba(0, 0, 0, 1);
|
--line-actions-active-color: rgba(0, 0, 0, 1);
|
||||||
|
|
||||||
|
--logo-button-hover-bg-color: #f0f0f0;
|
||||||
|
|
||||||
|
--xterm-viewport-border-color: rgba(0, 0, 0, 0.3);
|
||||||
|
|
||||||
|
--datepicker-cell-hover-bg-color: #f0f0f0;
|
||||||
|
--datepicker-cell-other-text-color: rgba(0, 0, 0, 0.3);
|
||||||
|
--datepicker-header-fade-color: rgba(0, 0, 0, 0.4);
|
||||||
|
--datepicker-year-header-bg-color: #f5f5f5;
|
||||||
|
--datepicker-year-header-border-color: #dcdcdc;
|
||||||
|
|
||||||
/* toggle colors */
|
/* toggle colors */
|
||||||
--toggle-thumb-color: var(--app-bg-color);
|
--toggle-thumb-color: var(--app-bg-color);
|
||||||
}
|
}
|
||||||
|
183
src/app/common/elements/datepicker.less
Normal file
183
src/app/common/elements/datepicker.less
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
.day-picker-modal {
|
||||||
|
background-color: var(--form-element-bg-color);
|
||||||
|
z-index: 1000;
|
||||||
|
width: 250px;
|
||||||
|
height: 293px;
|
||||||
|
font-size: 13px;
|
||||||
|
border: 1px solid var(--form-element-border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
color: var(--form-element-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-picker-input {
|
||||||
|
cursor: default;
|
||||||
|
border: 1px solid var(--form-element-border-color) !important;
|
||||||
|
height: 34px;
|
||||||
|
user-select: none;
|
||||||
|
display: flex;
|
||||||
|
border-radius: 4px;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0 15px !important;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
input {
|
||||||
|
outline: none !important;
|
||||||
|
border: none !important;
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 !important;
|
||||||
|
cursor: default !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fa-calendar {
|
||||||
|
margin-left: 10px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-picker-header {
|
||||||
|
color: var(--form-element-text-color);
|
||||||
|
padding: 10px 10px 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
height: 40px;
|
||||||
|
position: sticky;
|
||||||
|
|
||||||
|
div {
|
||||||
|
cursor: default;
|
||||||
|
font-weight: bold;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&.fade {
|
||||||
|
color: var(--datepicker-header-fade-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.arrows {
|
||||||
|
display: flex;
|
||||||
|
|
||||||
|
.wave-button {
|
||||||
|
padding: 5px !important;
|
||||||
|
font-size: 18px !important;
|
||||||
|
cursor: default !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-header {
|
||||||
|
display: flex;
|
||||||
|
color: var(--form-element-text-color);
|
||||||
|
padding: 0 10px;
|
||||||
|
|
||||||
|
.day-header-cell {
|
||||||
|
flex: 1;
|
||||||
|
padding: 10px;
|
||||||
|
color: var(--form-element-text-color);
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.day-picker {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(7, 1fr);
|
||||||
|
gap: 0;
|
||||||
|
padding: 0 10px 10px;
|
||||||
|
|
||||||
|
.day {
|
||||||
|
padding: 10px 4px;
|
||||||
|
color: var(--form-element-text-color);
|
||||||
|
text-align: center;
|
||||||
|
cursor: default;
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--datepicker-cell-hover-bg-color);
|
||||||
|
}
|
||||||
|
&.selected {
|
||||||
|
background-color: var(--form-element-primary-color);
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.other-month {
|
||||||
|
color: var(--datepicker-cell-other-text-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.year-month-accordion-wrapper {
|
||||||
|
overflow-y: auto;
|
||||||
|
height: 243px;
|
||||||
|
margin-top: 8px;
|
||||||
|
padding: 0 10px 10px;
|
||||||
|
|
||||||
|
.year-month-accordion {
|
||||||
|
color: var(--form-element-text-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.year-header {
|
||||||
|
padding: 5px 10px;
|
||||||
|
background-color: var(--datepicker-year-header-bg-color);
|
||||||
|
border-bottom: 1px solid var(--datepicker-year-header-border-color);
|
||||||
|
margin-bottom: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: center;
|
||||||
|
cursor: default;
|
||||||
|
color: var(--form-element-text-color);
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.month-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(3, 1fr);
|
||||||
|
gap: 5px;
|
||||||
|
padding: 10px;
|
||||||
|
max-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
transition: max-height 0.3s ease;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.month {
|
||||||
|
padding: 5px;
|
||||||
|
text-align: center;
|
||||||
|
border-radius: 3px;
|
||||||
|
color: var(--form-element-text-color);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--datepicker-cell-hover-bg-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
background-color: var(--form-element-primary-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.expanded {
|
||||||
|
max-height: 500px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-arrow {
|
||||||
|
display: inline-block;
|
||||||
|
margin-left: 5px;
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 5px solid transparent;
|
||||||
|
border-right: 5px solid transparent;
|
||||||
|
border-top: 5px solid var(--form-element-text-color);
|
||||||
|
vertical-align: middle;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&.fade {
|
||||||
|
border-top: 5px solid var(--datepicker-header-fade-color);
|
||||||
|
}
|
||||||
|
}
|
446
src/app/common/elements/datepicker.tsx
Normal file
446
src/app/common/elements/datepicker.tsx
Normal file
@ -0,0 +1,446 @@
|
|||||||
|
import React, { useState, useEffect, useRef, createRef } from "react";
|
||||||
|
import ReactDOM from "react-dom";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import cs from "classnames";
|
||||||
|
import { Button } from "@/elements";
|
||||||
|
import { If } from "tsx-control-statements/components";
|
||||||
|
|
||||||
|
import "./datepicker.less";
|
||||||
|
|
||||||
|
interface YearRefs {
|
||||||
|
[key: number]: React.RefObject<HTMLDivElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
type DatePickerProps = {
|
||||||
|
selectedDate: Date;
|
||||||
|
onSelectDate: (date: Date) => void;
|
||||||
|
format?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const DatePicker: React.FC<DatePickerProps> = ({ selectedDate, format = "MM/DD/YYYY", onSelectDate }) => {
|
||||||
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const [selDate, setSelDate] = useState(dayjs(selectedDate)); // Initialize with dayjs object
|
||||||
|
const [showYearAccordion, setShowYearAccordion] = useState(false);
|
||||||
|
const [expandedYear, setExpandedYear] = useState<number | null>(selDate.year());
|
||||||
|
const yearRefs = useRef<YearRefs>({});
|
||||||
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||||
|
const modalRef = useRef<HTMLDivElement>(null);
|
||||||
|
const calendarIconRef = useRef(null);
|
||||||
|
const inputRefs = useRef({ YYYY: null, MM: null, DD: null });
|
||||||
|
// Extract delimiter using regex
|
||||||
|
const delimiter = format.replace(/[0-9YMD]/g, "")[0] || "/";
|
||||||
|
// Split format and create state for each part
|
||||||
|
const formatParts = format.split(delimiter);
|
||||||
|
const [dateParts, setDateParts] = useState({
|
||||||
|
YYYY: selDate.format("YYYY"),
|
||||||
|
MM: selDate.format("MM"),
|
||||||
|
DD: selDate.format("DD"),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
inputRefs.current = {
|
||||||
|
YYYY: createRef(),
|
||||||
|
MM: createRef(),
|
||||||
|
DD: createRef(),
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (showYearAccordion && expandedYear && yearRefs.current[expandedYear]) {
|
||||||
|
yearRefs.current[expandedYear].current?.scrollIntoView({
|
||||||
|
block: "nearest",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [showYearAccordion, expandedYear]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener("mousedown", handleClickOutside);
|
||||||
|
return () => document.removeEventListener("mousedown", handleClickOutside);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setDateParts({
|
||||||
|
YYYY: selDate.format("YYYY"),
|
||||||
|
MM: selDate.format("MM"),
|
||||||
|
DD: selDate.format("DD"),
|
||||||
|
});
|
||||||
|
}, [selDate]);
|
||||||
|
|
||||||
|
const handleClickOutside = (event: MouseEvent) => {
|
||||||
|
// Check if the click is on the calendar icon
|
||||||
|
if (calendarIconRef.current && calendarIconRef.current.contains(event.target as Node)) {
|
||||||
|
// Click is on the calendar icon, do nothing
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the click is outside the modal
|
||||||
|
if (modalRef.current && !modalRef.current.contains(event.target as Node)) {
|
||||||
|
setIsOpen(false); // Close the modal
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDayClick = (date: Date) => {
|
||||||
|
const newDate = dayjs(date);
|
||||||
|
setSelDate(newDate); // Update selDate with the new dayjs object
|
||||||
|
onSelectDate && onSelectDate(date); // Call parent's onSelectDate
|
||||||
|
setIsOpen(false); // Close the picker
|
||||||
|
};
|
||||||
|
|
||||||
|
const changeMonth = (delta: number) => {
|
||||||
|
const newDate = selDate.add(delta, "month");
|
||||||
|
setSelDate(newDate);
|
||||||
|
onSelectDate && onSelectDate(newDate.toDate());
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderHeader = () => {
|
||||||
|
return (
|
||||||
|
<div className="day-picker-header">
|
||||||
|
<div
|
||||||
|
className={cs({ fade: showYearAccordion })}
|
||||||
|
onClick={() => {
|
||||||
|
if (!showYearAccordion) {
|
||||||
|
setExpandedYear(selDate.year()); // Set expandedYear when opening accordion
|
||||||
|
}
|
||||||
|
setShowYearAccordion(!showYearAccordion);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selDate.format("MMMM YYYY")}
|
||||||
|
<span className={cs("dropdown-arrow", { fade: showYearAccordion })}></span>
|
||||||
|
</div>
|
||||||
|
<If condition={!showYearAccordion}>
|
||||||
|
<div className="arrows">
|
||||||
|
<Button className="secondary ghost" onClick={() => changeMonth(-1)}>
|
||||||
|
↑
|
||||||
|
</Button>
|
||||||
|
<Button className="secondary ghost" onClick={() => changeMonth(1)}>
|
||||||
|
↓
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</If>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderDayHeaders = () => {
|
||||||
|
const daysOfWeek = ["S", "M", "T", "W", "T", "F", "S"]; // First letter of each day
|
||||||
|
return (
|
||||||
|
<div className="day-header">
|
||||||
|
{daysOfWeek.map((day, i) => (
|
||||||
|
<div key={`${day}-${i}`} className="day-header-cell">
|
||||||
|
{day}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderDays = () => {
|
||||||
|
const days = [];
|
||||||
|
const startDay = selDate.startOf("month");
|
||||||
|
const endDay = selDate.endOf("month");
|
||||||
|
const startDate = startDay.day(); // 0 for Sunday, 1 for Monday, ..., 6 for Saturday
|
||||||
|
|
||||||
|
// Previous month's filler days
|
||||||
|
const previousMonth = startDay.subtract(1, "month");
|
||||||
|
const daysInPreviousMonth = previousMonth.daysInMonth();
|
||||||
|
for (let i = daysInPreviousMonth - startDate + 1; i <= daysInPreviousMonth; i++) {
|
||||||
|
const dayDate = previousMonth.date(i);
|
||||||
|
days.push(
|
||||||
|
<div
|
||||||
|
key={`prev-month-day-${i}`}
|
||||||
|
className="day other-month"
|
||||||
|
onClick={() => handleDayClick(dayDate.toDate())}
|
||||||
|
>
|
||||||
|
{i}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Current month's days
|
||||||
|
for (
|
||||||
|
let dayCount = 1;
|
||||||
|
startDay.add(dayCount - 1, "day").isBefore(endDay) ||
|
||||||
|
startDay.add(dayCount - 1, "day").isSame(endDay, "day");
|
||||||
|
dayCount++
|
||||||
|
) {
|
||||||
|
const currentDate = startDay.add(dayCount - 1, "day");
|
||||||
|
days.push(
|
||||||
|
<div
|
||||||
|
key={dayCount}
|
||||||
|
className={`day ${selDate.isSame(currentDate, "day") ? "selected" : ""}`}
|
||||||
|
onClick={() => handleDayClick(currentDate.toDate())}
|
||||||
|
>
|
||||||
|
{dayCount}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next month's filler days
|
||||||
|
while (days.length < 42) {
|
||||||
|
const fillerDayCount = days.length - daysInPreviousMonth - endDay.date();
|
||||||
|
const dayDate = endDay.add(fillerDayCount + 1, "day");
|
||||||
|
days.push(
|
||||||
|
<div
|
||||||
|
key={`next-month-day-${dayDate.format("YYYY-MM-DD")}`}
|
||||||
|
className="day other-month"
|
||||||
|
onClick={() => handleDayClick(dayDate.toDate())}
|
||||||
|
>
|
||||||
|
{dayDate.date()}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return days;
|
||||||
|
};
|
||||||
|
|
||||||
|
const calculatePosition = (): React.CSSProperties => {
|
||||||
|
if (wrapperRef.current) {
|
||||||
|
const rect = wrapperRef.current.getBoundingClientRect();
|
||||||
|
return {
|
||||||
|
position: "absolute",
|
||||||
|
top: `${rect.bottom + window.scrollY + 2}px`,
|
||||||
|
left: `${rect.left + window.scrollX}px`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
const populateYears = () => {
|
||||||
|
const currentYear = dayjs().year();
|
||||||
|
const startYear = currentYear - 10;
|
||||||
|
const endYear = currentYear + 10;
|
||||||
|
const yearsRange = [];
|
||||||
|
|
||||||
|
for (let year = startYear; year <= endYear; year++) {
|
||||||
|
yearsRange.push(year);
|
||||||
|
yearRefs.current[year] = React.createRef();
|
||||||
|
}
|
||||||
|
|
||||||
|
return yearsRange;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleMonthYearSelect = (month: number, year: number) => {
|
||||||
|
const newDate = dayjs(new Date(year, month - 1));
|
||||||
|
setSelDate(newDate);
|
||||||
|
setShowYearAccordion(false); // Close accordion
|
||||||
|
onSelectDate && onSelectDate(newDate.toDate());
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderYearMonthAccordion = () => {
|
||||||
|
const years = populateYears();
|
||||||
|
const currentYear = selDate.year();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="year-month-accordion-wrapper">
|
||||||
|
<div className="year-month-accordion">
|
||||||
|
{years.map((year) => (
|
||||||
|
<div key={year} ref={yearRefs.current[year]}>
|
||||||
|
<div
|
||||||
|
className="year-header"
|
||||||
|
data-year={year}
|
||||||
|
onClick={() => setExpandedYear(year === expandedYear ? null : year)}
|
||||||
|
>
|
||||||
|
{year}
|
||||||
|
</div>
|
||||||
|
<If condition={expandedYear === year}>
|
||||||
|
<div
|
||||||
|
className={cs("month-container", {
|
||||||
|
expanded: expandedYear === year,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{Array.from({ length: 12 }, (_, i) => i + 1).map((month) => (
|
||||||
|
<div
|
||||||
|
key={month}
|
||||||
|
className={cs("month", {
|
||||||
|
selected: year === currentYear && month === selDate.month() + 1,
|
||||||
|
})}
|
||||||
|
onClick={() => handleMonthYearSelect(month, year)}
|
||||||
|
>
|
||||||
|
{dayjs(new Date(year, month - 1)).format("MMM")}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</If>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleModal = () => {
|
||||||
|
setIsOpen((prevIsOpen) => !prevIsOpen);
|
||||||
|
setShowYearAccordion(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const dayPickerModal = isOpen
|
||||||
|
? ReactDOM.createPortal(
|
||||||
|
<div ref={modalRef} className="day-picker-modal" style={calculatePosition()}>
|
||||||
|
{renderHeader()}
|
||||||
|
{showYearAccordion && renderYearMonthAccordion()}
|
||||||
|
<If condition={!showYearAccordion}>
|
||||||
|
<>
|
||||||
|
{renderDayHeaders()}
|
||||||
|
<div className="day-picker">{renderDays()}</div>
|
||||||
|
</>
|
||||||
|
</If>
|
||||||
|
</div>,
|
||||||
|
document.getElementById("app")!
|
||||||
|
)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
const handleDatePartChange = (part, value) => {
|
||||||
|
const newDateParts = { ...dateParts, [part]: value };
|
||||||
|
setDateParts(newDateParts);
|
||||||
|
|
||||||
|
// Construct a new date from the updated parts
|
||||||
|
const newDate = dayjs(`${newDateParts.YYYY}-${newDateParts.MM}-${newDateParts.DD}`);
|
||||||
|
if (newDate.isValid()) {
|
||||||
|
onSelectDate(newDate.toDate()); // Call onSelectDate with the new date
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleArrowNavigation = (event, currentPart) => {
|
||||||
|
const currentIndex = formatParts.indexOf(currentPart);
|
||||||
|
let targetInput;
|
||||||
|
|
||||||
|
if (event.key === "ArrowLeft" && currentIndex > 0) {
|
||||||
|
targetInput = inputRefs.current[formatParts[currentIndex - 1]].current;
|
||||||
|
} else if (event.key === "ArrowRight" && currentIndex < formatParts.length - 1) {
|
||||||
|
targetInput = inputRefs.current[formatParts[currentIndex + 1]].current;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (targetInput) {
|
||||||
|
targetInput.focus();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleKeyDown = (event, currentPart) => {
|
||||||
|
const key = event.key;
|
||||||
|
|
||||||
|
if (key === "ArrowLeft" || key === "ArrowRight") {
|
||||||
|
// Handle arrow navigation without selecting text
|
||||||
|
handleArrowNavigation(event, currentPart);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === " ") {
|
||||||
|
// Handle spacebar press to toggle the modal
|
||||||
|
toggleModal();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key.match(/[0-9]/)) {
|
||||||
|
// Handle numeric keys
|
||||||
|
event.preventDefault();
|
||||||
|
const maxLength = currentPart === "YYYY" ? 4 : 2;
|
||||||
|
const newValue = event.target.value.length < maxLength ? event.target.value + key : key;
|
||||||
|
let selectionTimeoutId = null;
|
||||||
|
handleDatePartChange(currentPart, newValue);
|
||||||
|
|
||||||
|
// Clear any existing timeout
|
||||||
|
if (selectionTimeoutId !== null) {
|
||||||
|
clearTimeout(selectionTimeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Re-focus and select the input after state update
|
||||||
|
selectionTimeoutId = setTimeout(() => {
|
||||||
|
event.target.focus();
|
||||||
|
event.target.select();
|
||||||
|
}, 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFocus = (event) => {
|
||||||
|
event.target.select();
|
||||||
|
};
|
||||||
|
|
||||||
|
// Prevent use from selecting text in the input
|
||||||
|
const handleMouseDown = (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
handleFocus(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleIconKeyDown = (event) => {
|
||||||
|
if (event.key === "Enter") {
|
||||||
|
toggleModal();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const setInputWidth = (inputRef, value) => {
|
||||||
|
const span = document.createElement("span");
|
||||||
|
document.body.appendChild(span);
|
||||||
|
span.style.font = "inherit";
|
||||||
|
span.style.visibility = "hidden";
|
||||||
|
span.style.position = "absolute";
|
||||||
|
span.textContent = value;
|
||||||
|
const textWidth = span.offsetWidth;
|
||||||
|
document.body.removeChild(span);
|
||||||
|
|
||||||
|
if (inputRef.current) {
|
||||||
|
inputRef.current.style.width = `${textWidth}px`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// This timeout ensures that the effect runs after the DOM updates
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
formatParts.forEach((part) => {
|
||||||
|
const inputRef = inputRefs.current[part];
|
||||||
|
if (inputRef && inputRef.current) {
|
||||||
|
setInputWidth(inputRef, dateParts[part]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
return () => clearTimeout(timeoutId); // Cleanup timeout on unmount
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const renderDatePickerInput = () => {
|
||||||
|
return (
|
||||||
|
<div className="day-picker-input">
|
||||||
|
{formatParts.map((part, index) => {
|
||||||
|
const inputRef = inputRefs.current[part];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<React.Fragment key={part}>
|
||||||
|
{index > 0 && <span>{delimiter}</span>}
|
||||||
|
<input
|
||||||
|
readOnly
|
||||||
|
ref={inputRef}
|
||||||
|
type="text"
|
||||||
|
value={dateParts[part]}
|
||||||
|
onChange={(e) => handleDatePartChange(part, e.target.value)}
|
||||||
|
onKeyDown={(e) => handleKeyDown(e, part)}
|
||||||
|
onMouseDown={handleMouseDown}
|
||||||
|
onFocus={handleFocus}
|
||||||
|
maxLength={part === "YYYY" ? 4 : 2}
|
||||||
|
className="date-input"
|
||||||
|
placeholder={part}
|
||||||
|
/>
|
||||||
|
</React.Fragment>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<i
|
||||||
|
ref={calendarIconRef}
|
||||||
|
className="fa-sharp fa-regular fa-calendar"
|
||||||
|
onClick={toggleModal}
|
||||||
|
onKeyDown={handleIconKeyDown}
|
||||||
|
tabIndex={0} // Makes the icon focusable
|
||||||
|
role="button" // Semantic role for accessibility
|
||||||
|
aria-label="Toggle date picker" // Accessible label for screen readers
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={wrapperRef}>
|
||||||
|
{renderDatePickerInput()}
|
||||||
|
{dayPickerModal}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export { DatePicker };
|
@ -17,3 +17,4 @@ export { TextField } from "./textfield";
|
|||||||
export { Toggle } from "./toggle";
|
export { Toggle } from "./toggle";
|
||||||
export { Tooltip } from "./tooltip";
|
export { Tooltip } from "./tooltip";
|
||||||
export { TabIcon } from "./tabicon";
|
export { TabIcon } from "./tabicon";
|
||||||
|
export { DatePicker } from "./datepicker";
|
||||||
|
@ -14,7 +14,7 @@ import localizedFormat from "dayjs/plugin/localizedFormat";
|
|||||||
import customParseFormat from "dayjs/plugin/customParseFormat";
|
import customParseFormat from "dayjs/plugin/customParseFormat";
|
||||||
import { Line } from "@/app/line/linecomps";
|
import { Line } from "@/app/line/linecomps";
|
||||||
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "@/util/keyutil";
|
import { checkKeyPressed, adaptFromReactOrNativeKeyEvent } from "@/util/keyutil";
|
||||||
import { TextField, Dropdown, Button } from "@/elements";
|
import { TextField, Dropdown, Button, DatePicker } from "@/elements";
|
||||||
|
|
||||||
import { ReactComponent as AngleDownIcon } from "@/assets/icons/history/angle-down.svg";
|
import { ReactComponent as AngleDownIcon } from "@/assets/icons/history/angle-down.svg";
|
||||||
import { ReactComponent as ChevronLeftIcon } from "@/assets/icons/history/chevron-left.svg";
|
import { ReactComponent as ChevronLeftIcon } from "@/assets/icons/history/chevron-left.svg";
|
||||||
@ -286,15 +286,16 @@ class HistoryView extends React.Component<{}, {}> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
handleFromTsChange(e: any): void {
|
handleFromTsChange(date: Date): void {
|
||||||
let hvm = GlobalModel.historyViewModel;
|
let hvm = GlobalModel.historyViewModel;
|
||||||
let newDate = e.target.value;
|
let newDate = dayjs(date).format("YYYY-MM-DD");
|
||||||
let today = dayjs().format("YYYY-MM-DD");
|
let today = dayjs().format("YYYY-MM-DD");
|
||||||
if (newDate == "" || newDate == today) {
|
if (newDate == "" || newDate == today) {
|
||||||
hvm.setFromDate(null);
|
hvm.setFromDate(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
hvm.setFromDate(e.target.value);
|
console.log;
|
||||||
|
hvm.setFromDate(newDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
@boundMethod
|
@boundMethod
|
||||||
@ -472,13 +473,7 @@ class HistoryView extends React.Component<{}, {}> {
|
|||||||
/>
|
/>
|
||||||
<div className="fromts">
|
<div className="fromts">
|
||||||
<div className="fromts-text">From: </div>
|
<div className="fromts-text">From: </div>
|
||||||
<div className="hoverEffect">
|
<DatePicker selectedDate={new Date()} onSelectDate={this.handleFromTsChange} />
|
||||||
<input
|
|
||||||
type="date"
|
|
||||||
onChange={this.handleFromTsChange}
|
|
||||||
value={this.searchFromTsInputValue()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
className="filter-cmds search-checkbox hoverEffect"
|
className="filter-cmds search-checkbox hoverEffect"
|
||||||
|
@ -3825,7 +3825,8 @@ func HistoryViewAllCommand(ctx context.Context, pk *scpacket.FeCommandPacketType
|
|||||||
if pk.Kwargs["fromts"] != "" {
|
if pk.Kwargs["fromts"] != "" {
|
||||||
fromTs, err := resolvePosInt(pk.Kwargs["fromts"], 0)
|
fromTs, err := resolvePosInt(pk.Kwargs["fromts"], 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("invalid fromts (must be unixtime (milliseconds): %v", err)
|
// no error here anymore (otherwise it jams up the frontend, just ignore and set to 0)
|
||||||
|
opts.FromTs = 0
|
||||||
}
|
}
|
||||||
if fromTs > 0 {
|
if fromTs > 0 {
|
||||||
opts.FromTs = int64(fromTs)
|
opts.FromTs = int64(fromTs)
|
||||||
|
Loading…
Reference in New Issue
Block a user