src/date-picker/months.tsx (91 lines of code) (raw):
import {useEffect, useRef} from 'react';
import {addMonths} from 'date-fns/addMonths';
import {getDay} from 'date-fns/getDay';
import {getDaysInMonth} from 'date-fns/getDaysInMonth';
import {startOfMonth} from 'date-fns/startOfMonth';
import {subMonths} from 'date-fns/subMonths';
import {endOfMonth} from 'date-fns/endOfMonth';
import scheduleRAF from '../global/schedule-raf';
import linearFunction from '../global/linear-function';
import useEventCallback from '../global/use-event-callback';
import Month from './month';
import MonthNames from './month-names';
import units, {DOUBLE, HALF, type MonthsProps, WEEK, weekdays} from './consts';
import styles from './date-picker.css';
const {unit, cellSize, calHeight} = units;
const FridayToSunday = WEEK + weekdays.SU - weekdays.FR;
const FIVELINES = 31;
const TALLMONTH = 6;
const SHORTMONTH = 5;
const PADDING = 2;
const MONTHSBACK = 2;
function monthHeight(date: Date | number) {
const monthStart = startOfMonth(date);
const daysSinceLastFriday = (getDay(monthStart) + FridayToSunday) % WEEK;
const monthLines = daysSinceLastFriday + getDaysInMonth(monthStart) > FIVELINES ? TALLMONTH : SHORTMONTH;
return monthLines * cellSize + unit * PADDING;
}
// in milliseconds per pixel
function scrollSpeed(date: Date | number) {
const monthStart = startOfMonth(date);
const monthEnd = endOfMonth(date);
return (Number(monthEnd) - Number(monthStart)) / monthHeight(monthStart);
}
const scrollSchedule = scheduleRAF();
let dy = 0;
export default function Months(props: MonthsProps) {
const {scrollDate} = props;
const monthDate = scrollDate instanceof Date ? scrollDate : new Date(scrollDate);
const monthStart = startOfMonth(monthDate);
let month = subMonths(monthStart, MONTHSBACK);
const months = [month];
for (let i = 0; i < MONTHSBACK * DOUBLE; i++) {
month = addMonths(month, 1);
months.push(month);
}
const currentSpeed = scrollSpeed(scrollDate);
const pxToDate = linearFunction(0, Number(scrollDate), currentSpeed);
const offset = pxToDate.x(Number(monthStart)); // is a negative number
const bottomOffset = monthHeight(scrollDate) + offset;
const componentRef = useRef<HTMLDivElement>(null);
const handleWheel = useEventCallback((e: WheelEvent) => {
e.preventDefault();
dy += e.deltaY;
scrollSchedule(() => {
let date;
// adjust scroll speed to prevent glitches
if (dy < offset) {
date = pxToDate.y(offset) + (dy - offset) * scrollSpeed(months[1]);
} else if (dy > bottomOffset) {
date = pxToDate.y(bottomOffset) + (dy - bottomOffset) * scrollSpeed(months[MONTHSBACK + 1]);
} else {
date = pxToDate.y(dy);
}
props.onScroll(date);
dy = 0;
});
});
useEffect(() => {
const current = componentRef.current;
if (current) {
current.addEventListener('wheel', handleWheel, {passive: false});
}
return () => {
if (current) {
current.removeEventListener('wheel', handleWheel);
}
};
}, [handleWheel]);
return (
<div className={styles.months} ref={componentRef}>
<div
style={{
top: Math.floor(calHeight * HALF - monthHeight(months[0]) - monthHeight(months[1]) + offset),
}}
className={styles.days}
>
{months.map(date => (
<Month {...props} month={date} key={+date} />
))}
</div>
<MonthNames {...props} />
</div>
);
}