Calendar field and time field

This commit is contained in:
Ben Grant 2021-03-02 20:31:32 +11:00
parent edcd4dcaa0
commit 0dde47109f
32 changed files with 901 additions and 65 deletions

View file

@ -0,0 +1,192 @@
import { useState, useEffect, useRef } from 'react';
import dayjs from 'dayjs';
import isToday from 'dayjs/plugin/isToday';
import { Button } from 'components';
import {
Wrapper,
StyledLabel,
StyledSubLabel,
CalendarHeader,
CalendarBody,
Date,
Day,
} from './calendarFieldStyle';
dayjs.extend(isToday);
const days = [
'Sun',
'Mon',
'Tue',
'Wed',
'Thu',
'Fri',
'Sat',
];
const months = [
'January',
'February',
'March',
'April',
'May',
'June',
'July',
'August',
'September',
'October',
'November',
'December',
];
const calculateMonth = (month, year) => {
const date = dayjs().month(month).year(year);
const daysInMonth = date.daysInMonth();
const daysBefore = date.date(1).day();
const daysAfter = 6 - date.date(daysInMonth).day();
let dates = [];
let curDate = date.date(1).subtract(daysBefore, 'day');
let y = 0;
let x = 0;
for (let i = 0; i < daysBefore + daysInMonth + daysAfter; i++) {
if (x === 0) dates[y] = [];
dates[y][x] = curDate.clone();
curDate = curDate.add(1, 'day');
x++;
if (x > 6) {
x = 0;
y++;
}
}
return dates;
};
const CalendarField = ({
label,
subLabel,
id,
register,
...props
}) => {
const [dates, setDates] = useState(calculateMonth(dayjs().month(), dayjs().year()));
const [month, setMonth] = useState(dayjs().month());
const [year, setYear] = useState(dayjs().year());
const [selectedDates, setSelectedDates] = useState([]);
const [selectingDates, _setSelectingDates] = useState([]);
const staticSelectingDates = useRef([]);
const setSelectingDates = newDates => {
staticSelectingDates.current = newDates;
_setSelectingDates(newDates);
};
const startPos = useRef({});
const staticMode = useRef(null);
const [mode, _setMode] = useState(staticMode.current);
const setMode = newMode => {
staticMode.current = newMode;
_setMode(newMode);
};
useEffect(() => {
setDates(calculateMonth(month, year));
}, [month, year]);
return (
<Wrapper>
{label && <StyledLabel htmlFor={id}>{label}</StyledLabel>}
{subLabel && <StyledSubLabel htmlFor={id}>{subLabel}</StyledSubLabel>}
<input
id={id}
type="hidden"
ref={register}
value={JSON.stringify(selectedDates)}
{...props}
/>
<CalendarHeader>
<Button
buttonHeight="30px"
buttonWidth="30px"
padding="0"
title="Previous month"
type="button"
onClick={() => {
if (month-1 < 0) {
setYear(year-1);
setMonth(11);
} else {
setMonth(month-1);
}
}}
>&lt;</Button>
<span>{months[month]} {year}</span>
<Button
buttonHeight="30px"
buttonWidth="30px"
padding="0"
title="Next month"
type="button"
onClick={() => {
if (month+1 > 11) {
setYear(year+1);
setMonth(0);
} else {
setMonth(month+1);
}
}}
>&gt;</Button>
</CalendarHeader>
<CalendarBody>
{days.map((name, i) =>
<Day key={i}>{name}</Day>
)}
{dates.length > 0 && dates.map((dateRow, y) =>
dateRow.map((date, x) =>
<Date
key={y+x}
otherMonth={date.month() !== month}
isToday={date.isToday()}
title={`${date.date()} ${months[date.month()]}${date.isToday() ? ' (today)' : ''}`}
selected={selectedDates.includes(date.format('DDMMYYYY'))}
selecting={selectingDates.includes(date)}
mode={mode}
onMouseDown={() => {
startPos.current = {x, y};
setMode(selectedDates.includes(date.format('DDMMYYYY')) ? 'remove' : 'add');
setSelectingDates([date]);
document.addEventListener('mouseup', () => {
if (staticMode.current === 'add') {
setSelectedDates([...selectedDates, ...staticSelectingDates.current.map(d => d.format('DDMMYYYY'))]);
} else if (staticMode.current === 'remove') {
const toRemove = staticSelectingDates.current.map(d => d.format('DDMMYYYY'));
setSelectedDates(selectedDates.filter(d => !toRemove.includes(d)));
}
setMode(null);
}, { once: true });
}}
onMouseEnter={() => {
if (staticMode.current) {
let found = [];
for (let cy = Math.min(startPos.current.y, y); cy < Math.max(startPos.current.y, y)+1; cy++) {
for (let cx = Math.min(startPos.current.x, x); cx < Math.max(startPos.current.x, x)+1; cx++) {
found.push({y: cy, x: cx});
}
}
setSelectingDates(found.map(d => dates[d.y][d.x]));
}
}}
>{date.date()}</Date>
)
)}
</CalendarBody>
</Wrapper>
);
};
export default CalendarField;

View file

@ -0,0 +1,73 @@
import styled from '@emotion/styled';
export const Wrapper = styled.div`
margin: 30px 0;
`;
export const StyledLabel = styled.label`
display: block;
padding-bottom: 4px;
font-size: 18px;
`;
export const StyledSubLabel = styled.label`
display: block;
padding-bottom: 6px;
font-size: 13px;
opacity: .6;
`;
export const CalendarHeader = styled.div`
display: flex;
align-items: center;
justify-content: space-between;
user-select: none;
padding: 6px 0;
font-size: 1.2em;
font-weight: bold;
`;
export const CalendarBody = styled.div`
display: grid;
grid-template-columns: repeat(7, 1fr);
grid-gap: 2px;
`;
export const Date = styled.div`
background-color: ${props => props.theme.primary}22;
border: 1px solid ${props => props.theme.primaryLight};
display: flex;
align-items: center;
justify-content: center;
padding: 10px 0;
border-radius: 3px;
user-select: none;
${props => props.otherMonth && `
color: ${props.theme.primaryLight};
`}
${props => props.isToday && `
font-weight: 900;
color: ${props.theme.primaryDark};
`}
${props => (props.selected || (props.mode === 'add' && props.selecting)) && `
color: ${props.otherMonth ? 'rgba(255,255,255,.5)' : '#FFF'};
background-color: ${props.theme.primary};
border-color: ${props.theme.primary};
`}
${props => props.mode === 'remove' && props.selecting && `
background-color: ${props.theme.primary}22;
border: 1px solid ${props.theme.primaryLight};
color: ${props.isToday ? props.theme.primaryDark : (props.otherMonth ? props.theme.primaryLight : 'inherit')};
`}
`;
export const Day = styled.div`
display: flex;
align-items: center;
justify-content: center;
padding: 3px 10px;
font-weight: bold;
user-select: none;
opacity: .7;
`;