Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Day.js support #1953

Closed
edardev opened this issue Jun 3, 2021 · 14 comments · Fixed by #2264
Closed

Day.js support #1953

edardev opened this issue Jun 3, 2021 · 14 comments · Fixed by #2264

Comments

@edardev
Copy link

edardev commented Jun 3, 2021

#Add day.js support for react apps to minimize bundle size.

@aStanleyLiang
Copy link

For those who would like to adopt dayjs as the localizer, I create a dayjsLocalizer without making a fork

// @ts-ignore
import * as dates from 'react-big-calendar/lib/utils/dates'
import { DateLocalizer } from 'react-big-calendar'
import dayjs from 'dayjs'

let dateRangeFormat = ({ start, end }: any, culture: any, local: any) =>
  local.format(start, 'L', culture) + ' – ' + local.format(end, 'L', culture)

let timeRangeFormat = ({ start, end }: any, culture: any, local: any) =>
  local.format(start, 'LT', culture) + ' – ' + local.format(end, 'LT', culture)

let timeRangeStartFormat = ({ start }: any, culture: any, local: any) =>
  local.format(start, 'LT', culture) + ' – '

let timeRangeEndFormat = ({ end }: any, culture: any, local: any) =>
  ' – ' + local.format(end, 'LT', culture)

let weekRangeFormat = ({ start, end }: any, culture: any, local: any) =>
  local.format(start, 'MMMM DD', culture) +
  ' – ' +
  local.format(end, dates.eq(start, end, 'month') ? 'DD' : 'MMMM DD', culture)

export let formats = {
  dateFormat: 'DD',
  dayFormat: 'DD ddd',
  weekdayFormat: 'ddd',

  selectRangeFormat: timeRangeFormat,
  eventTimeRangeFormat: timeRangeFormat,
  eventTimeRangeStartFormat: timeRangeStartFormat,
  eventTimeRangeEndFormat: timeRangeEndFormat,

  timeGutterFormat: 'LT',

  monthHeaderFormat: 'MMMM YYYY',
  dayHeaderFormat: 'dddd MMM DD',
  dayRangeHeaderFormat: weekRangeFormat,
  agendaHeaderFormat: dateRangeFormat,

  agendaDateFormat: 'ddd MMM DD',
  agendaTimeFormat: 'LT',
  agendaTimeRangeFormat: timeRangeFormat,
}

const dayjsLocalizer = () => {
  let locale = (m: any, c: any) => (c ? m.locale(c) : m)

  return new DateLocalizer({
    formats,
    firstOfWeek(culture) {
      let data = dayjs.localeData()
      return data ? data.firstDayOfWeek() : 0
    },

    format(value, format, culture) {
      return locale(dayjs(value), culture).format(format)
    },
  })
}

import dayjs from 'dayjs'
import localeData from 'dayjs/plugin/localeData'

dayjs.extend(localeData)

const localizer = dayjsLocalizer()

@cutterbl
Copy link
Collaborator

cutterbl commented Oct 6, 2021

@aStanleyLiang The latest version of RBC will be a breaking change for you, as all Date Math has now been added to the localizer, and all localizers are instances of DateLocalizer. The fallback is to use the date-arithmetic methods, but you can override them if needed. Look at the source of the moment localizer or the new luxon https://github.com/jquense/react-big-calendar/blob/master/src/localizers/luxon.js for examples.

@MalikBagwala
Copy link

@cutterbl @aStanleyLiang Is your answer still relevant today? I am trying to implement this in my code and couldn't find good resources on this.

@cutterbl
Copy link
Collaborator

cutterbl commented Nov 9, 2021

@MalikBagwala My answer is still relevant. The Day localizer given by @aStanleyLiang above would not work today, now that all Date math comes directly from the localizer, and his does not implement a DateLocalizer instance.

@MalikBagwala
Copy link

MalikBagwala commented Nov 10, 2021

@cutterbl @aStanleyLiang Okay. Can you suggest the change in his method. I am unable to figure out the required changes.

@dhruvgoel92
Copy link
Contributor

dhruvgoel92 commented Jan 10, 2022

I updated the code to handle latest localizer changes.

@jquense Let me know if this makes sense, happy to send out a PR to merge it in react-big-calendar.

Client code

