我正在使用 PrimeReact(版本 10.8.3)开发 React TypeScript 应用程序,特别是具有范围选择模式的日历组件。我为日历创建了一个单独的组件,并对其进行了自定义以实现一项功能,该功能突出显示第一个选定日期和悬停日期之间的所有日期,甚至跨不同年份,例如:
我选择的第一个日期是 2023 年 12 月 1 日,然后如果用户将鼠标悬停在该日期之后的任何日期,甚至不同年份的日期(例如 2024 年),它应该突出显示从 12 月 2 日到悬停的那一天的所有日期。
像这样,在我的例子中唯一不同的是它一次显示一个月: 示例图片
这是我的 React tsx 代码:
// calendar-component.tsx
import React, { useEffect, useRef, useState } from "react";
import { Calendar } from "primereact/calendar";
import "./calendar-component.scss";
import moment from "moment";
import { getDatesBetween, getMonthsYears } from "../../../utils/sharedUtils";
interface ICalendarProps {
handleDateFilter: (dates: Date[]) => void;
maxDate?: Date;
dateFormat?: string;
}
function CalendarComponent({
handleDateFilter,
maxDate,
dateFormat = "mm/dd/yy",
}: ICalendarProps) {
const [dates, setDates] = useState([new Date(), new Date()]);
const calendarRef = useRef<any>(null);
const calendarInputRef = useRef<any>(null);
const [isOpen, setIsOpen] = useState<boolean>(false);
const [viewDate, setViewDate] = useState(new Date());
const [rangeDates, setRangeDates] = useState<any[]>([]);
// const [dayCells, setDayCells] = useState<Element[]>([]);
useEffect(() => {
if (dates?.[0] && dates?.[1]) {
handleDateFilter(dates);
}
setDynamicWidth();
}, [dates]);
const handleCalendarOpen = () => {
calendarRef.current?.hide();
};
useEffect(() => {
const tableScroller = document.querySelector(".p-datatable-wrapper");
window.addEventListener("scroll", handleCalendarOpen);
tableScroller?.addEventListener("scroll", handleCalendarOpen);
return () => {
window.removeEventListener("scroll", handleCalendarOpen);
tableScroller?.removeEventListener("scroll", handleCalendarOpen);
};
}, []);
const handleMouseOver = (e: any) => {
const { month, year } = getMonthsYears(new Date(viewDate));
const day = Number(e.target.textContent);
const hoveredDate = new Date(year, month, day);
if (
(dates[0] && hoveredDate?.getTime() < dates[0]?.getTime()) ||
(dates[1] && hoveredDate?.getTime() > dates[1]?.getTime())
) {
return;
}
setRangeDates((prevList) => [
...getDatesBetween(new Date(dates[0]), hoveredDate)?.map((val) =>
val.toString()
),
]);
};
// useEffect(() => {
// if (dayCells.length) {
//
// dayCells.forEach((cell) => {
// cell.addEventListener("mouseover", handleMouseOver);
// return () => {
// cell.removeEventListener("mouseover", handleMouseOver);
// };
// });
// }
// // eslint-disable-next-line react-hooks/exhaustive-deps
// }, [dayCells]);
useEffect(() => {
if (viewDate && isOpen) {
const dayCellsTimeout = setTimeout(() => {
const dayCells = document.querySelectorAll(".custom-day-label");
dayCells.forEach((cell) => {
const isDisabled = cell.getAttribute("data-p-disabled");
if (isDisabled === "false") {
cell.addEventListener("mouseover", handleMouseOver);
return () => {
cell.removeEventListener("mouseover", handleMouseOver);
};
}
});
}, 0);
return () => {
clearTimeout(dayCellsTimeout);
const dayCells = document.querySelectorAll(".custom-day-label");
dayCells.forEach((cell) => {
cell.removeEventListener("mouseover", handleMouseOver);
});
};
}
}, [viewDate, isOpen, dates, rangeDates]); // flickering causing due to rangeDates dependency but removing it stops it from listening to the mouseover as reference to the current dayCells changes
useEffect(() => {
if (dates[0] && rangeDates?.length > 0) {
const { month, year } = getMonthsYears(new Date(viewDate));
const highlightTimeout = setTimeout(() => {
const dayCells = document.querySelectorAll(".custom-day-label");
dayCells.forEach((cell: any, cellIndex: number) => {
const cellDate = new Date(year, month, cell.textContent);
const isHiglighted = cell.getAttribute("data-p-highlight");
const isDisabled = cell.getAttribute("data-p-disabled");
if (
dates[0].toString() !== cellDate.toString() &&
(!dates[1] || dates[1].toString() !== cellDate.toString()) &&
isHiglighted === "false" &&
isDisabled === "false"
) {
if (rangeDates.includes(cellDate.toString())) {
cell.style.backgroundColor = "#e3e9f7";
} else {
cell.style.backgroundColor = "white";
}
}
});
}, 0);
return () => {
clearTimeout(highlightTimeout);
};
}
}, [dates, rangeDates, viewDate]);
const setDynamicWidth = () => {
const hiddenSpan = document.createElement("span");
hiddenSpan.className = "hiddenSpan";
hiddenSpan.textContent = getDisplayValue().toString();
hiddenSpan.style.visibility = "hidden";
document.body.appendChild(hiddenSpan);
if (hiddenSpan && calendarInputRef.current) {
hiddenSpan.textContent = getDisplayValue().toString();
calendarInputRef.current.style.width =
hiddenSpan.getClientRects()[0].width + "px";
document.body.removeChild(hiddenSpan);
}
};
const getDisplayValue = () => {
return dates[0] && dates[1] && dates[0].getTime() === dates[1].getTime()
? moment(dates[0]).format("DD MMM YYYY")
: `${dates[0] ? moment(dates[0]).format("DD MMM YYYY") : ""} - ${
dates[1] ? moment(dates[1]).format("DD MMM YYYY") : ""
}`;
};
return (
<div className="calendar-wrapper">
<Calendar
panelClassName="date-filter-panel"
value={dates}
onChange={(e: any) => setDates(e.value)}
selectionMode="range"
readOnlyInput
maxDate={maxDate || new Date()}
showIcon
iconPos="left"
dateFormat={dateFormat}
ref={calendarRef}
inputRef={calendarInputRef}
viewDate={viewDate}
onShow={() => setIsOpen(true)}
onHide={() => {
setIsOpen(false);
setRangeDates([]);
}}
onViewDateChange={(e) => {
setViewDate(e.value);
}}
pt={{
dayLabel: {
className: "custom-day-label",
},
}}
/>
<div
className="display-value"
style={{ width: calendarInputRef?.current?.style?.width + "px" }}
>
{getDisplayValue()}
</div>
</div>
);
}
export default CalendarComponent;
// sharedUtils.ts:
export const getDatesBetween = (startDate: Date, endDate: Date) => {
const dates: any[] = [];
const currentDate = new Date(startDate);
// Ensure that startDate is before endDate
if (startDate > endDate) return dates;
while (currentDate <= endDate) {
dates.push(new Date(currentDate));
currentDate.setDate(currentDate.getDate() + 1); // Increment the date
}
return dates;
};
export const getMonthsYears = (date: Date) => {
const year = date.getFullYear();
const month = date.getMonth();
return { year, month };
};
// calendar-component.scss
@import "../../../styles/variable.scss";
.calendar-wrapper {
width: 100%;
max-width: fit-content;
.p-calendar {
border-radius: 4px;
border: 1px solid #d0d5dd;
box-shadow: 0px 1px 2px 0px #1018280d;
position: absolute;
width: 100%;
max-width: fit-content;
background: white;
.p-button {
background-color: $whiteColor !important;
color: $blackColor;
border: none !important;
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
.p-inputtext {
border: none !important;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
font-size: 14px;
font-weight: 600;
padding-left: 0;
z-index: 9;
opacity: 0;
}
}
.display-value {
align-items: center;
display: flex;
position: relative;
left: 38px;
height: 40px;
background: white;
width: 100%;
font-size: 14px;
font-weight: 600;
border-top: 1px solid #d0d5dd;
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
.p-calendar:not(.p-calendar-disabled).p-focus > .p-inputtext {
box-shadow: none !important;
}
}
.date-filter-panel {
margin-left: -38px;
margin-top: 1.5px;
width: 310px !important;
.p-datepicker-group {
.p-datepicker-header {
.p-datepicker-title {
font-size: 15px;
.p-datepicker-month {
font-family: "Inter-Medium";
padding-right: 6px;
letter-spacing: 0.5px;
&:enabled:hover {
color: #4b5563;
}
}
.p-datepicker-year {
padding-left: 0;
font-family: "Inter-Medium";
letter-spacing: 0.5px;
&:enabled:hover {
color: #4b5563;
}
}
}
}
.p-datepicker-calendar-container {
.p-datepicker-calendar {
font-size: 13px;
thead {
tr {
font-weight: 500;
color: #7e818c;
}
}
tbody {
tr {
td {
font-family: "Inter-Medium";
font-weight: 500;
padding: 7px;
span {
width: 28px;
height: 28px;
}
span.p-highlight {
background-color: $primaryColor;
color: $whiteColor;
}
}
}
}
}
}
}
}
.date-filter-panel
.p-datepicker-group
.p-datepicker-calendar-container
.p-datepicker-calendar
tbody
tr
td {
font-weight: 500;
padding: 3px !important;
}
.date-filter-panel {
max-width: 260px !important;
table td > span:focus {
box-shadow: none;
}
.p-datepicker-buttonbar {
padding: 8px 8px;
position: absolute;
width: calc(100% - 19px);
top: 48px;
left: 9.5px;
border: none;
gap: 10px;
}
.p-monthpicker-month {
font-size: 14px;
}
.p-yearpicker {
padding-top: 10px;
}
.p-yearpicker-year {
font-size: 14px;
}
.p-datepicker-header {
border: none;
padding-top: 0;
}
.calendar-button {
color: #737986;
border-radius: 5px;
border: 1px solid #d5d9eb;
background-color: #f8f9fc;
padding: 4px 16px;
width: 124px;
font-size: 13px;
span {
font-family: "Inter-Medium";
font-weight: 500;
}
}
.tommorow-button::before {
content: "Tommorrow";
font-family: "Inter-Medium";
}
.tommorow-button {
span {
display: none;
}
}
.selected-button {
border: 1px solid var(--color-primary);
color: var(--color-primary);
}
}
我尝试过的:
当前行为 目前,当我将鼠标悬停在第一个选定日期之后的日期上时,预期范围会正确突出显示,但导致闪烁问题可能是由于重新渲染所致。
预期行为 我希望当我悬停任何日期时,范围内的日期应该突出显示,而不会闪烁,并且不会导致任何不必要的重新渲染。
我的实现中可能缺少什么,导致跨年的日期无法正确突出显示?任何有关如何解决此行为的建议或见解将不胜感激!
我开发了一段代码,在某种程度上满足了你想要的功能。如果需要,您可以在此处查看并应用代码。当您根据需要更改它并应用它时,它就会起作用。这里我展示了如何突出显示第一个日期之后达到的范围内的值。您可以将其添加到 Primereact Calendar 系列组件中并尝试一下。它可能无法完全按照您想要的方式工作,但您可以通过更新代码来修复它。
pt={{
day: {
onMouseOver: (e: any) => {
const dayToHighlight: any = [];
if (dates?.[0] && !dates?.[1]) {
if (
e.relatedTarget['innerText'] >= dates[0].getDay()
) {
for (
let i = dates[0].getDay();
i <= e.relatedTarget['innerText'];
i++
)
dayToHighlight.push(i);
}
const elements = document.querySelectorAll(
'.p-datepicker-calendar tbody tr td span'
);
elements.forEach((element) =>
dayToHighlight.includes(+element.innerHTML)
? (element.className = 'p-highlight')
: element.className = ''
);
}
},
},
}}