基于悬停选择范围来突出显示 PrimeReact 日历中的日期

问题描述 投票:0回答:1

我正在使用 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);
  }
}

我尝试过的:

  • 我已经实现了handleMouseOver函数来更新突出显示的范围。
  • 我利用 getDatesBetween 来计算要突出显示的日期。
  • 我确保鼠标悬停的事件侦听器附加到日期单元格。

当前行为 目前,当我将鼠标悬停在第一个选定日期之后的日期上时,预期范围会正确突出显示,但导致闪烁问题可能是由于重新渲染所致。

预期行为 我希望当我悬停任何日期时,范围内的日期应该突出显示,而不会闪烁,并且不会导致任何不必要的重新渲染。

我的实现中可能缺少什么,导致跨年的日期无法正确突出显示?任何有关如何解决此行为的建议或见解将不胜感激!

reactjs typescript calendar primereact
1个回答
0
投票

我开发了一段代码,在某种程度上满足了你想要的功能。如果需要,您可以在此处查看并应用代码。当您根据需要更改它并应用它时,它就会起作用。这里我展示了如何突出显示第一个日期之后达到的范围内的值。您可以将其添加到 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 = ''
                  );
                }
              },
            },
          }}
© www.soinside.com 2019 - 2024. All rights reserved.