import dayjs from 'dayjs';
import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
import dayjslocalizer from './dayjslocalizer';
import localeData from 'dayjs/plugin/localeData';
import minMax from 'dayjs/plugin/minMax';
import localizedFormat from 'dayjs/plugin/localizedFormat';
import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
dayjs.locale('en');
dayjs.extend(isSameOrBefore);
dayjs.extend(localeData);
dayjs.extend(minMax);
dayjs.extend(localizedFormat);
dayjs.extend(utc);
dayjs.extend(timezone);


 return (
    <>
      <Calendar localizer={localizer} events={myEvents} />
    </>
  );
/* eslint-disable */
// @ts-nocheck

import { DateLocalizer } from 'react-big-calendar';

let weekRangeFormat = ({ start, end }, culture, local) =>
  local.format(start, 'MMMM DD', culture) +
  ' – ' +
  local.format(end, local.eq(start, end, 'month') ? 'DD' : 'MMMM DD', culture)

  let dateRangeFormat = ({ start, end }, culture, local) =>
  local.format(start, 'L', culture) + ' – ' + local.format(end, 'L', culture)

let timeRangeFormat = ({ start, end }, culture, local) =>
  local.format(start, 'LT', culture) + ' – ' + local.format(end, 'LT', culture)

let timeRangeStartFormat = ({ start }, culture, local) =>
  local.format(start, 'LT', culture) + ' – '

let timeRangeEndFormat = ({ end }, culture, local) =>
  ' – ' + local.format(end, 'LT', culture)

export const formats = {
  dateFormat: 'DD',
  dayFormat: 'DD ddd',
  weekdayFormat: 'ddd',

  selectRangeFormat: timeRangeFormat,
  eventTimeRangeFormat: timeRangeFormat,
  eventTimeRangeStartFormat: timeRangeStartFormat,
  eventTimeRangeEndFormat: timeRangeEndFormat,

  timeGutterFormat: 'LT',

  monthHeaderFormat: 'MMMM YYYY',
  dayHeaderFormat: 'dddd MMM DD',
  dayRangeHeaderFormat: weekRangeFormat,
  agendaHeaderFormat: dateRangeFormat,

  agendaDateFormat: 'ddd MMM DD',
  agendaTimeFormat: 'LT',
  agendaTimeRangeFormat: timeRangeFormat,
}

function fixUnit(unit) {
  let datePart = unit ? unit.toLowerCase() : unit
  if (datePart === 'FullYear') {
    datePart = 'year'
  } else if (!datePart) {
    datePart = undefined
  }
  return datePart
}

