react-native-ui-datepicker
Version: 
Customizable date picker for React Native
626 lines (569 loc) • 15 kB
text/typescript
import dayjs from 'dayjs';
import type {
  DateType,
  CalendarDay,
  CalendarMonth,
  CalendarWeek,
  Numerals,
  CalendarType,
} from './types';
import { clsx, type ClassValue } from 'clsx';
import { twMerge } from 'tailwind-merge';
import { useRef } from 'react';
import { isEqual } from 'lodash';
import { numeralSystems } from './numerals';
export const CALENDAR_FORMAT = 'YYYY-MM-DD HH:mm';
export const DATE_FORMAT = 'YYYY-MM-DD';
export const YEAR_PAGE_SIZE = 12;
export const VALID_JALALI_LOCALES = new Set(['fa', 'en']);
export const JALALI_MONTHS = {
  en: [
    'Farvardin',
    'Ordibehesht',
    'Khordad',
    'Tir',
    'Mordad',
    'Shahrivar',
    'Mehr',
    'Aban',
    'Azar',
    'Dey',
    'Bahman',
    'Esfand',
  ],
  fa: [
    'فروردین',
    'اردیبهشت',
    'خرداد',
    'تیر',
    'مرداد',
    'شهریور',
    'مهر',
    'آبان',
    'آذر',
    'دی',
    'بهمن',
    'اسفند',
  ],
};
export const isValidJalaliLocale = (locale: string): boolean =>
  VALID_JALALI_LOCALES.has(locale);
export const getJalaliMonths = (locale: string) =>
  JALALI_MONTHS[locale as 'fa' | 'en'] || JALALI_MONTHS.en;
export const getMonths = () => dayjs.months();
export const getMonthName = (month: number) => dayjs.months()[month];
/**
 * Get months array
 *
 * @returns months array
 */
export const getMonthsArray = ({
  calendar,
  locale,
}: {
  calendar: CalendarType;
  locale: string;
}): CalendarMonth[] => {
  const monthNames =
    calendar === 'jalali' ? getJalaliMonths(locale) : dayjs.months();
  const monthShortNames =
    calendar === 'jalali' ? getJalaliMonths(locale) : dayjs.monthsShort();
  return monthNames.map((name, index) => ({
    index,
    name: {
      full: name,
      short: monthShortNames[index] || '',
    },
    isSelected: false,
  }));
};
/**
 * Get weekdays
 *
 * @param locale - locale
 * @param firstDayOfWeek - first day of week
 * @param format - format short, min or full
 *
 * @returns weekdays
 */
export const getWeekdays = (
  locale: string,
  firstDayOfWeek: number
): CalendarWeek[] => {
  dayjs.locale(locale);
  const weekdayNames = dayjs.weekdays();
  const weekdayShortNames = dayjs.weekdaysShort();
  const weekdayMinNames = dayjs.weekdaysMin();
  let weekdays: CalendarWeek[] = weekdayNames.map((name, index) => ({
    index,
    name: {
      full: name,
      short: weekdayShortNames[index] || '',
      min: weekdayMinNames[index] || '',
    },
  }));
  if (firstDayOfWeek > 0) {
    weekdays = [
      ...weekdays.slice(firstDayOfWeek, weekdays.length),
      ...weekdays.slice(0, firstDayOfWeek),
    ] as CalendarWeek[];
  }
  return weekdays;
};
export const getFormated = (date: DateType) =>
  dayjs(date).format(CALENDAR_FORMAT);
export const getDateMonth = (date: DateType) => dayjs(date).month();
export const getDateYear = (date: DateType) => dayjs(date).year();
/**
 * Check if two dates are on the same day
 *
 * @param a - date to check
 * @param b - date to check
 *
 * @returns true if dates are on the same day, false otherwise
 */
export function areDatesOnSameDay(a: DateType, b: DateType) {
  if (!a || !b) {
    return false;
  }
  const date_a = dayjs(a).format(DATE_FORMAT);
  const date_b = dayjs(b).format(DATE_FORMAT);
  return date_a === date_b;
}
/**
 * Check if date is between two dates
 *
 * @param date - date to check
 * @param options - options
 *
 * @returns true if date is between two dates, false otherwise
 */
