import React, {lazy, Suspense} from "react"
import PropTypes from "prop-types"
import { useDispatch, useSelector } from "react-redux"
import * as paramsAction from "../../features/params"
import * as selectedDateAction from "../../features/selectedDate"
import * as errorAction from "../../features/error"
import { selectSelectedDate } from "../../utils/selectors";
import Dialog from "../dialog";
import CalendarSelect from "./CalendarSelect";
import { CalendarBox, CalendarList, CalendarListItem, CalendarSection, DateSelect, style } from "../../style"
import { datePickerParams } from "../../utils/datePickerParams"
import { weekdays, months, getLimitYear, transformToNumber, currentDate } from "../../utils/date";
import { validation } from "../../utils/validation";
const TimeSelect = lazy( () => import("./TimeSelect") )
/**
* iterates over elements with class selected-day and removes it
* @function
*/
const deleteSelectedDay = () => document.querySelectorAll(".selected-day").forEach((element) => { element.classList.remove("selected-day") })
/**
* Checks that the current day, passed as a parameter, is included in the number of days of the month. Otherwise fix it
* @function
* @param {number} currentDay
* @param {number} monthLength
* @param {number} startDay
* @returns {number} dayNumber
*/
const getNumberDay = (currentDay, monthLength, startDay) => (currentDay >= (startDay+1) && (currentDay - startDay) <= monthLength )
&& parseInt(currentDay - startDay)
Calendar.propTypes = {
baseId: PropTypes.string.isRequired,
displayBox: PropTypes.bool
}
Calendar.defaultProp = {
displayBox: false
}
/**
* Display Calendar
* @component
* @param {string} props.baseId - use to create calendar modal id
* @param {boolean} props.displayBox - true : element is diplayed
* @returns {object} Dialog component
*/
function Calendar(props){
const {baseId, displayBox} = props
const dispatch = useDispatch()
/**
* Object containing methods to set the date and/or time
*/
const calendarDate = {
/**
* change isEndDate attribute value
* @method
* @memberof calendarDate
* @returns {boolean}
*/
changeIsEndDate: () => calendarDate.isEndDate = !calendarDate.isEndDate,
/**
* Provide selected hour (start or end)
* @method
* @memberof calendarDate
* @param {string | boolean} typeDate
* @returns {number} Hour
*/
getHours: (typeDate = false) => typeDate === "start" ? selectedDate.start.hour ? parseInt(selectedDate.start.hour) : 12
: typeDate === "end" ? selectedDate.end.hour ? parseInt(selectedDate.end.hour) : 12
: calendarDate.hour,
/**
* Provides selected minutes (start or end) in correct format
* @method
* @memberof calendarDate
* @param {string | boolean} unitOrDecimal - sets the desired minutes (unit or decimal)
* @param {string | boolean} typeDate - set type date : end or start
* @returns {number} minutes
*/
getMinutes: (unitOrDecimal = false, typeDate = false) => {
const minutes = typeDate === "end" ? selectedDate.end.minute ? selectedDate.end.minute : 0
: typeDate === "start" ? selectedDate.start.minute ? selectedDate.start.minute : calendarDate.minute : calendarDate.minute
return !unitOrDecimal ? parseInt(minutes) : unitOrDecimal === "unit"
? minutes > 9 ? parseInt(String(minutes).substring(1)) : parseInt(minutes)
: minutes > 9 ? parseInt(String(minutes).substring(0, 1)) : 0
},
/**
* Set and store date and his type
* @method
* @memberof calendarDate
* @param {object} selectedDate
*/
initDate: (selectedDate) => {
if(datePickerParams.is[baseId].period){
datePickerParams.is[baseId].period = true
calendarDate.nameSuffix = "End"
calendarDate.typeDate = !selectedDate.start.day || !calendarDate.isEndDate() ? "start" : "end"
} else { calendarDate.typeDate = false }
if(datePickerParams.is[baseId].date){ calendarDate.setDate(selectedDate.calendar) }
if(datePickerParams.is[baseId].time){ calendarDate.setTime(selectedDate.calendar) }
},
/**
* Defines whether date is end or start date
* @method
* @memberof calendarDate
* @return {boolean} isEndDate
*/
isEndDate: () => selectedDate.status === "pending" ? true : false,
nameSuffix: "",
/**
* Store date displayed by Calendar
* @method
* @memberof calendarDate
* @param {object} date
* @example { day: 01, month: 0, year: 2022}
*/
setDate: (date) => {
calendarDate.day = date.day ? parseInt(date.day) : currentDate.day
calendarDate.month = date.month ? parseInt(date.month) : currentDate.month
calendarDate.year = date.year ? parseInt(date.year) : currentDate.year
},
/**
* Store time displayed by Calendar
* @method
* @memberof calendarDate
* @param {object} time
* @example { hour: 01, minute: 00}
*/
setTime: (time) => {
calendarDate.hour = time.hour ? parseInt(time.hour) : 12
calendarDate.minute = time.minute ? parseInt(time.minute) : 0
}
}
const selectedDate = useSelector(selectSelectedDate(baseId))
if(!calendarDate.day){ calendarDate.initDate(selectedDate) }
const calendarMonthSelected = calendarDate.month - 1
const date = new Date(calendarDate.year, calendarMonthSelected, 1)
const startDay = date.getDay()
const monthLength = months.getLength(calendarMonthSelected, calendarDate.year)
const monthDays = Array(42).fill(1)
const startYear = getLimitYear("min")
const years = Array(100).fill(startYear)
const calendarOptions = [
{ name: "previous-month", type: "icon" },
{ name: "home", type: "icon" },
{ name: "month", type: "select", value: months.name[calendarMonthSelected] },
{ name: "year", type: "select", value: calendarDate.year },
{ name: "next-month", type: "icon" }
]
/**
* Object containing the methods to apply during a click
*/
const click = {
/**
* Checks that the month parameter is between 1 and 12, otherwise assigns a new value (start or end of the interval)
* @method
* @memberof click
* @param {number} month
* @returns {number} month
*/
browseMonths: (month) => month > 12 ? 1 : month < 1 ? 12 : month,
/**
* Function to trigger when clicking on a day
* @method
* @memberof click
* @param {object} e - event
* @see click.fct
*/
days: (e) => click.fct(e, "Day"),
/**
* Save the values, selected on click, and animate the calendar: opening, closing and moving in the sections.
* Function to trigger when clicking on a day, month, year, hour or minute.
* @method
* @memberof click
* @param {object} e - event
*/
fct: (e, name) => {
// Delete error messages
dispatch(errorAction.clear(baseId))
// Get value
let value = name === "Month" ? parseInt(months.name.indexOf(e.target.textContent)) + 1
: name === "Minute" ? parseInt(e.target.getAttribute("id").indexOf("minutesuni") > 0
? String(document.querySelector(
`div#${datePickerParams.getTimeSelectId(baseId, "minutesDec", calendarDate.typeDate)} .selected-option`).textContent)
+ String(document.querySelector(
`div#${datePickerParams.getTimeSelectId(baseId, "minutesUni", calendarDate.typeDate)} .selected-option`).textContent)
: String(document.querySelector(
`div#${datePickerParams.getTimeSelectId(baseId, "minutesDec", calendarDate.typeDate)} .selected-option`).textContent)
+ String(document.querySelector(
`div#${datePickerParams.getTimeSelectId(baseId, "minutesUni", calendarDate.typeDate)} .selected-option`).textContent)
) : name === "Hour" ? parseInt(document.querySelector(
`div#${datePickerParams.getTimeSelectId(baseId, "hours", calendarDate.typeDate)} .selected-option`).textContent)
: parseInt(e.target.textContent)
if(Number.isInteger(value)){
// Store value
dispatch(selectedDateAction[`setCalendar${name}`](value, baseId))
// If day save all selected values (month, year, hour, ...) and change type date or close calendar
if(name === "Day"){
dispatch(selectedDateAction.setDay(value, baseId, calendarDate.typeDate))
dispatch(selectedDateAction.setMonth(calendarDate.month, baseId, calendarDate.typeDate))
dispatch(selectedDateAction.setYear(calendarDate.year, baseId, calendarDate.typeDate))
if(calendarDate.typeDate !== "start"){ dispatch(paramsAction.updateDisplay(datePickerParams.id[baseId].modal, false)) }
let time = false
if(datePickerParams.is[baseId].time){
const hour = parseInt(
document.querySelector(`div#${datePickerParams.getTimeSelectId(baseId, "hours", calendarDate.typeDate)} .selected-option`).textContent)
if(transformToNumber(hour) !== calendarDate.hour){ dispatch(selectedDateAction.setHour(hour, baseId, calendarDate.typeDate)) }
const minutes = String(document.querySelector(`div#${datePickerParams.getTimeSelectId(baseId, "minutesDec", calendarDate.typeDate)} .selected-option`).textContent)
+ String(document.querySelector(`div#${datePickerParams.getTimeSelectId(baseId, "minutesUni", calendarDate.typeDate)} .selected-option`).textContent)
if(transformToNumber(minutes) !== selectedDate.minute){ dispatch(selectedDateAction.setMinute(minutes, baseId, calendarDate.typeDate)) }
time = transformToNumber(hour) + ":" + minutes
}
const dateToVerify = click.getFormattedValue(value, time)
document.getElementById(calendarDate.typeDate && calendarDate.typeDate === "end" ? baseId+"-end" : baseId).value = dateToVerify
datePickerParams.eventFunction.execute(baseId, dateToVerify, "onBlur")
if(datePickerParams.is[baseId].period){ calendarDate.changeIsEndDate() }
} else { click.show(datePickerParams.id[baseId].daySelect, baseId) }
}
dispatch(errorAction.getErrors(baseId))
},
/**
* Provide value in correct format
* @method
* @memberof click
* @param {string} day
* @param {string} time
* @returns {string} date
*/
getFormattedValue: (day, time = false) => {
day = parseInt(day)
const month = parseInt(calendarDate.month)
const year = calendarDate.year
let date = false
switch(validation.formats.lang){
case "de":
date = day+"."+month+"."+year
break
case "es": case "it":
date = day+"-"+month+"-"+year
break
case "fr":
date = transformToNumber(day)+"-"+transformToNumber(month)+"-"+year
break
default:
date = year+"-"+transformToNumber(month)+"-"+transformToNumber(day)
}
return time ? date + " " + time : date
},
/**
* Function to trigger when clicking on a hour
* @method
* @memberof click
* @param {object} e - event
* @see click.fct
*/
hour: (e) => click.fct(e, "Hour"),
/**
* Function to trigger when clicking on a minute
* @method
* @memberof click
* @param {object} e - event
* @see click.fct
*/
minute: (e) => click.fct(e, "Minute"),
/**
* Function to trigger when clicking on a month
* @method
* @memberof click
* @param {object} e - event
* @see click.fct
*/
months: (e) => click.fct(e, "Month"),
/**
* Triggers calendar menu actions
* @method
* @memberof click
* @param {object} e - event
*/
optionSelect: (e) => {
const item = e.target
deleteSelectedDay()
switch(item.getAttribute("id")){
case datePickerParams.id[baseId].nextMonthBtn :
dispatch(selectedDateAction.setCalendarMonth(click.browseMonths(parseInt(calendarDate.month) + 1), baseId, calendarDate.typeDate))
break
case datePickerParams.id[baseId].prevMonthBtn :
dispatch(selectedDateAction.setCalendarMonth(click.browseMonths(parseInt(calendarDate.month) - 1), baseId, calendarDate.typeDate))
break
case datePickerParams.id[baseId].selectedMonth :
click.show(datePickerParams.id[baseId].monthSelect, baseId)
break
case datePickerParams.id[baseId].selectedYear :
click.show(datePickerParams.id[baseId].yearSelect, baseId)
document.getElementById(datePickerParams.id[baseId].yearSelect).scroll(
0, (calendarDate.year - getLimitYear("min"))*8
)
break
case datePickerParams.id[baseId].todayBtn :
dispatch(selectedDateAction.initCalendar(baseId))
break
default: return false
}},
/**
* Displays element corresponding to first parameter
* @method
* @memberof click
* @param {string} elementId
* @param {string} baseId
*/
show: (elementId, baseId) => {
document.querySelector(`#${datePickerParams.id[baseId].modal} .show`).classList.remove("show")
document.getElementById(elementId).classList.add("show")
},
/**
* Function to trigger when clicking on a year
* @method
* @memberof click
* @param {object} e - event
* @see click.fct
*/
years: (e) => click.fct(e, "Year")
}
/**
* Displays the selected date on the calendar
* @param {object} e - event
*/
function displaySelectedDay(e = false) {
const startMonth = parseInt(selectedDate.start.month)
const startYear = parseInt(selectedDate.start.year)
const selectedDayItem = e && e.target
if(startMonth !== calendarDate.month || startYear !== calendarDate.year || calendarDate.typeDate !== "start" ) {
deleteSelectedDay()
}
if(!selectedDate.start.day || calendarDate.typeDate === "start"){ return }
if(calendarDate.month < startMonth && calendarDate.year <= startYear) { return }
const end = parseInt(selectedDayItem ? selectedDayItem.textContent : selectedDate.end.day ? selectedDate.end.day : selectedDate.start.day)
const start = startMonth === calendarDate.month ? parseInt(selectedDate.start.day) - 1 : 0
if(end > start){
const idSplitted = e.target.getAttribute("id").split("-")
const numberId = idSplitted[idSplitted.length - 1]
Array(end - start).fill(0).map((item, index) => {
const element = document.getElementById(`dayLi-${baseId}-${numberId - index}`)
if(element){ element.classList.toggle("selected-day") }
})
}
}
/**
* Highlights the day of the week corresponding to the hovered day of the month
* @param {number} weekdayNumber
* @param {boolean} highlight
*/
function showCorrespondingWeekday(weekdayNumber, highlight = true){
document.getElementById(`wd${baseId}-${weekdays[weekdayNumber]}`).style.opacity = highlight ? 1 : 0.5
document.getElementById(`wd${baseId}-${weekdays[weekdayNumber]}`).style.borderBottom = highlight ? "1px solid" : "none"
}
return(
<Dialog
dialogBoxId={datePickerParams.id[baseId].modal}
name="hrnet-dp-modal"
displayBox={displayBox}
isModal={true}
color={style.color()}
backgroundColor={style.backgroundColor()}
longSize={datePickerParams.is[baseId].dateTime}
>
{(selectedDate.type !== "time" && selectedDate.type !== "timePeriod") && (
<CalendarSection>
<CalendarSelect baseId={baseId} name="option" list={calendarOptions} onClickFunction={click.optionSelect} />
<CalendarBox $name={`display`} id={datePickerParams.id[baseId].calendarDisplayBox} className="date-ctn" data-testid="date-section-test" >
<DateSelect $name={"days"} className="show" id={datePickerParams.id[baseId].daySelect}>
<CalendarList $name="weekdays">
{ weekdays.map( (day) => (
<CalendarListItem key={`wd-${baseId}-${day}`} id={`wd${baseId}-${day}`}>{day}</CalendarListItem>
))}
</CalendarList>
<CalendarList $name="month-days" onClick={click.days}>
{monthDays.map((day, index)=> (
<CalendarListItem
key={`dayLi-${baseId}-${index}`}
id={`dayLi-${baseId}-${index}`}
$type={(index < startDay || index > (monthLength + startDay - 1)) ? ("empty-cell") : ("clickable") }
onMouseOver={(e) => { showCorrespondingWeekday( (index) - (parseInt(index/7)*7) )
datePickerParams.is[baseId].period && displaySelectedDay(e, calendarMonthSelected+1)}}
onMouseOut={(e) => { showCorrespondingWeekday( (index) - (parseInt(index/7)*7), false )
datePickerParams.is[baseId].period && displaySelectedDay(e, calendarMonthSelected+1)}}
>{getNumberDay(index+1, monthLength, startDay)}</CalendarListItem>
))}
</CalendarList>
</DateSelect>
<CalendarSelect baseId={baseId} name={"month"} list={months.name} onClickFunction={click.months} />
<CalendarSelect baseId={baseId} name={"year"} list={years} onClickFunction={click.years} />
</CalendarBox>
</CalendarSection>
)}
{(selectedDate.type !== "date" && selectedDate.type !== "datePeriod") && (
<CalendarSection $flexDirection="row" $name="timeSection" $flexWrap={datePickerParams.is[baseId].period && "wrap"} className={datePickerParams.is[baseId].period ? "time-period" : null}>
{datePickerParams.is[baseId].period && (
<div>
<p>Start</p>
<Suspense fallback={<div>Loading Time Select Component</div>}>
<TimeSelect
baseId={baseId}
maxValue={23}
name={`hoursStart`}
reduceSize={true}
selectedValue={calendarDate.getHours("start")}
onClickFunction={click.hour}
/>
</Suspense>
<div className="time-separator">:</div>
<div className="minutes-ctn">
<Suspense fallback={<div>Loading Time Select Component</div>}>
<TimeSelect
baseId={baseId}
maxValue={5}
name={`minutesDecStart`}
reduceSize={true}
selectedValue={calendarDate.getMinutes("deci", "start")}
onClickFunction={click.minute}
/>
<TimeSelect
baseId={baseId}
name={`minutesUniStart`}
reduceSize={true}
selectedValue={calendarDate.getMinutes("unit", "start")}
onClickFunction={click.minute}
/>
</Suspense>
</div>
</div>
)}
<div data-testid="time-section-test" style={{display: calendarDate.typeHour === "start" && "none"}}>
{datePickerParams.is[baseId].period && (<p>End</p>)}
<Suspense fallback={<div>Loading Time Select Component</div>}>
<TimeSelect
baseId={baseId}
maxValue={23}
name={`hours${calendarDate.nameSuffix}`}
reduceSize={datePickerParams.is[baseId].period}
selectedValue={calendarDate.getHours(datePickerParams.is[baseId].period && "end")}
onClickFunction={click.hour}
/>
</Suspense>
<div className="time-separator">:</div>
<div className="minutes-ctn">
<Suspense fallback={<div>Loading Time Select Component</div>}>
<TimeSelect
baseId={baseId}
maxValue={5}
name={`minutesDec${calendarDate.nameSuffix}`}
reduceSize={datePickerParams.is[baseId].period}
selectedValue={calendarDate.getMinutes("deci", datePickerParams.is[baseId].period && "end")}
onClickFunction={click.minute}
/>
<TimeSelect
baseId={baseId}
name={`minutesUni${calendarDate.nameSuffix}`}
reduceSize={datePickerParams.is[baseId].period}
selectedValue={calendarDate.getMinutes("unit", datePickerParams.is[baseId].period && "end")}
onClickFunction={click.minute}
/>
</Suspense>
</div>
</div>
</CalendarSection>
)}
</Dialog>
)
}
export default Calendar
Source