export default function(dayjs) {
  const locale = (m, c) => (c ? m.locale(c) : m)

  /*** BEGIN localized date arithmetic methods with dayjs ***/
  function defineComparators(a, b, unit) {
    const datePart = fixUnit(unit)
    const dtA = datePart ? dayjs(a).startOf(datePart) : dayjs(a)
    const dtB = datePart ? dayjs(b).startOf(datePart) : dayjs(b)
    return [dtA, dtB, datePart]
  }

  function startOf(date = null, unit) {
    const datePart = fixUnit(unit)
    if (datePart) {
      return dayjs(date)
        .startOf(datePart)
        .toDate()
    }
    return dayjs(date).toDate()
  }

  function endOf(date = null, unit) {
    const datePart = fixUnit(unit)
    if (datePart) {
      return dayjs(date)
        .endOf(datePart)
        .toDate()
    }
    return dayjs(date).toDate()
  }

  // dayjs comparison operations *always* convert both sides to dayjs objects
  // prior to running the comparisons
  function eq(a, b, unit) {
    const [dtA, dtB, datePart] = defineComparators(a, b, unit)
    return dtA.isSame(dtB, datePart)
  }

  function neq(a, b, unit) {
    return !eq(a, b, unit)
  }

  function gt(a, b, unit) {
    const [dtA, dtB, datePart] = defineComparators(a, b, unit)
    return dtA.isAfter(dtB, datePart)
  }

  function lt(a, b, unit) {
    const [dtA, dtB, datePart] = defineComparators(a, b, unit)
    return dtA.isBefore(dtB, datePart)
  }

  function gte(a, b, unit) {
    const [dtA, dtB, datePart] = defineComparators(a, b, unit)
    return dtA.isSameOrBefore(dtB, datePart)
  }

  function lte(a, b, unit) {
    const [dtA, dtB, datePart] = defineComparators(a, b, unit)
    return dtA.isSameOrBefore(dtB, datePart)
  }

  function inRange(day, min, max, unit = 'day') {
    const datePart = fixUnit(unit)
    const mDay = dayjs(day)
    const mMin = dayjs(min)
    const mMax = dayjs(max)
    return mDay.isBetween(mMin, mMax, datePart, '[]')
  }

  function min(dateA, dateB) {
    const dtA = dayjs(dateA)
    const dtB = dayjs(dateB)
    const minDt = dayjs.min(dtA, dtB)
    return minDt.toDate()
  }

  function max(dateA, dateB) {
    const dtA = dayjs(dateA)
    const dtB = dayjs(dateB)
    const maxDt = dayjs.max(dtA, dtB)
    return maxDt.toDate()
  }

  function merge(date, time) {
    if (!date && !time) return null

    const tm = dayjs(time).format('HH:mm:ss')
    const dt = dayjs(date)
      .startOf('day')
      .format('MM/DD/YYYY')
    // We do it this way to avoid issues when timezone switching
    return dayjs(`${dt} ${tm}`, 'MM/DD/YYYY HH:mm:ss').toDate()
  }

  function add(date, adder, unit) {
    const datePart = fixUnit(unit)
    return dayjs(date)
      .add(adder, datePart)
      .toDate()
  }

  function range(start, end, unit = 'day') {
    const datePart = fixUnit(unit)
    // because the add method will put these in tz, we have to start that way
    let current = dayjs(start).toDate()
    const days = []

    while (lte(current, end)) {
      days.push(current)
      current = add(current, 1, datePart)
    }

    return days
  }

  function ceil(date, unit) {
    const datePart = fixUnit(unit)
    const floor = startOf(date, datePart)

    return eq(floor, date) ? floor : add(floor, 1, datePart)
  }

  function diff(a, b, unit = 'day') {
    const datePart = fixUnit(unit)
    // don't use 'defineComparators' here, as we don't want to mutate the values
    const dtA = dayjs(a)
    const dtB = dayjs(b)
    return dtB.diff(dtA, datePart)
  }

  function minutes(date) {
    const dt = dayjs(date)
    return dt.minutes()
  }

  function firstOfWeek() {
    const data = dayjs.localeData()
    return data ? data.firstDayOfWeek() : 0
  }

  function firstVisibleDay(date) {
    return dayjs(date)
      .startOf('month')
      .startOf('week')
      .toDate()
  }

  function lastVisibleDay(date) {
    return dayjs(date)
      .endOf('month')
      .endOf('week')
      .toDate()
  }

  function visibleDays(date) {
    let current = firstVisibleDay(date)
    const last = lastVisibleDay(date)
    const days = []

    while (lte(current, last)) {
      days.push(current)
      current = add(current, 1, 'd')
    }

    return days
  }
  /*** END localized date arithmetic methods with dayjs ***/

  /**
   * Moved from TimeSlots.js, this method overrides the method of the same name
   * in the localizer.js, using dayjs to construct the js Date
   * @param {Date} dt - date to start with
   * @param {Number} minutesFromMidnight
   * @param {Number} offset
   * @returns {Date}
   */
  function getSlotDate(dt, minutesFromMidnight, offset) {
    return dayjs(dt)
      .startOf('day')
      .minute(minutesFromMidnight + offset)
      .toDate()
  }

  // dayjs will automatically handle DST differences in it's calculations
  function getTotalMin(start, end) {
    return diff(start, end, 'minutes')
  }

  function getMinutesFromMidnight(start) {
    const dayStart = dayjs(start).startOf('day')
    const day = dayjs(start)
    return day.diff(dayStart, 'minutes')
  }

  // These two are used by DateSlotMetrics
  function continuesPrior(start, first) {
    const mStart = dayjs(start)
    const mFirst = dayjs(first)
    return mStart.isBefore(mFirst, 'day')
  }

  function continuesAfter(start, end, last) {
    const mEnd = dayjs(end)
    const mLast = dayjs(last)
    return mEnd.isSameOrAfter(mLast, 'minutes')
  }

  // These two are used by eventLevels
  function sortEvents({
    evtA: { start: aStart, end: aEnd, allDay: aAllDay },
    evtB: { start: bStart, end: bEnd, allDay: bAllDay },
  }) {
    const startSort = +startOf(aStart, 'day') - +startOf(bStart, 'day')

    const durA = diff(aStart, ceil(aEnd, 'day'), 'day')

    const durB = diff(bStart, ceil(bEnd, 'day'), 'day')

    return (
      startSort || // sort by start Day first
      Math.max(durB, 1) - Math.max(durA, 1) || // events spanning multiple days go first
      !!bAllDay - !!aAllDay || // then allDay single day events
      +aStart - +bStart || // then sort by start time *don't need dayjs conversion here
      +aEnd - +bEnd // then sort by end time *don't need dayjs conversion here either
    )
  }

  function inEventRange({
    event: { start, end },
    range: { start: rangeStart, end: rangeEnd },
  }) {
    const startOfDay = dayjs(start).startOf('day')
    const eEnd = dayjs(end)
    const rStart = dayjs(rangeStart)
    const rEnd = dayjs(rangeEnd)

    const startsBeforeEnd = startOfDay.isSameOrBefore(rEnd, 'day')
    // when the event is zero duration we need to handle a bit differently
    const sameMin = !startOfDay.isSame(eEnd, 'minutes')
    const endsAfterStart = sameMin
      ? eEnd.isAfter(rStart, 'minutes')
      : eEnd.isSameOrAfter(rStart, 'minutes')

    return startsBeforeEnd && endsAfterStart
  }

  // dayjs treats 'day' and 'date' equality very different
  // dayjs(date1).isSame(date2, 'day') would test that they were both the same day of the week
  // dayjs(date1).isSame(date2, 'date') would test that they were both the same date of the month of the year
  function isSameDate(date1, date2) {
    const dt = dayjs(date1)
    const dt2 = dayjs(date2)
    return dt.isSame(dt2, 'date')
  }

  /**
   * This method, called once in the localizer constructor, is used by eventLevels
   * 'eventSegments()' to assist in determining the 'span' of the event in the display,
   * specifically when using a timezone that is greater than the browser native timezone.
   * @returns number
   */
  function browserTZOffset() {
    /**
     * Date.prototype.getTimezoneOffset horrifically flips the positive/negative from
     * what you see in it's string, so we have to jump through some hoops to get a value
     * we can actually compare.
     */
    const dt = new Date()
    const neg = /-/.test(dt.toString()) ? '-' : ''
    const dtOffset = dt.getTimezoneOffset()
    const comparator = Number(`${neg}${Math.abs(dtOffset)}`)
    // dayjs correctly provides positive/negative offset, as expected
    const mtOffset = dayjs().utcOffset()
    return mtOffset > comparator ? 1 : 0
  }

  return new DateLocalizer({
    formats,

    firstOfWeek,
    firstVisibleDay,
    lastVisibleDay,
    visibleDays,

    format(value, format, culture) {
      return locale(dayjs(value), culture).format(format)
    },

    lt,
    lte,
    gt,
    gte,
    eq,
    neq,
    merge,
    inRange,
    startOf,
    endOf,
    range,
    add,
    diff,
    ceil,
    min,
    max,
    minutes,

    getSlotDate,
    getTotalMin,
    getMinutesFromMidnight,
    continuesPrior,
    continuesAfter,
    sortEvents,
    inEventRange,
    isSameDate,
    browserTZOffset,
  })
}
/* eslint-enable */