export function isDateBetween(
  date: DateType,
  {
    startDate,
    endDate,
  }: {
    startDate?: DateType;
    endDate?: DateType;
  }
): boolean {
  if (!startDate || !endDate) {
    return false;
  }
  return dayjs(date) <= endDate && dayjs(date) >= startDate;
}
/**
 * Check if date is disabled
 *
 * @param date - date to check
 * @param options - options
 *
 * @returns true if date is disabled, false otherwise
 */
export function isDateDisabled(
  date: dayjs.Dayjs,
  {
    minDate,
    maxDate,
    enabledDates,
    disabledDates,
  }: {
    minDate?: DateType;
    maxDate?: DateType;
    enabledDates?: DateType[] | ((date: DateType) => boolean) | undefined;
    disabledDates?: DateType[] | ((date: DateType) => boolean) | undefined;
  }
): boolean {
  if (minDate && date.isBefore(dayjs(minDate).startOf('day'))) {
    return true;
  }
  if (maxDate && date.isAfter(dayjs(maxDate).endOf('day'))) {
    return true;
  }
  if (enabledDates) {
    if (Array.isArray(enabledDates)) {
      const isEnabled = enabledDates.some((enabledDate) =>
        areDatesOnSameDay(date, enabledDate)
      );
      return !isEnabled;
    } else if (enabledDates instanceof Function) {
      return !enabledDates(date);
    }
  } else if (disabledDates) {
    if (Array.isArray(disabledDates)) {
      const isDisabled = disabledDates.some((disabledDate) =>
        areDatesOnSameDay(date, disabledDate)
      );
      return isDisabled;
    } else if (disabledDates instanceof Function) {
      return disabledDates(date);
    }
  }
  return false;
}
/**
 * Check if year is disabled
 *
 * @param year - year to check
 * @param options - options
 *
 * @returns true if year is disabled, false otherwise
 */
export function isYearDisabled(
  year: number,
  {
    minDate,
    maxDate,
  }: {
    minDate?: DateType;
    maxDate?: DateType;
  }
): boolean {
  if (minDate && year < getDateYear(minDate)) return true;
  if (maxDate && year > getDateYear(maxDate)) return true;
  return false;
}
/**
 * Check if month is disabled
 *
 * @param month - month to check
 * @param date - date to check
 * @param options - options
 *
 * @returns true if month is disabled, false otherwise
 */
export function isMonthDisabled(
  month: number,
  date: DateType,
  {
    minDate,
    maxDate,
  }: {
    minDate?: DateType;
    maxDate?: DateType;
  }
): boolean {
  if (
    minDate &&
    month < getDateMonth(minDate) &&
    getDateYear(date) === getDateYear(minDate)
  )
    return true;
  if (
    maxDate &&
    month > getDateMonth(maxDate) &&
    getDateYear(date) === getDateYear(maxDate)
  )
    return true;
  return false;
}
/**
 * Get formated date
 *
 * @param date - date to get formated date from
 * @param format - format to get formated date from
 *
 * @returns formated date
 */
export const getFormatedDate = (date: DateType, format: string) =>
  dayjs(date).format(format);
/**
 * Get date
 *
 * @param date - date to get
 *
 * @returns date
 */
export const getDate = (date: DateType) => dayjs(date);
/**
 * Get year range
 *
 * @param year - year to get year range from
 *
 * @returns year range
 */
export const getYearRange = (year: number) => {
  const endYear = YEAR_PAGE_SIZE * Math.ceil(year / YEAR_PAGE_SIZE);
  let startYear = endYear === year ? endYear : endYear - YEAR_PAGE_SIZE;
  if (startYear < 0) {
    startYear = 0;
  }
  return Array.from({ length: YEAR_PAGE_SIZE }, (_, i) => startYear + i);
};
/**
 * Get days in month
 *
 * @param date - date to get days in month from
 * @param showOutsideDays - whether to show outside days
 * @param firstDayOfWeek - first day of week, number 0-6, 0 – Sunday, 6 – Saturday
 *
 * @returns days in month
 */
