datepicker icon indicating copy to clipboard operation
datepicker copied to clipboard

independent multiple calendars option

Open JHSeo-git opened this issue 2 years ago • 11 comments

First, I'd like to say that I'm enjoying using the library. 😄

I have a small question about additional functionality for range mode.

There is currently no method to select a date through multiple independent calendars when using range mode. We can use offset to show multiple calendars, but they are sequential and not independent. (like will-be-in-range state when hover, days when move prev month / next month, ...)

In order to use independent calendars in RANGE mode, I have personally implemented a few methods in my limited skills.

If you could provide this functionality in the library, I think it could be applied in many cases.

Once again, thank you for creating such an awesome library. 👍

Here is the sample code and application I did.

  • sample: https://practice-storybook-vite.pages.dev/?path=/story/components-datepicker--range-template
  • code: https://github.com/JHSeo-git/practice-storybook-vite/blob/8c9528b671a2b0b35d7ee0f95fba0c601372007e/src/components/DatePicker/DatePicker.Range.tsx#L69

JHSeo-git avatar Feb 18 '23 12:02 JHSeo-git

@JHSeo-git, thanks for reaching out with this proposal. Your examples look great, firstly I haven't realised that this is my lib :) I will think about how to make it happen, but at the same time it could be weird when you select 2 same months and try to select something :)

Feshchenko avatar Feb 18 '23 19:02 Feshchenko

@Feshchenko Thank you for your kind comment.

You're right, it can look weird when selecting for the same month. So I looked up some references. here is the one of them I liked.

  • https://rsuitejs.com/components/date-range-picker/#basic

The idea here is to remove the choice of the same month appearing in 2 calendars from the options.

The maxMonth of the start-range calendar is always one month less than the end-range calendar, MinMonth for the end-range calendar is always one month greater than the start-range calendar.

I think this is something you might want to consider, so here's a little suggestion! Thank you for your interest in this.

JHSeo-git avatar Feb 19 '23 02:02 JHSeo-git

@JHSeo-git thanks for the reference. It is easy to understand how to do this with 2 calendars. But what if you have 3 or 5 🤪 It is nice thing to think of in the future :)

Feshchenko avatar Feb 19 '23 07:02 Feshchenko

@Feshchenko Oh that's right, I hadn't thought about that a bit more. 🤔 Thanks for the kind words, even though it's a weak idea. :)

JHSeo-git avatar Feb 21 '23 13:02 JHSeo-git

@JHSeo-git I want create a date picker same as your. After working on it for several days, I was able to develop a date picker similar to what you wanted. However, I had to implement several workarounds to achieve the desired functionality. In order to make it work smoothly and better clean code, I ended up forking the repository and adding an offsetDay parameter to the configuration. Here is a demo of my date picker:

Tab-1689058010605.webm

This is my changes of create-initial-state.ts in forked repo:

  const {
    selectedDates,
    offsetDate,
    focusDate,
    dates: { minDate, maxDate },
    years,
  } = config;

  let initialOffsetDate = undefined;

  if (offsetDate) {
    initialOffsetDate = getCalendarStartDate(
      minDate,
      maxDate,
      getCleanDate(offsetDate),
    );
  } else {
    initialOffsetDate =
      selectedDates.length > 0
        ? selectedDates[selectedDates.length - 1]
        : getCalendarStartDate(minDate, maxDate, getCleanDate(newDate()));
  }

  return {
    focusDate,
    rangeEnd: null,
    offsetDate: initialOffsetDate,
    offsetYear: getCurrentYearPosition(
      getDateParts(initialOffsetDate).Y,
      years,
    ),
  };
};

huongdevvn avatar Jul 11 '23 06:07 huongdevvn

@huongdevvn Wow That looks good! appreciate your comment. 😄

Similarly, I created a custom hook for a similar functionality It's not optimized code, but it works well for cases, so that's what I use for now.

Here's that code.

/* -------------------------------------------------------------------------------------------------
 * useRangeCalendarOffset
 * -----------------------------------------------------------------------------------------------*/