@cutterbl
Copy link
Collaborator

@ dhruvgoel92 Initial quick look it looks pretty good. Prefer to 'disable' eslint on a 'next-line' basis, and only as needed, but that's something else. I think we'll have to create a quick example for side-by-side comparisons, as well as run it through the unit testing (have to manually change the localizers for that testing, currently). This could be a nice addition, although I continually wonder if all of these add-ons wouldn't be better served as separate packages (localizers included).

@dhruvgoel92
Copy link
Contributor

@cutterbl I think I forgot a few dayjs extensions in the client code, updated the code in the original comment as well.

import utc from 'dayjs/plugin/utc';
import timezone from 'dayjs/plugin/timezone';
dayjs.extend(utc);
dayjs.extend(timezone);

@sujin-sreekumaran
Copy link

@aStanleyLiang it worked for me but in the timeRangeFormat() the culture logs undefined, and I can't get the timerangeformat for the calender

@marnixhoh
Copy link
Contributor

Are there any plans to merge the above Day.js localizer any time soon?

Would be great if it gets merged! 🚀

@cutterbl
Copy link
Collaborator

@marnixhoh We would welcome a PR that follows our Contribution guidelines.

@marnixhoh
Copy link
Contributor

@cutterbl I would love to drop a PR for this! But it seems @dhruvgoel92 has already done all the heavy lifting in his comment above. @dhruvgoel92 Are you interested to turn your code into a PR or is it ok for me to base a PR of of your code?

@dhruvgoel92
Copy link
Contributor

@marnixhoh feel free to go ahead and convert it into a PR

@marnixhoh
Copy link
Contributor

Just wanted to give a status update that I'm almost done with the PR. I expect to create it today or tomorrow

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging a pull request may close this issue.

7 participants