export function getDaysInMonth(
  date: DateType,
  showOutsideDays: boolean | undefined,
  firstDayOfWeek: number
) {
  const daysInCurrentMonth = dayjs(date).daysInMonth();
  const prevMonthDays = dayjs(date).add(-1, 'month').daysInMonth();
  const firstDay = dayjs(date).date(1 - firstDayOfWeek);
  const prevMonthOffset = firstDay.day() % 7;
  const daysInPrevMonth = showOutsideDays ? prevMonthOffset : 0;
  const monthDaysOffset = prevMonthOffset + daysInCurrentMonth;
  const daysInNextMonth = showOutsideDays
    ? monthDaysOffset > 35
      ? 42 - monthDaysOffset
      : 35 - monthDaysOffset
    : 0;
  const fullDaysInMonth =
    daysInPrevMonth + daysInCurrentMonth + daysInNextMonth;
  return {
    prevMonthDays,
    prevMonthOffset,
    daysInCurrentMonth,
    daysInNextMonth,
    fullDaysInMonth,
  };
}
/**
 * Get first day of month
 *
 * @param date - date to get first day of month from
 * @param firstDayOfWeek - first day of week, number 0-6, 0 – Sunday, 6 – Saturday
 *
 * @returns first day of month
 */
export function getFirstDayOfMonth(
  date: DateType,
  firstDayOfWeek: number
): number {
  const d = getDate(date);
  return d.date(1 - firstDayOfWeek).day();
}
/**
 * Get start of day
 *
 * @param date - date to get start of day from
 *
 * @returns start of day
 */
export function getStartOfDay(date: DateType): DateType {
  return dayjs(date).startOf('day');
}
/**
 * Get end of day
 *
 * @param date - date to get end of day from
 *
 * @returns end of day
 */
export function getEndOfDay(date: DateType): DateType {
  return dayjs(date).endOf('day');
}
/**
 * Convert date to unix timestamp
 *
 * @param date - date to convert
 *
 * @returns unix timestamp
 */
export function dateToUnix(date: DateType): number {
  return dayjs(date).unix();
}
/**
 * Remove time from date
 *
 * @param date - date to remove time from
 *
 * @returns date with time removed
 */
export function removeTime(
  date: DateType,
  timeZone: string | undefined
): DateType {
  return date ? dayjs.tz(date, timeZone).startOf('day') : undefined;
}
/**
 * Get detailed date object
 *
 * @param date Get detailed date object
 *
 * @returns parsed date object
 */
export const getParsedDate = (date: DateType) => {
  return {
    year: dayjs(date).year(),
    month: dayjs(date).month(),
    hour: dayjs(date).hour(),
    hour12: parseInt(dayjs(date).format('hh')),
    minute: dayjs(date).minute(),
    period: dayjs(date).format('A'),
  };
};
/**
 * Calculate month days array based on current date
 *
 * @param datetime - The current date that selected
 * @param showOutsideDays
 * @param minDate - min selectable date
 * @param maxDate - max selectable date
 * @param firstDayOfWeek - first day of week, number 0-6, 0 – Sunday, 6 – Saturday
 * @param enabledDates - array of enabled dates, or a function that returns true for a given date (takes precedence over disabledDates)
 * @param disabledDates - array of disabled dates, or a function that returns true for a given date
 * @param prevMonthDays - number of days in the previous month
 * @param prevMonthOffset - number of days to offset the previous month
 * @param daysInCurrentMonth - number of days in the current month
 * @param daysInNextMonth - number of days in the next month
 *
 * @returns days array based on current date
 */