type RangeType = 'start' | 'end';
interface UseRangeCalendarOffsetProps {
  startDpState: DPState;
  endDpState: DPState;
  range: RangeType;
}
function useRangeCalendarOffset({ startDpState, endDpState, range }: UseRangeCalendarOffsetProps) {
  const startCalendars = useCalendars(startDpState);
  const endCalendars = useCalendars(endDpState);
  const startCalendar = startCalendars.calendars[0];
  const endCalendar = endCalendars.calendars[0];

  const startRangeCalendar = React.useMemo(
    () => ({
      year: parseInt(getNumericText(startCalendar.year)),
      month: parseInt(getNumericText(startCalendar.month)),
    }),
    [startCalendar]
  );
  const endRangeCalendar = React.useMemo(
    () => ({
      year: parseInt(getNumericText(endCalendar.year)),
      month: parseInt(getNumericText(endCalendar.month)),
    }),
    [endCalendar]
  );

  const { dispatch: startDpDispatch } = startDpState;
  const { dispatch: endDpDispatch } = endDpState;

  const onCalculateDayOffset = React.useCallback(() => {
    const startDate = new Date(startRangeCalendar.year, startRangeCalendar.month - 1, 1);
    const endDate = new Date(endRangeCalendar.year, endRangeCalendar.month - 1, 1);

    if (range === 'start') {
      const nextDate = addMonths(startDate, 1);
      if (endDate <= nextDate) {
        endDpDispatch({
          type: 'SET_OFFSET_DATE',
          date: addMonths(nextDate, 1),
        });

        return;
      }
    }

    if (range === 'end') {
      const nextDate = subMonths(endDate, 1);
      if (startDate >= nextDate) {
        startDpDispatch({
          type: 'SET_OFFSET_DATE',
          date: subMonths(nextDate, 1),
        });

        return;
      }
    }
  }, [startRangeCalendar, endRangeCalendar, startDpDispatch, endDpDispatch, range]);

  const onCalculateMonthOffset = React.useCallback(
    (m: CalendarMonth) => {
      const startDate = new Date(startRangeCalendar.year, startRangeCalendar.month - 1, 1);
      const endDate = new Date(endRangeCalendar.year, endRangeCalendar.month - 1, 1);

      if (range === 'start') {
        const nextDate = m.$date;
        if (endDate <= nextDate) {
          endDpDispatch({
            type: 'SET_OFFSET_DATE',
            date: addMonths(nextDate, 1),
          });

          return;
        }
      }

      if (range === 'end') {
        const nextDate = m.$date;
        if (startDate >= nextDate) {
          startDpDispatch({
            type: 'SET_OFFSET_DATE',
            date: subMonths(nextDate, 1),
          });

          return;
        }
      }
    },
    [startRangeCalendar, endRangeCalendar, startDpDispatch, endDpDispatch, range]
  );
  const onCalculateYearOffset = React.useCallback(
    (y: CalendarYear) => {
      const startDate = new Date(startRangeCalendar.year, startRangeCalendar.month - 1, 1);
      const endDate = new Date(endRangeCalendar.year, endRangeCalendar.month - 1, 1);

      if (range === 'start') {
        const nextDate = y.$date;
        if (endDate <= nextDate) {
          endDpDispatch({
            type: 'SET_OFFSET_DATE',
            date: addMonths(nextDate, 1),
          });

          return;
        }
      }

      if (range === 'end') {
        const nextDate = y.$date;
        if (startDate >= nextDate) {
          startDpDispatch({
            type: 'SET_OFFSET_DATE',
            date: subMonths(nextDate, 1),
          });

          return;
        }
      }
    },
    [startRangeCalendar, endRangeCalendar, startDpDispatch, endDpDispatch, range]
  );

  return { onCalculateDayOffset, onCalculateMonthOffset, onCalculateYearOffset };
}

JHSeo-git avatar Jul 12 '23 04:07 JHSeo-git

Any update here @Feshchenko ?

aelfannir avatar Sep 06 '23 12:09 aelfannir

Hi @JHSeo-git are you able to provide an example of how you would actually use this hook. Thanks!

simontong avatar Jan 23 '24 02:01 simontong

Hi @JHSeo-git are you able to provide an example of how you would actually use this hook. Thanks!

@simontong Where I used that hook was to naturally change the start and end dates like a rsuit datepicker when an event occurs in day, month, and year mode, so here's what I used (not exactly the same, but closely)

interface CalendarRangeProps {
  startDpState: DPState;
  endDpState: DPState;
  range: 'start' | 'end';
}
const CalendarRange = ({ startDpState, endDpState, range }: CalendarRangeProps) => {
  const targetDpState = range === 'start' ? startDpState : endDpState;
  const [calendarViewMode, setCalendarViewMode] = useCalendarViewMode();
  const { onCalculateDayOffset, onCalculateMonthOffset, onCalculateYearOffset } = 
    userRangeCalendarOffset({ startDpState, endDpState, range }); 

  React.useLayoutEffect(() => {
    const startDate = startDpState.selectedDates[0];

    if (startDate) {
      startDpState.dispatch({ type: 'SET_OFFSET_DATE', date: startDate });
    }

    if (!endDate) {
      endDpState.dispatch({ type: 'SET_OFFSET_DATE', date: addMonths(startDate ?? today, 1) });
    }

    if (startDate?.getMonth() <= endDate?.getMonth()) {
      endDpState.dispatch({ type: 'SET_OFFSET_DATE', date: addMonths(startDate ?? today, 1) });
    }

    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, []);

  return (
    <div>
      <CalendarHeader
        dpState={range === 'start' ? startDpState : endDpState}
        calendarViewMode={calendarViewMode}
        setCalendarViewMode={setCalendarViewMode}
        onPrevClick={range === 'end' ? onCalculateDayOffset : undefined}
        onNextClick={range === 'start' ? onCalculateDayOffset : undefined}
      />
      <div className="mt-2">
        {calendarViewMode === 'days' && (
          <CalendarRangeDays startDpState={startDpState} endDpState={endDpState} range={range} />
        )}
        {calendarViewMode === 'months' && (
          <CalendarMonths 
            dpState={targetDpState} 
            setCalendarViewMode={setCalendarViewMode} 
            onMonthClick={onCalculateMonthOffset} 
          />
        )}
        {calendarViewMode === 'years' && (
          <CalendarYears 
            dpState={targetDpState} 
            setCalendarViewMode={setCalendarViewMode} 
            onYearClick={onCalculateYearOffset} 
          />
        )}
      </div>
    </div>
  );
};

JHSeo-git avatar Jan 29 '24 01:01 JHSeo-git

Is it possible to provide a more complete example that shows how the onCalculateDayOffset, onCalculateMonthOffset, onCalculateYearOffset are used, I was unable to get this to work. Thanks

simontong avatar Jan 31 '24 02:01 simontong

Is it possible to provide a more complete example that shows how the onCalculateDayOffset, onCalculateMonthOffset, onCalculateYearOffset are used, I was unable to get this to work. Thanks

simontong avatar Jan 31 '24 02:01 simontong