From a8595cd81f661c582ea96182f348ef84fc7094f5 Mon Sep 17 00:00:00 2001 From: Marnix Heuker of Hoek <47228669+marnixhoh@users.noreply.github.com> Date: Wed, 24 Aug 2022 20:20:47 +0200 Subject: [PATCH 01/15] docs: fixed instructions to run examples locally --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e3ec3533e1..7f20a6bf18 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,13 @@ element has a height, or the calendar won't be visible. To provide your own cust ## Run examples locally +Note: node >= 16 is required to run the storybook + ```sh $ git clone git@github.com:jquense/react-big-calendar.git $ cd react-big-calendar $ yarn -$ yarn examples +$ yarn storybook ``` - Open [localhost:3000/examples/index.html](http://localhost:3000/examples/index.html). From 7799567450adb0a254fd6ba3357baf9b8cea0017 Mon Sep 17 00:00:00 2001 From: Marnix Heuker of Hoek <47228669+marnixhoh@users.noreply.github.com> Date: Wed, 24 Aug 2022 20:22:12 +0200 Subject: [PATCH 02/15] fix: luxon story now uses luxon instead of moment --- stories/demos/luxon.stories.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/stories/demos/luxon.stories.js b/stories/demos/luxon.stories.js index 32178738fd..7fa3604324 100644 --- a/stories/demos/luxon.stories.js +++ b/stories/demos/luxon.stories.js @@ -1,6 +1,6 @@ import React from 'react' -import moment from 'moment' -import { Calendar, momentLocalizer } from '../../src' +import { DateTime } from 'luxon' +import { Calendar, luxonLocalizer } from '../../src' import Luxon from './exampleCode/luxon' export default { @@ -13,7 +13,7 @@ export default { }, } -const localizer = momentLocalizer(moment) +const localizer = luxonLocalizer(DateTime) export function LuxonLocalizer() { return From 5ebe9dc34ba001b0f0e21ce4dfee13d34bef27ea Mon Sep 17 00:00:00 2001 From: Marnix Heuker of Hoek <47228669+marnixhoh@users.noreply.github.com> Date: Wed, 24 Aug 2022 20:27:55 +0200 Subject: [PATCH 03/15] fix(moment localizer): removed comments about 'day' and 'date' in the call to isSame() --- src/localizers/moment.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/localizers/moment.js b/src/localizers/moment.js index a63b0451fb..7421d28d37 100644 --- a/src/localizers/moment.js +++ b/src/localizers/moment.js @@ -55,9 +55,7 @@ export default function (moment) { function getTimezoneOffset(date) { // ensures this gets cast to timezone - return moment(date) - .toDate() - .getTimezoneOffset() + return moment(date).toDate().getTimezoneOffset() } function getDstOffset(start, end) { @@ -314,13 +312,10 @@ export default function (moment) { return startsBeforeEnd && endsAfterStart } - // moment treats 'day' and 'date' equality very different - // moment(date1).isSame(date2, 'day') would test that they were both the same day of the week - // moment(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 = moment(date1) const dt2 = moment(date2) - return dt.isSame(dt2, 'date') + return dt.isSame(dt2, 'day') } /** From cf065fcd7be71b52bf727b6e27423302781c0bfc Mon Sep 17 00:00:00 2001 From: Marnix Heuker of Hoek <47228669+marnixhoh@users.noreply.github.com> Date: Wed, 24 Aug 2022 20:29:13 +0200 Subject: [PATCH 04/15] feat: created dayjs localizer --- README.md | 41 ++- package.json | 1 + src/index.js | 1 + src/localizers/dayjs.js | 386 +++++++++++++++++++++++++++++ stories/demos/dayjs.stories.js | 20 ++ stories/demos/exampleCode/dayjs.js | 67 +++++ yarn.lock | 5 + 7 files changed, 520 insertions(+), 1 deletion(-) create mode 100644 src/localizers/dayjs.js create mode 100644 stories/demos/dayjs.stories.js create mode 100644 stories/demos/exampleCode/dayjs.js diff --git a/README.md b/README.md index 7f20a6bf18..e762209c12 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ $ yarn storybook ### Localization and Date Formatting `react-big-calendar` includes three options for handling the date formatting and culture localization, depending -on your preference of DateTime libraries. You can use either the [Moment.js](https://momentjs.com/), [Globalize.js](https://github.com/jquery/globalize) or [date-fns](https://date-fns.org/) localizers. +on your preference of DateTime libraries. You can use either the [Moment.js](https://momentjs.com/), [Globalize.js](https://github.com/jquery/globalize), [date-fns](https://date-fns.org/), [Day.js](https://day.js.org) localizers. Regardless of your choice, you **must** choose a localizer to use this library: @@ -120,6 +120,45 @@ const MyCalendar = (props) => ( ) ``` +```js +import { Calendar, dayjsLocalizer } from 'react-big-calendar' +import dayjs from 'dayjs' + +// import necessary dayjs plugins +import isBetween from 'dayjs/plugin/isBetween' +import isSameOrAfter from 'dayjs/plugin/isSameOrAfter' +import isSameOrBefore from 'dayjs/plugin/isSameOrBefore' +import localeData from 'dayjs/plugin/localeData' +import localizedFormat from 'dayjs/plugin/localizedFormat' +import minMax from 'dayjs/plugin/minMax' +import timezone from 'dayjs/plugin/timezone' +import utc from 'dayjs/plugin/utc' + +// load necessary dayjs plugins +dayjs.extend(isBetween) +dayjs.extend(isSameOrAfter) +dayjs.extend(isSameOrBefore) +dayjs.extend(localeData) +dayjs.extend(localizedFormat) +dayjs.extend(minMax) +dayjs.extend(timezone) +dayjs.extend(utc) + +const localizer = dayjsLocalizer(dayjs) + +const MyCalendar = (props) => ( +
+ +
+) +``` + ## Custom Styling Out of the box, you can include the compiled CSS files and be up and running. But, sometimes, you may want to style diff --git a/package.json b/package.json index a399c8a0f2..1dccbe4859 100644 --- a/package.json +++ b/package.json @@ -107,6 +107,7 @@ "@babel/runtime": "^7.18.6", "clsx": "^1.2.1", "date-arithmetic": "^4.1.0", + "dayjs": "^1.11.5", "dom-helpers": "^5.2.1", "globalize": "^0.1.1", "invariant": "^2.2.4", diff --git a/src/index.js b/src/index.js index afa51c186a..85034943dd 100644 --- a/src/index.js +++ b/src/index.js @@ -13,5 +13,6 @@ export { default as momentLocalizer } from './localizers/moment' export { default as luxonLocalizer } from './localizers/luxon' export { default as globalizeLocalizer } from './localizers/globalize' export { default as dateFnsLocalizer } from './localizers/date-fns' +export { default as dayjsLocalizer } from './localizers/dayjs' export { default as move } from './utils/move' export { views as Views, navigate as Navigate } from './utils/constants' diff --git a/src/localizers/dayjs.js b/src/localizers/dayjs.js new file mode 100644 index 0000000000..cfc7f55c5b --- /dev/null +++ b/src/localizers/dayjs.js @@ -0,0 +1,386 @@ +import { DateLocalizer } from '../localizer' + +const weekRangeFormat = ({ start, end }, culture, local) => + local.format(start, 'MMMM DD', culture) + + ' – ' + + // updated to use this localizer 'eq()' method + local.format(end, local.eq(start, end, 'month') ? 'DD' : 'MMMM DD', culture) + +const dateRangeFormat = ({ start, end }, culture, local) => + local.format(start, 'L', culture) + ' – ' + local.format(end, 'L', culture) + +const timeRangeFormat = ({ start, end }, culture, local) => + local.format(start, 'LT', culture) + ' – ' + local.format(end, 'LT', culture) + +const timeRangeStartFormat = ({ start }, culture, local) => + local.format(start, 'LT', culture) + ' – ' + +const 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 (dayjsLib) { + const locale = (dj, c) => (c ? dj.locale(c) : dj) + + // if a default timezone is set (dayjsLib.tz.setDefault()), + // then use the timezone aware version + const dayjs = dayjsLib.tz().$x.$timezone ? dayjsLib.tz : dayjsLib + + function getTimezoneOffset(date) { + // ensures this gets cast to timezone + return dayjs(date).toDate().getTimezoneOffset() + } + + function getDstOffset(start, end) { + // convert to dayjs, in case + const st = dayjs(start) + const ed = dayjs(end) + + /** + * If a default timezone has been applied, then + * use this to get the proper timezone offset, otherwise default + * the timezone to the browser local + */ + const tzName = st.tz().$x.$timezone ?? dayjsLib.tz.guess() + // invert offsets to be inline with moment.js + const startOffset = -dayjs.tz(+st, tzName).utcOffset() + const endOffset = -dayjs.tz(+ed, tzName).utcOffset() + return startOffset - endOffset + } + + function getDayStartDstOffset(start) { + const dayStart = dayjs(start).startOf('day') + return getDstOffset(dayStart, start) + } + + /*** 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 djDay = dayjs(day) + const djMin = dayjs(min) + const djMax = dayjs(max) + return djDay.isBetween(djMin, djMax, 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(culture) { + const data = culture ? dayjs.localeData(culture) : 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') + getDayStartDstOffset(start) + } + + // 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 + } + + function isSameDate(date1, date2) { + const dt = dayjs(date1) + const dt2 = dayjs(date2) + return dt.isSame(dt2, 'day') + } + + /** + * 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, + getTimezoneOffset, + getDstOffset, + getTotalMin, + getMinutesFromMidnight, + continuesPrior, + continuesAfter, + sortEvents, + inEventRange, + isSameDate, + browserTZOffset, + }) +} diff --git a/stories/demos/dayjs.stories.js b/stories/demos/dayjs.stories.js new file mode 100644 index 0000000000..51d3e8f763 --- /dev/null +++ b/stories/demos/dayjs.stories.js @@ -0,0 +1,20 @@ +import React from 'react' +import dayjs from 'dayjs' +import { Calendar, dayjsLocalizer } from '../../src' +import Dayjs from './exampleCode/dayjs' + +export default { + title: 'Examples', + component: Calendar, + parameters: { + docs: { + page: null, + }, + }, +} + +const localizer = dayjsLocalizer(dayjs) + +export function DayjsLocalizer() { + return +} diff --git a/stories/demos/exampleCode/dayjs.js b/stories/demos/exampleCode/dayjs.js new file mode 100644 index 0000000000..ef0a3af272 --- /dev/null +++ b/stories/demos/exampleCode/dayjs.js @@ -0,0 +1,67 @@ +import React, { Fragment, useMemo } from 'react' +import dayjs from 'dayjs' +import { Calendar, Views, dayjsLocalizer } from 'react-big-calendar' +import DemoLink from '../../DemoLink.component' +import events from '../../resources/events' +import * as dates from '../../../src/utils/dates' + +// import dayjs plugins +import isBetween from 'dayjs/plugin/isBetween' +import isSameOrAfter from 'dayjs/plugin/isSameOrAfter' +import isSameOrBefore from 'dayjs/plugin/isSameOrBefore' +import localeData from 'dayjs/plugin/localeData' +import localizedFormat from 'dayjs/plugin/localizedFormat' +import minMax from 'dayjs/plugin/minMax' +import timezone from 'dayjs/plugin/timezone' +import utc from 'dayjs/plugin/utc' + +// load dayjs plugins +dayjs.extend(isBetween) +dayjs.extend(isSameOrAfter) +dayjs.extend(isSameOrBefore) +dayjs.extend(localeData) +dayjs.extend(localizedFormat) +dayjs.extend(minMax) +dayjs.extend(timezone) +dayjs.extend(utc) + +const djLocalizer = dayjsLocalizer(dayjs) + +const ColoredDateCellWrapper = ({ children }) => + React.cloneElement(React.Children.only(children), { + style: { + backgroundColor: 'lightblue', + }, + }) + +export default function Dayjs({ ...props }) { + const { components, defaultDate, max, views } = useMemo( + () => ({ + components: { + timeSlotWrapper: ColoredDateCellWrapper, + }, + defaultDate: new Date(2015, 3, 1), + max: dates.add(dates.endOf(new Date(2015, 17, 1), 'day'), -1, 'hours'), + views: Object.keys(Views).map((k) => Views[k]), + }), + [] + ) + + return ( + + +
+ +
+
+ ) +} diff --git a/yarn.lock b/yarn.lock index 84cf95982b..6258f5d9da 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6755,6 +6755,11 @@ dateformat@^3.0.0: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== +dayjs@^1.11.5: + version "1.11.5" + resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.5.tgz#00e8cc627f231f9499c19b38af49f56dc0ac5e93" + integrity sha512-CAdX5Q3YW3Gclyo5Vpqkgpj8fSdLQcRuzfX6mC6Phy0nfJ0eGYOeS7m4mt2plDWLAtA4TqTakvbboHvUxfe4iA== + debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" From d7d5767a1cde817fca2799702d215022d810c531 Mon Sep 17 00:00:00 2001 From: Marnix Heuker of Hoek <47228669+marnixhoh@users.noreply.github.com> Date: Thu, 25 Aug 2022 13:45:19 +0200 Subject: [PATCH 05/15] fix(dayjs localizer): min() & max() now both behave correctly when a default timezone is applied --- src/localizers/dayjs.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/localizers/dayjs.js b/src/localizers/dayjs.js index cfc7f55c5b..8081a34a91 100644 --- a/src/localizers/dayjs.js +++ b/src/localizers/dayjs.js @@ -150,14 +150,14 @@ export default function (dayjsLib) { function min(dateA, dateB) { const dtA = dayjs(dateA) const dtB = dayjs(dateB) - const minDt = dayjs.min(dtA, dtB) + const minDt = dayjsLib.min(dtA, dtB) return minDt.toDate() } function max(dateA, dateB) { const dtA = dayjs(dateA) const dtB = dayjs(dateB) - const maxDt = dayjs.max(dtA, dtB) + const maxDt = dayjsLib.max(dtA, dtB) return maxDt.toDate() } @@ -210,7 +210,7 @@ export default function (dayjsLib) { } function firstOfWeek(culture) { - const data = culture ? dayjs.localeData(culture) : dayjs.localeData() + const data = culture ? dayjsLib.localeData(culture) : dayjsLib.localeData() return data ? data.firstDayOfWeek() : 0 } @@ -264,15 +264,15 @@ export default function (dayjsLib) { // These two are used by DateSlotMetrics function continuesPrior(start, first) { - const mStart = dayjs(start) - const mFirst = dayjs(first) - return mStart.isBefore(mFirst, 'day') + const djStart = dayjs(start) + const djFirst = dayjs(first) + return djStart.isBefore(djFirst, 'day') } function continuesAfter(start, end, last) { - const mEnd = dayjs(end) - const mLast = dayjs(last) - return mEnd.isSameOrAfter(mLast, 'minutes') + const djEnd = dayjs(end) + const djLast = dayjs(last) + return djEnd.isSameOrAfter(djLast, 'minutes') } // These two are used by eventLevels From edb1e2538c8c156b4612343be7675bd1c0e1abda Mon Sep 17 00:00:00 2001 From: Marnix Heuker of Hoek <47228669+marnixhoh@users.noreply.github.com> Date: Sat, 24 Sep 2022 17:11:06 +0200 Subject: [PATCH 06/15] docs(localizer guide): edited last two paragraphs to include the new localizer --- stories/guides/localizer.stories.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/stories/guides/localizer.stories.mdx b/stories/guides/localizer.stories.mdx index f1f78ba115..d6885676c6 100644 --- a/stories/guides/localizer.stories.mdx +++ b/stories/guides/localizer.stories.mdx @@ -45,6 +45,6 @@ Each `localizer`, when created, creates an instance of `DateLocalizer` class, an - `startOfWeek(culture) => number` (alias of `firstOfWeek()`) - `visibleDays(date:Date) => Array[Date]` -Many of these methods are used by Big Calendar in the background for determining layout. You can create your own custom `localizer`, to utilize some other library (for instance [Day.js](https://day.js.org/)), as long as they implement these methods. The `DateLocalizer` class defaults these methods to methods from the [date-arithmetic](https://www.npmjs.com/package/date-arithmetic) package. +Many of these methods are used by Big Calendar in the background for determining layout. You can create your own custom `localizer`, to utilize some other library, as long as they implement these methods. The `DateLocalizer` class defaults these methods to methods from the [date-arithmetic](https://www.npmjs.com/package/date-arithmetic) package. -For examples of building your own custom `localizer` look at the [momentLocalizer](https://github.com/jquense/react-big-calendar/blob/master/src/localizers/moment.js) or [luxonLocalizer](https://github.com/jquense/react-big-calendar/blob/master/src/localizers/luxon.js) as an example. If you do build your own `localizer`, please consider publishing it to [npm](https://npmjs.org). We suggest a common naming convention like `rbc-addon-mylocalizername`. +For examples of building your own custom `localizer` take a look at the [currently implemented localizers](https://github.com/jquense/react-big-calendar/blob/master/src/localizers). If you do build your own `localizer`, please consider publishing it to [npm](https://npmjs.org). We suggest a common naming convention like `rbc-addon-mylocalizername`. From 28a82471f690ad562e8b3d4af7816c97727b08bc Mon Sep 17 00:00:00 2001 From: Marnix Heuker of Hoek <47228669+marnixhoh@users.noreply.github.com> Date: Fri, 30 Sep 2022 10:25:30 +0200 Subject: [PATCH 07/15] fix(dayjs localizer): dayjs plugins are now loaded by the localizer --- src/localizers/dayjs.js | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/src/localizers/dayjs.js b/src/localizers/dayjs.js index 8081a34a91..07cb9119ef 100644 --- a/src/localizers/dayjs.js +++ b/src/localizers/dayjs.js @@ -1,5 +1,26 @@ +import dayjs from 'dayjs' import { DateLocalizer } from '../localizer' +// import dayjs plugins +// Note that the timezone plugin is not imported here +// this plugin can be optionally loaded by the user +import isBetween from 'dayjs/plugin/isBetween' +import isSameOrAfter from 'dayjs/plugin/isSameOrAfter' +import isSameOrBefore from 'dayjs/plugin/isSameOrBefore' +import localeData from 'dayjs/plugin/localeData' +import localizedFormat from 'dayjs/plugin/localizedFormat' +import minMax from 'dayjs/plugin/minMax' +import utc from 'dayjs/plugin/utc' + +// load dayjs plugins +dayjs.extend(isBetween) +dayjs.extend(isSameOrAfter) +dayjs.extend(isSameOrBefore) +dayjs.extend(localeData) +dayjs.extend(localizedFormat) +dayjs.extend(minMax) +dayjs.extend(utc) + const weekRangeFormat = ({ start, end }, culture, local) => local.format(start, 'MMMM DD', culture) + ' – ' + @@ -53,9 +74,9 @@ function fixUnit(unit) { export default function (dayjsLib) { const locale = (dj, c) => (c ? dj.locale(c) : dj) - // if a default timezone is set (dayjsLib.tz.setDefault()), + // if the timezone plugin is loaded, // then use the timezone aware version - const dayjs = dayjsLib.tz().$x.$timezone ? dayjsLib.tz : dayjsLib + const dayjs = dayjsLib.tz ? dayjsLib.tz : dayjsLib function getTimezoneOffset(date) { // ensures this gets cast to timezone @@ -66,7 +87,10 @@ export default function (dayjsLib) { // convert to dayjs, in case const st = dayjs(start) const ed = dayjs(end) - + // if not using the dayjs timezone plugin + if (!dayjs.tz) { + return st.toDate().getTimezoneOffset() - ed.toDate().getTimezoneOffset() + } /** * If a default timezone has been applied, then * use this to get the proper timezone offset, otherwise default From e3d68f0c338f3fc44df4f038ec66505592be374a Mon Sep 17 00:00:00 2001 From: Marnix Heuker of Hoek <47228669+marnixhoh@users.noreply.github.com> Date: Fri, 30 Sep 2022 10:28:22 +0200 Subject: [PATCH 08/15] docs: updated README.md & exampleCode to reflect dayjs plugins being loaded by the localizer --- README.md | 22 ++-------------------- stories/demos/exampleCode/dayjs.js | 18 +----------------- 2 files changed, 3 insertions(+), 37 deletions(-) diff --git a/README.md b/README.md index e762209c12..34b5f578b5 100644 --- a/README.md +++ b/README.md @@ -120,30 +120,12 @@ const MyCalendar = (props) => ( ) ``` +#### Day.js + ```js import { Calendar, dayjsLocalizer } from 'react-big-calendar' import dayjs from 'dayjs' -// import necessary dayjs plugins -import isBetween from 'dayjs/plugin/isBetween' -import isSameOrAfter from 'dayjs/plugin/isSameOrAfter' -import isSameOrBefore from 'dayjs/plugin/isSameOrBefore' -import localeData from 'dayjs/plugin/localeData' -import localizedFormat from 'dayjs/plugin/localizedFormat' -import minMax from 'dayjs/plugin/minMax' -import timezone from 'dayjs/plugin/timezone' -import utc from 'dayjs/plugin/utc' - -// load necessary dayjs plugins -dayjs.extend(isBetween) -dayjs.extend(isSameOrAfter) -dayjs.extend(isSameOrBefore) -dayjs.extend(localeData) -dayjs.extend(localizedFormat) -dayjs.extend(minMax) -dayjs.extend(timezone) -dayjs.extend(utc) - const localizer = dayjsLocalizer(dayjs) const MyCalendar = (props) => ( diff --git a/stories/demos/exampleCode/dayjs.js b/stories/demos/exampleCode/dayjs.js index ef0a3af272..cba0a6c1ae 100644 --- a/stories/demos/exampleCode/dayjs.js +++ b/stories/demos/exampleCode/dayjs.js @@ -5,25 +5,9 @@ import DemoLink from '../../DemoLink.component' import events from '../../resources/events' import * as dates from '../../../src/utils/dates' -// import dayjs plugins -import isBetween from 'dayjs/plugin/isBetween' -import isSameOrAfter from 'dayjs/plugin/isSameOrAfter' -import isSameOrBefore from 'dayjs/plugin/isSameOrBefore' -import localeData from 'dayjs/plugin/localeData' -import localizedFormat from 'dayjs/plugin/localizedFormat' -import minMax from 'dayjs/plugin/minMax' +// add optional time zone support import timezone from 'dayjs/plugin/timezone' -import utc from 'dayjs/plugin/utc' - -// load dayjs plugins -dayjs.extend(isBetween) -dayjs.extend(isSameOrAfter) -dayjs.extend(isSameOrBefore) -dayjs.extend(localeData) -dayjs.extend(localizedFormat) -dayjs.extend(minMax) dayjs.extend(timezone) -dayjs.extend(utc) const djLocalizer = dayjsLocalizer(dayjs) From c33ee90859517fd19b2819c574b7246267c935df Mon Sep 17 00:00:00 2001 From: Marnix Heuker of Hoek <47228669+marnixhoh@users.noreply.github.com> Date: Fri, 30 Sep 2022 10:31:40 +0200 Subject: [PATCH 09/15] docs(localizer props): added dayjs example & sorted examples alphabetically --- stories/props/localizer.mdx | 39 ++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/stories/props/localizer.mdx b/stories/props/localizer.mdx index 3fb52a7282..e43c6be04d 100644 --- a/stories/props/localizer.mdx +++ b/stories/props/localizer.mdx @@ -8,24 +8,24 @@ import LinkTo from '@storybook/addon-links/react' The localizer used for **formatting dates** and times according to the formats and culture. To format buttons and messaging use the messages prop. ```js -// When using `Globalize` -import { globalizeLocalizer } from 'react-big-calendar' -import globalize from 'globalize' +// When using `Day.js` +import { dayjsLocalizer } from 'react-big-calendar' +import dayjs from 'dayjs' +// and, for optional time zone support +import timezone from 'dayjs/plugin/timezone' -const localizer = globalizeLocalizer(globalize) +dayjs.extend(timezone) +// end optional time zone support + +const localizer = dayjsLocalizer(dayjs) ``` ```js -// When using `moment` -import { momentLocalizer } from 'react-big-calendar' -import moment from 'moment' -// and, for optional time zone support -import 'moment-timezone' - -moment.tz.setDefault('America/Los_Angeles') -// end optional time zone support +// When using `Globalize` +import { globalizeLocalizer } from 'react-big-calendar' +import globalize from 'globalize' -const localizer = momentLocalizer(moment) +const localizer = globalizeLocalizer(globalize) ``` ```js @@ -45,6 +45,19 @@ Settings.defaultZone = 'America/Los_Angeles' const localizer = luxonLocalizer(DateTime, { firstDayOfWeek: 7 }) ``` +```js +// When using `moment` +import { momentLocalizer } from 'react-big-calendar' +import moment from 'moment' +// and, for optional time zone support +import 'moment-timezone' + +moment.tz.setDefault('America/Los_Angeles') +// end optional time zone support + +const localizer = momentLocalizer(moment) +``` + See the Localization Example for another example of combining `rtl`, `localizer`, `culture` and `messages`. From 126fa49df3c779392803293c355dae71dbb52698 Mon Sep 17 00:00:00 2001 From: Marnix Heuker of Hoek <47228669+marnixhoh@users.noreply.github.com> Date: Fri, 30 Sep 2022 11:01:08 +0200 Subject: [PATCH 10/15] docs(exampleCode dayjs): use dayjs to construct date based props --- src/localizers/dayjs.js | 2 +- stories/demos/exampleCode/dayjs.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/localizers/dayjs.js b/src/localizers/dayjs.js index 07cb9119ef..2ad248dab1 100644 --- a/src/localizers/dayjs.js +++ b/src/localizers/dayjs.js @@ -191,7 +191,7 @@ export default function (dayjsLib) { 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() + return dayjsLib(`${dt} ${tm}`, 'MM/DD/YYYY HH:mm:ss').toDate() } function add(date, adder, unit) { diff --git a/stories/demos/exampleCode/dayjs.js b/stories/demos/exampleCode/dayjs.js index cba0a6c1ae..410bf472d9 100644 --- a/stories/demos/exampleCode/dayjs.js +++ b/stories/demos/exampleCode/dayjs.js @@ -3,7 +3,6 @@ import dayjs from 'dayjs' import { Calendar, Views, dayjsLocalizer } from 'react-big-calendar' import DemoLink from '../../DemoLink.component' import events from '../../resources/events' -import * as dates from '../../../src/utils/dates' // add optional time zone support import timezone from 'dayjs/plugin/timezone' @@ -25,7 +24,7 @@ export default function Dayjs({ ...props }) { timeSlotWrapper: ColoredDateCellWrapper, }, defaultDate: new Date(2015, 3, 1), - max: dates.add(dates.endOf(new Date(2015, 17, 1), 'day'), -1, 'hours'), + max: dayjs().endOf('day').subtract(1, 'hours').toDate(), views: Object.keys(Views).map((k) => Views[k]), }), [] From 44c184be0080bb9ceaf529172a24127c2bac7321 Mon Sep 17 00:00:00 2001 From: Marnix Heuker of Hoek <47228669+marnixhoh@users.noreply.github.com> Date: Fri, 30 Sep 2022 11:07:52 +0200 Subject: [PATCH 11/15] test: added commented out dayjs configuration --- test/utils/DayEventLayout.test.js | 4 ++++ test/utils/TimeSlots.test.js | 3 +++ test/utils/eventLevels.test.js | 3 +++ 3 files changed, 10 insertions(+) diff --git a/test/utils/DayEventLayout.test.js b/test/utils/DayEventLayout.test.js index 5e981023ce..4f4c598e72 100644 --- a/test/utils/DayEventLayout.test.js +++ b/test/utils/DayEventLayout.test.js @@ -1,12 +1,16 @@ import moment from 'moment' import momentLocalizer from '../../src/localizers/moment' +// import dayjs from 'dayjs' +// import dayjsLocalizer from '../../src/localizers/dayjs' //import { DateTime } from 'luxon' //import luxonLocalizer from '../../src/localizers/luxon' + import { getStyledEvents } from '../../src/utils/DayEventLayout' import { getSlotMetrics } from '../../src/utils/TimeSlots' import * as dates from '../../src/utils/dates' const localizer = momentLocalizer(moment) +// const localizer = dayjsLocalizer(dayjs) //const localizer = luxonLocalizer(DateTime) describe('getStyledEvents', () => { diff --git a/test/utils/TimeSlots.test.js b/test/utils/TimeSlots.test.js index c2296f28b1..4464f182c5 100644 --- a/test/utils/TimeSlots.test.js +++ b/test/utils/TimeSlots.test.js @@ -1,11 +1,14 @@ import moment from 'moment' import momentLocalizer from '../../src/localizers/moment' +// import dayjs from 'dayjs' +// import dayjsLocalizer from '../../src/localizers/dayjs' //import { DateTime } from 'luxon' //import luxonLocalizer from '../../src/localizers/luxon' import { getSlotMetrics } from '../../src/utils/TimeSlots' import * as dates from '../../src/utils/dates' const localizer = momentLocalizer(moment) +// const localizer = dayjsLocalizer(dayjs) //const localizer = luxonLocalizer(DateTime) describe('getSlotMetrics', () => { diff --git a/test/utils/eventLevels.test.js b/test/utils/eventLevels.test.js index 5d0dea77d8..c8e075a955 100644 --- a/test/utils/eventLevels.test.js +++ b/test/utils/eventLevels.test.js @@ -1,5 +1,7 @@ import moment from 'moment' import momentLocalizer from '../../src/localizers/moment' +// import dayjs from 'dayjs' +// import dayjsLocalizer from '../../src/localizers/dayjs' //import { DateTime } from 'luxon' //import luxonLocalizer from '../../src/localizers/luxon' import { @@ -12,6 +14,7 @@ import { } from '../../src/utils/eventLevels' const localizer = momentLocalizer(moment) +// const localizer = dayjsLocalizer(dayjs) //const localizer = luxonLocalizer(DateTime) describe('endOfRange', () => { From 634296b5b6c3d6ed517364e0e406db078a7555de Mon Sep 17 00:00:00 2001 From: Marnix Heuker of Hoek <47228669+marnixhoh@users.noreply.github.com> Date: Fri, 30 Sep 2022 11:56:24 +0200 Subject: [PATCH 12/15] revert(README.md): remove note on node 16 storybook requirement --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 34b5f578b5..d2bf04fda3 100644 --- a/README.md +++ b/README.md @@ -25,8 +25,6 @@ element has a height, or the calendar won't be visible. To provide your own cust ## Run examples locally -Note: node >= 16 is required to run the storybook - ```sh $ git clone git@github.com:jquense/react-big-calendar.git $ cd react-big-calendar From 5ba62cbab9cac5041be53aefb7764651210ad6c1 Mon Sep 17 00:00:00 2001 From: Marnix Heuker of Hoek <47228669+marnixhoh@users.noreply.github.com> Date: Fri, 30 Sep 2022 14:53:25 +0200 Subject: [PATCH 13/15] fix(dayjs localizer): dayjs plugins are now loaded by the dayjsLocalizer --- src/localizers/dayjs.js | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/localizers/dayjs.js b/src/localizers/dayjs.js index 2ad248dab1..4f87874187 100644 --- a/src/localizers/dayjs.js +++ b/src/localizers/dayjs.js @@ -1,4 +1,3 @@ -import dayjs from 'dayjs' import { DateLocalizer } from '../localizer' // import dayjs plugins @@ -12,15 +11,6 @@ import localizedFormat from 'dayjs/plugin/localizedFormat' import minMax from 'dayjs/plugin/minMax' import utc from 'dayjs/plugin/utc' -// load dayjs plugins -dayjs.extend(isBetween) -dayjs.extend(isSameOrAfter) -dayjs.extend(isSameOrBefore) -dayjs.extend(localeData) -dayjs.extend(localizedFormat) -dayjs.extend(minMax) -dayjs.extend(utc) - const weekRangeFormat = ({ start, end }, culture, local) => local.format(start, 'MMMM DD', culture) + ' – ' + @@ -72,6 +62,15 @@ function fixUnit(unit) { } export default function (dayjsLib) { + // load dayjs plugins + dayjsLib.extend(isBetween) + dayjsLib.extend(isSameOrAfter) + dayjsLib.extend(isSameOrBefore) + dayjsLib.extend(localeData) + dayjsLib.extend(localizedFormat) + dayjsLib.extend(minMax) + dayjsLib.extend(utc) + const locale = (dj, c) => (c ? dj.locale(c) : dj) // if the timezone plugin is loaded, From 427e15e49ca64155e00ce1840c83bab6b10580a0 Mon Sep 17 00:00:00 2001 From: Marnix Heuker of Hoek <47228669+marnixhoh@users.noreply.github.com> Date: Fri, 30 Sep 2022 14:55:35 +0200 Subject: [PATCH 14/15] docs(dayjs localizer): mention dayjs plugins being loaded by the dayjsLocalizer --- README.md | 12 +++++++++++- stories/demos/exampleCode/dayjs.js | 9 +++++++++ stories/props/localizer.mdx | 9 +++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index d2bf04fda3..e5c9fc62e4 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ $ yarn storybook ### Localization and Date Formatting -`react-big-calendar` includes three options for handling the date formatting and culture localization, depending +`react-big-calendar` includes four options for handling the date formatting and culture localization, depending on your preference of DateTime libraries. You can use either the [Moment.js](https://momentjs.com/), [Globalize.js](https://github.com/jquery/globalize), [date-fns](https://date-fns.org/), [Day.js](https://day.js.org) localizers. Regardless of your choice, you **must** choose a localizer to use this library: @@ -120,6 +120,16 @@ const MyCalendar = (props) => ( #### Day.js +Note that the dayjsLocalizer extends Day.js with the following plugins: + +- [IsBetween](https://day.js.org/docs/en/plugin/is-between) +- [IsSameOrAfter](https://day.js.org/docs/en/plugin/is-same-or-after) +- [IsSameOrBefore](https://day.js.org/docs/en/plugin/is-same-or-before) +- [LocaleData](https://day.js.org/docs/en/plugin/locale-data) +- [LocalizedFormat](https://day.js.org/docs/en/plugin/localized-format) +- [MinMax](https://day.js.org/docs/en/plugin/min-max) +- [UTC](https://day.js.org/docs/en/plugin/utc) + ```js import { Calendar, dayjsLocalizer } from 'react-big-calendar' import dayjs from 'dayjs' diff --git a/stories/demos/exampleCode/dayjs.js b/stories/demos/exampleCode/dayjs.js index 410bf472d9..12d3bb4004 100644 --- a/stories/demos/exampleCode/dayjs.js +++ b/stories/demos/exampleCode/dayjs.js @@ -4,6 +4,15 @@ import { Calendar, Views, dayjsLocalizer } from 'react-big-calendar' import DemoLink from '../../DemoLink.component' import events from '../../resources/events' +// Note that the dayjsLocalizer extends Day.js with the following plugins: +// - IsBetween +// - IsSameOrAfter +// - IsSameOrBefore +// - LocaleData +// - LocalizedFormat +// - MinMax +// - UTC + // add optional time zone support import timezone from 'dayjs/plugin/timezone' dayjs.extend(timezone) diff --git a/stories/props/localizer.mdx b/stories/props/localizer.mdx index e43c6be04d..c0670e9147 100644 --- a/stories/props/localizer.mdx +++ b/stories/props/localizer.mdx @@ -17,6 +17,15 @@ import timezone from 'dayjs/plugin/timezone' dayjs.extend(timezone) // end optional time zone support +// Note that the dayjsLocalizer extends Day.js with the following plugins: +// - IsBetween +// - IsSameOrAfter +// - IsSameOrBefore +// - LocaleData +// - LocalizedFormat +// - MinMax +// - UTC + const localizer = dayjsLocalizer(dayjs) ``` From 3249fbddddb64404d418ef95aeb80b0073351175 Mon Sep 17 00:00:00 2001 From: Marnix Heuker of Hoek <47228669+marnixhoh@users.noreply.github.com> Date: Fri, 30 Sep 2022 14:56:19 +0200 Subject: [PATCH 15/15] docs(timezones): mention the dayjsLocalizer's ability to handle timezones too --- stories/guides/Timezones.stories.mdx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stories/guides/Timezones.stories.mdx b/stories/guides/Timezones.stories.mdx index c3faaa4c72..295bdf2141 100644 --- a/stories/guides/Timezones.stories.mdx +++ b/stories/guides/Timezones.stories.mdx @@ -34,4 +34,4 @@ const MyCalendar = (props) => ( The `momentLocalizer` will now handle all dates and date math as if the date is in the timezone you specified. It is important to note that [changing moment's default timezone](https://momentjs.com/timezone/docs/#/using-timezones/default-timezone/) affects all dates, created by moment, from that point forward, so you may want to reset the default when your component unmounts. And, if switching timezones 'on-the-fly', you want to update your `localizer` and any Date based props (min, max, getNow, etc) at the same time. -**Note:** The new `luxonLocalizer` operates in a similar fashion. View the 'Luxon Localizer' demo and view it's source for an example of it's usage. +**Note:** The `luxonLocalizer` and `dayjsLocalizer` operate in a similar fashion. View their respective demos and source for examples of their usage.