import { React, classnames, ReactCalendar, PropTypes } from '@gravityforms/libraries';
import { useStateWithDep } from '@gravityforms/react-utils';
import { spacerClasses, deepMerge } from '@gravityforms/utils';
import { LeftArrow, RightArrow, DoubleLeftArrow, DoubleRightArrow } from './Icons';
import Button from '../../elements/Button';
const { forwardRef, useRef, useEffect, Fragment } = React;
/**
* @constant
* @type {object}
* @description Default attributes for the ReactCalendar component.
*
* @since 4.4.0
*
* @property {string|JSX.Element} nextLabel The next button label.
* @property {string|JSX.Element} next2Label The next2 button label.
* @property {Function} onChange The onChange event handler.
* @property {string|JSX.Element} prevLabel The previous button label.
* @property {string|JSX.Element} prev2Label The previous2 button label.
*/
const defaultCalendarAttributes = {
maxDetail: 'month',
nextLabel: <RightArrow />,
next2Label: <DoubleRightArrow />,
onActiveStartDateChange: () => {},
onChange: () => {},
onViewChange: () => {},
prevLabel: <LeftArrow />,
prev2Label: <DoubleLeftArrow />,
};
/**
* @function getTodayStartOfDay
* @description Get the date object of the start of day for today.
*
* @since 4.4.0
*
* @return {Date} The date object of the start of day for today.
*/
const getTodayStartOfDay = () => {
const date = new Date();
date.setHours( 0, 0, 0, 0 );
return date;
};
/**
* @function getTodayEndOfDay
* @description Get the date object of the end of day for today.
*
* @since 4.4.0
*
* @return {Date} The date object of the end of day for today.
*/
const getTodayEndOfDay = () => {
const date = new Date();
date.setHours( 23, 59, 59, 999 );
return date;
};
/**
* @function getStart
* @description Get the start date based on range type.
*
* @since 4.4.0
*
* @param {string} rangeType The range type, one of `century`, `decade`, `year`, or `month`.
* @param {Date} date The date object.
*
* @return {Date} The start of range type date object.
*/
const getStart = ( rangeType, date ) => {
let year = date.getFullYear();
let month = 0;
switch ( rangeType ) {
case 'century':
year = year + ( ( -year + 1 ) % 100 );
break;
case 'decade':
year = year + ( ( -year + 1 ) % 10 );
break;
case 'year':
break;
case 'month':
month = date.getMonth();
break;
default:
throw new Error( `Invalid rangeType: ${ rangeType }` );
}
const begin = new Date();
begin.setFullYear( year, month, 1 );
begin.setHours( 0, 0, 0, 0 );
return begin;
};
/**
* @module Calendar
* @description A calendar component. The calendar is run in controlled mode to allow for the today button to work.
*
* @since 4.4.0
*
* @param {object} props Component props.
* @param {object} props.calendarAttributes Custom attributes for the calendar.
* @param {string|Array|object} props.calendarClasses Custom classes for the calendar.
* @param {object} props.customAttributes Custom attributes for the component.
* @param {string|Array|object} props.customClasses Custom classes for the component.
* @param {Function} props.onTodayClick The click event handler for the today button.
* @param {boolean} props.showTodayButton Whether to show the today button or not.
* @param {string|number|Array|object} props.spacing The spacing for the component, as a string, number, array, or object.
* @param {boolean} props.triggerActive Whether the trigger is active or not.
* @param {object} props.triggerAttributes Custom attributes for the trigger button.
* @param {string|Array|object} props.triggerClasses Custom classes for the trigger button.
* @param {boolean} props.withTrigger Whether to show the trigger button or not.
*
* @return {object} The Calendar component.
*
* @example
* import Calendar from '@gravityforms/components/react/admin/modules/Calendar';
*
* return <Calendar />;
*
*/
const Calendar = forwardRef( ( {
calendarAttributes = {},
calendarClasses = [],
customAttributes = {},
customClasses = [],
onTodayClick = () => {},
showTodayButton = true,
spacing = '',
triggerActive = false,
triggerAttributes = {},
triggerClasses = [],
withTrigger = false,
}, ref ) => {
const [ activeStartDate, setActiveStartDate ] = useStateWithDep( calendarAttributes.activeStartDate || null );
const [ value, setValue ] = useStateWithDep( calendarAttributes.value || null );
const [ view, setView ] = useStateWithDep( calendarAttributes.view || null );
const [ triggerIsActive, setTriggerIsActive ] = useStateWithDep( triggerActive );
// Refs
const calendarRef = useRef( null );
const triggerRef = useRef( null );
const mergedCalendarAttributes = deepMerge( defaultCalendarAttributes, calendarAttributes );
const maxDetail = mergedCalendarAttributes.maxDetail || 'month';
const calendarWrapperProps = {
className: classnames( {
'gform-calendar': true,
'gform-calendar--with-trigger': withTrigger,
'gform-calendar--trigger-active': triggerIsActive,
...( withTrigger ? {} : spacerClasses( spacing ) ),
}, customClasses ),
...customAttributes,
};
const adjustCalendarPosition = () => {
if ( triggerRef.current && calendarRef.current ) {
calendarRef.current.style.marginInlineStart = 0;
const calendarRect = calendarRef.current.getBoundingClientRect();
const viewportWidth = window.innerWidth;
let marginInlineStart = 0;
if ( ( calendarRect.right + 20 ) - viewportWidth >= 0 ) {
marginInlineStart = -( ( calendarRect.right + 20 ) - viewportWidth );
}
calendarRef.current.style.marginInlineStart = `${ marginInlineStart }px`;
}
};
useEffect( () => {
if ( withTrigger && triggerIsActive ) {
adjustCalendarPosition();
// eslint-disable-next-line jsdoc/require-description
/**
*
* @param event
*/
// eslint-disable-next-line no-inner-declarations
function handleClickOutside( event ) {
if (
calendarRef.current &&
! calendarRef.current.contains( event.target ) &&
triggerRef.current &&
! triggerRef.current.contains( event.target )
) {
setTriggerIsActive( false );
}
}
document.addEventListener( 'mousedown', handleClickOutside );
window.addEventListener( 'resize', adjustCalendarPosition );
return () => {
document.removeEventListener( 'mousedown', handleClickOutside );
window.removeEventListener( 'resize', adjustCalendarPosition );
};
}
}, [ withTrigger, triggerIsActive ] );
const resetControl = ( args ) => {
if ( activeStartDate ) {
setActiveStartDate( null );
}
setView( args.view );
};
const calendarProps = deepMerge( defaultCalendarAttributes, {
className: classnames( [
'gform-calendar__calendar',
], calendarClasses ),
...mergedCalendarAttributes,
activeStartDate,
onActiveStartDateChange: ( args ) => {
if ( [ 'drillDown', 'drillUp' ].includes( args.action ) ) {
return;
}
resetControl( args );
mergedCalendarAttributes.onActiveStartDateChange( args );
},
onChange: ( newValue, event ) => {
setValue( newValue );
mergedCalendarAttributes.onChange( newValue, event );
},
onViewChange: ( args ) => {
resetControl( args );
mergedCalendarAttributes.onViewChange( args );
},
value,
view,
} );
const todayButtonProps = {
label: 'Today',
onClick: () => {
const today = new Date();
const newValue = calendarProps.selectRange
? [ getTodayStartOfDay(), getTodayEndOfDay() ]
: today;
setActiveStartDate( today );
setView( maxDetail );
onTodayClick();
calendarProps.onChange( newValue );
if ( view !== maxDetail ) {
calendarProps.onViewChange( {
action: 'drillDown',
activeStartDate: getStart( maxDetail, today ),
value: today,
view: maxDetail,
} );
}
},
size: 'size-height-s',
type: 'white',
};
const triggerProps = {
className: classnames( 'gform-calendar__trigger', triggerClasses ),
onClick: () => {
setTriggerIsActive( ! triggerIsActive );
},
size: 'size-height-m',
type: 'white',
...triggerAttributes,
};
const outerWrapperProps = {
className: classnames( {
'gform-calendar__wrapper': true,
'gform-calendar__wrapper--trigger-active': triggerIsActive,
...( withTrigger ? spacerClasses( spacing ) : {} ),
} ),
};
const OuterWrapper = withTrigger ? 'div' : Fragment;
return (
<OuterWrapper { ...( OuterWrapper === 'div' ? outerWrapperProps : {} ) }>
{ withTrigger && <Button { ...triggerProps } ref={ triggerRef } /> }
<div { ...calendarWrapperProps } ref={ ( node ) => {
calendarRef.current = node;
if ( typeof ref === 'function' ) {
ref( node );
} else if ( ref ) {
ref.current = node;
}
} }>
<ReactCalendar.Calendar { ...calendarProps } />
{ showTodayButton && (
<div className="gform-calendar__footer">
<Button { ...todayButtonProps } />
</div>
) }
</div>
</OuterWrapper>
);
} );
Calendar.propTypes = {
calendarAttributes: PropTypes.object,
calendarClasses: PropTypes.oneOfType( [
PropTypes.string,
PropTypes.array,
PropTypes.object,
] ),
customAttributes: PropTypes.object,
customClasses: PropTypes.oneOfType( [
PropTypes.string,
PropTypes.array,
PropTypes.object,
] ),
onTodayClick: PropTypes.func,
showTodayButton: PropTypes.bool,
spacing: PropTypes.oneOfType( [
PropTypes.string,
PropTypes.number,
PropTypes.array,
PropTypes.object,
] ),
triggerActive: PropTypes.bool,
triggerAttributes: PropTypes.object,
triggerClasses: PropTypes.oneOfType( [
PropTypes.string,
PropTypes.array,
PropTypes.object,
] ),
withTrigger: PropTypes.bool,
};
export default Calendar;