export const getMonthDays = (
  datetime: DateType,
  showOutsideDays: boolean,
  minDate: DateType,
  maxDate: DateType,
  firstDayOfWeek: number,
  enabledDates: DateType[] | ((date: DateType) => boolean) | undefined,
  disabledDates: DateType[] | ((date: DateType) => boolean) | undefined,
  prevMonthDays: number,
  prevMonthOffset: number,
  daysInCurrentMonth: number,
  daysInNextMonth: number,
  numerals: Numerals
): CalendarDay[] => {
  const date = dayjs(datetime);
  const prevDays = showOutsideDays
    ? Array.from({ length: prevMonthOffset }, (_, index) => {
        const number = index + (prevMonthDays - prevMonthOffset + 1);
        const thisDay = date.add(-1, 'month').date(number);
        return generateCalendarDay(
          number,
          thisDay,
          minDate,
          maxDate,
          enabledDates,
          disabledDates,
          false,
          index + 1,
          firstDayOfWeek,
          numerals
        );
      })
    : Array(prevMonthOffset).fill(null);
  const currentDays = Array.from({ length: daysInCurrentMonth }, (_, index) => {
    const day = index + 1;
    const thisDay = date.date(day);
    return generateCalendarDay(
      day,
      thisDay,
      minDate,
      maxDate,
      enabledDates,
      disabledDates,
      true,
      prevMonthOffset + day,
      firstDayOfWeek,
      numerals
    );
  });
  const nextDays = Array.from({ length: daysInNextMonth }, (_, index) => {
    const day = index + 1;
    const thisDay = date.add(1, 'month').date(day);
    return generateCalendarDay(
      day,
      thisDay,
      minDate,
      maxDate,
      enabledDates,
      disabledDates,
      false,
      daysInCurrentMonth + prevMonthOffset + day,
      firstDayOfWeek,
      numerals
    );
  });
  return [...prevDays, ...currentDays, ...nextDays];
};
/**
 * Generate day object for displaying inside day cell
 *
 * @param number - number of day
 * @param date - calculated date based on day, month, and year
 * @param minDate - min selectable date
 * @param maxDate - max selectable date
 * @param enabledDates - array of enabled dates, or a function that returns true for a given date (takes precedence over disabledDates)
 * @param disabledDates - array of disabled dates, or a function that returns true for a given date
 * @param isCurrentMonth - define the day is in the current month
 * @param dayOfMonth - number the day in the current month
 * @param firstDayOfWeek - first day of week, number 0-6, 0 – Sunday, 6 – Saturday
 *
 * @returns days object based on current date
 */
const generateCalendarDay = (
  number: number,
  date: dayjs.Dayjs,
  minDate: DateType,
  maxDate: DateType,
  enabledDates: DateType[] | ((date: DateType) => boolean) | undefined,
  disabledDates: DateType[] | ((date: DateType) => boolean) | undefined,
  isCurrentMonth: boolean,
  dayOfMonth: number,
  firstDayOfWeek: number,
  numerals: Numerals
) => {
  const startOfWeek = dayjs(date).startOf('week').add(firstDayOfWeek, 'day');
  return {
    text: formatNumber(number, numerals),
    number,
    date: date,
    isDisabled: isDateDisabled(date, {
      minDate,
      maxDate,
      enabledDates,
      disabledDates,
    }),
    isCurrentMonth,
    dayOfMonth,
    isStartOfWeek: date.isSame(startOfWeek, 'day'),
    isEndOfWeek: date.day() === (firstDayOfWeek + 6) % 7,
  };
};
export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}
/**
 * Deep compare memo
 *
 * @param value - value to compare
 * @param deps - dependencies
 *
 * @returns memoized value
 */
export function useDeepCompareMemo<T>(value: T, deps: any[]): T {
  const ref = useRef<T>();
  const depsRef = useRef<any[]>();
  if (
    !depsRef.current ||
    !deps.every((dep, i) => isEqual(dep, depsRef.current![i]))
  ) {
    ref.current = value;
    depsRef.current = deps;
  }
  return ref.current as T;
}
function getDigitMap(numerals: Numerals): Record<string, string> {
  const digitMap: Record<string, string> = {};
  const numeralDigits = numeralSystems[numerals];
  for (let i = 0; i < 10; i++) {
    digitMap[i.toString()] = numeralDigits[i]!;
  }
  return digitMap;
}
function replaceDigits(input: string, numerals: Numerals): string {
  const digitMap = getDigitMap(numerals);
  return input.replace(/\d/g, (digit) => digitMap[digit] || digit);
}
export function formatNumber(value: number, numerals: Numerals): string {
  return replaceDigits(value.toString(), numerals);
}