UNPKG

@onesy/ui-react

Version:
526 lines (523 loc) 19 kB
import _extends from "@babel/runtime/helpers/extends"; import _objectWithoutProperties from "@babel/runtime/helpers/objectWithoutProperties"; import _defineProperty from "@babel/runtime/helpers/defineProperty"; const _excluded = ["date", "times", "events", "onOpen", "onTimeClick", "render", "statuses", "displayTime", "day", "className", "children"]; function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; } function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; } import React from 'react'; import { cleanValue, is, textToInnerHTML } from '@onesy/utils'; import { style as styleMethod, classNames, useOnesyTheme, colors } from '@onesy/style-react'; import { OnesyDate, add, endOf, format, set, startOf } from '@onesy/date'; import LineElement from '../Line'; import TypeElement from '../Type'; import TooltipElement from '../Tooltip'; import { formats, staticClassName } from '../utils'; const useStyle = styleMethod(theme => ({ root: { color: theme.methods.palette.color.value('primary', 10), background: theme.palette.background.default.primary, paddingLeft: '12px' }, weekDay: { width: '44px', height: '44px', borderRadius: '50%' }, today: { background: theme.palette.color.primary[40], color: '#fff' }, days: { overflow: 'auto hidden' }, day: { minWidth: '184px', height: '134vh', width: 'calc((100% / 7) - 106px)' }, dayBody: { position: 'relative', borderLeft: `1px solid ${theme.palette.light ? '#dadada' : '#575757'}` }, dayHeader: { height: '78px', padding: '4px' }, hours: { position: 'sticky', top: '0px', left: '0px', width: '44px', height: '134vh', flex: '0 0 auto', marginRight: '8px', background: theme.palette.background.default.primary, zIndex: 14 }, hourWrapper: { position: 'relative' }, hour: { width: '50px', alignSelf: 'flex-start', left: '0px', position: 'absolute', top: '0', marginTop: '-8px', zIndex: '40', borderRadius: theme.methods.shape.radius.value(0.5) }, guide: { position: 'absolute', left: '-9px', height: '1px', width: 'calc(100% + 9px)', background: theme.palette.light ? '#dadada' : '#575757' }, guidelineHour: { position: 'absolute', left: '0px', height: '2px', transform: 'translateY(-50%)', background: theme.palette.color.tertiary[50], zIndex: 14, '&::before': { position: 'absolute', content: "''", width: '12px', height: '12px', borderRadius: '50%', background: theme.palette.color.tertiary[50], top: '-5px', left: '-6.5px' } }, range: { position: 'absolute', left: 0, right: 0, cursor: 'pointer', zIndex: 1, transition: theme.methods.transitions.make('box-shadow'), overflow: 'hidden', '& > *': { whiteSpace: 'pre-wrap', transition: theme.methods.transitions.make('opacity') }, '& > .onesy-work-day-time': { opacity: 0 }, '&:hover': { zIndex: 14, boxShadow: theme.shadows.values.default[2], '& > .onesy-work-day-time': { opacity: 1 } } }, time: { position: 'relative', zIndex: 1, padding: '0px 2px', borderRadius: 2 } }), { name: 'onesy-CalendarWeek' }); const CalendarWeek = /*#__PURE__*/React.forwardRef((props_, ref) => { const theme = useOnesyTheme(); const l = theme.l; const props = React.useMemo(() => _objectSpread(_objectSpread(_objectSpread({}, theme?.ui?.elements?.all?.props?.default), theme?.ui?.elements?.onesyCalendarWeek?.props?.default), props_), [props_]); const Line = React.useMemo(() => theme?.elements?.Line || LineElement, [theme]); const Type = React.useMemo(() => theme?.elements?.Type || TypeElement, [theme]); const Tooltip = React.useMemo(() => theme?.elements?.Tooltip || TooltipElement, [theme]); const { date, times: timesProps, events, onOpen, onTimeClick, render, statuses = {}, displayTime = true, day: dayProp, className, children } = props, other = _objectWithoutProperties(props, _excluded); const { classes } = useStyle(); const [now, setNow] = React.useState(new OnesyDate()); const refs = { date: React.useRef(date), displayTime: React.useRef(displayTime), interval: React.useRef(undefined), days: React.useRef({}), overlaping: React.useRef({}), statuses: React.useRef(statuses) }; refs.date.current = date; refs.displayTime.current = displayTime; refs.statuses.current = statuses; const times = React.useMemo(() => { if (events) { return [{ dates: { active: true, values: (is('array', events) ? events : [events]).filter(Boolean) } }]; } return (is('array', timesProps) ? timesProps : [timesProps]).filter(Boolean); }, [events, timesProps]); const rangeShade = theme.palette.light ? 60 : 40; React.useEffect(() => { // 1 minute refs.interval.current = setInterval(() => { setNow(new OnesyDate()); }, 60 * 1e3); return () => { clearInterval(refs.interval.current); }; }, []); const getDates = React.useCallback(available => { const values = available.dates?.values || []; return values.map(item => { if (item.entire) { if (item.from) { let from = new OnesyDate(item.from); let to; if (['day', 'week', 'month', 'year'].includes(item.entire)) from = startOf(from, 'day'); if (item.entire === 'minute') from = startOf(from, 'minute'); if (item.entire === 'hour') from = startOf(from, 'hour'); to = endOf(from, item.entire); item.from = from.milliseconds; item.to = to.milliseconds; } } return item; }); }, [times]); const optionsStatus = React.useMemo(() => { return [{ name: l('Working'), value: 'working' }, { name: l('Not working'), value: 'not-working' }, { name: l('On a break'), value: 'break' }, { name: l('Scheduled'), value: 'pending' }, { name: l('Rescheduled'), value: 'rescheduled' }, { name: l('Cancelled'), value: 'canceled' }, { name: l('Other'), value: 'other' }]; }, []); const getColor = React.useCallback(item => { let palette = theme.palette.color.neutral; if (item?.status === 'working') palette = theme.palette.color.success; if (item?.status === 'not-working') palette = theme.palette.color.info; if (item?.status === 'break') palette = theme.palette.color.warning; if (item?.status === 'pending') palette = theme.methods.color(colors.yellow[50]); if (item?.status === 'rescheduled') palette = theme.methods.color(colors.purple[50]); if (item?.status === 'canceled') palette = theme.palette.color.error; if (item?.status === 'other') palette = theme.palette.color.neutral; if (item.color) palette = theme.methods.color(item.color); return palette[rangeShade]; }, [rangeShade, colors, theme]); const itemToText = React.useCallback(item => { if (item === 'pending') return l('Scheduled'); if (item === 'not-count-workout-session') return l(`Don't count workout session`); return optionsStatus?.find(itemStatus => itemStatus.value === item)?.name ?? l(item); }, []); const renderTimes = function (day, valuesAll) { let weekly = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : true; let itemDay = arguments.length > 3 ? arguments[3] : undefined; if (itemDay !== undefined && !itemDay?.active) return null; if (weekly) { const ends_at = itemDay?.ends_at ? new OnesyDate(itemDay.ends_at) : undefined; if (ends_at) { const day_StartDay = startOf(day, 'day'); const ends_at_StartDay = startOf(ends_at, 'day'); if (day_StartDay.milliseconds >= ends_at_StartDay.milliseconds) return null; } } const dayStartOfDay = startOf(day, 'day'); const dayDate = format(day, formats.date); const values = valuesAll?.filter(item => { const from = new OnesyDate(item.from); const fromStartOfDay = startOf(from, 'day'); const to = new OnesyDate(item.to); const toStartOfDay = startOf(to, 'day'); return [undefined, true].includes(refs.statuses.current[item.status || 'working']) && (weekly ? weekly : // from (dayDate === format(from, formats.date) || dayStartOfDay.milliseconds > fromStartOfDay.milliseconds) && ( // to dayDate === format(to, formats.date) || dayStartOfDay.milliseconds < toStartOfDay.milliseconds)); }); const elements = []; const renderTo = valueNew => { if (valueNew === '23:59') return '24:00'; return valueNew; }; values?.forEach((item, index) => { if (!(item.from && item.to)) return; let from = new OnesyDate(item.from); let to = new OnesyDate(item.to); if (!weekly) { const fromStartOfDay = startOf(from, 'day'); const toStartOfDay = startOf(to, 'day'); if (dayStartOfDay?.milliseconds > fromStartOfDay?.milliseconds && (dayDate === format(to, formats.date) || dayStartOfDay.milliseconds < toStartOfDay.milliseconds)) item.from = startOf(from, 'day').milliseconds; if ((dayDate === format(from, formats.date) || dayStartOfDay.milliseconds > fromStartOfDay.milliseconds) && dayStartOfDay?.milliseconds < toStartOfDay?.milliseconds) item.to = endOf(to, 'day').milliseconds; } from = new OnesyDate(item.from); to = new OnesyDate(item.to); const itemDate = format(day, formats.date); const top = +(100 * ((from.hour * 60 + from.minute) / (24 * 60))).toFixed(4); const bottom = +(100 - 100 * ((to.hour * 60 + (to.minute === 59 ? 60 : to.minute)) / (24 * 60))).toFixed(4); if (!refs.days.current[itemDate]) refs.days.current[itemDate] = []; if (!refs.overlaping.current[itemDate]) refs.overlaping.current[itemDate] = []; const bottom_ = 100 - bottom; // intersections const overlaps = refs.days.current[itemDate].filter(_ref => { let [itemTop, itemBottom] = _ref; return !(top >= itemBottom || bottom_ <= itemTop); }); let level = 0; if (overlaps.length) { level = refs.overlaping.current[itemDate].filter(_ref2 => { let [itemTop, itemBottom] = _ref2; return !(top >= itemBottom || bottom_ <= itemTop); }).length + 1; refs.overlaping.current[itemDate].push([top, bottom_]); } refs.days.current[itemDate].push([top, bottom_]); const minimal = 100 - bottom - top < 5; const background = getColor(item); const WrapperElement = item.status ? Tooltip : React.Fragment; const WrapperElementProps = item.status ? { name: cleanValue(itemToText(item.status), { capitalize: true }), color: getColor(item) } : undefined; const itemProps = { onClick: event => onOpen(_objectSpread(_objectSpread({}, item), {}, { day, weekly }), event), className: classes.range }; const style = _objectSpread({ top: `${top}%`, bottom: `${bottom}%`, color: theme.methods.palette.color.text(background), background, left: `calc(0px + ${level * 10}px)` }, top === 0 && bottom === 0 && { border: 'none' }); const elementRendered = is('function', render) ? render(item, dayProp ? 'day' : 'week') : /*#__PURE__*/React.createElement(Line, _extends({ key: index, gap: 0.5, align: "center", justify: "center" }, itemProps), !minimal && refs.displayTime.current && /*#__PURE__*/React.createElement(Type, { version: "l2", weight: 300, align: "center", className: classes.time, style: { background: getColor(item) } }, format(from, 'hh:mm a', { l }), " - ", renderTo(format(to, 'hh:mm a', { l }))), item.description && /*#__PURE__*/React.createElement(Type, { version: "b2", dangerouslySetInnerHTML: { __html: textToInnerHTML(item.description) }, style: { maxHeight: 24, maxWidth: '90%' } })); elements.push(/*#__PURE__*/React.createElement(WrapperElement, WrapperElementProps, /*#__PURE__*/React.cloneElement(elementRendered, _objectSpread(_objectSpread({}, itemProps), {}, { className: classNames([itemProps.className, elementRendered.props?.className]), style: _objectSpread(_objectSpread({}, style), elementRendered.props?.style) })))); }); return elements; }; const onTimeClickMethod = React.useCallback((itemDay, event) => { const rect = event.currentTarget.getBoundingClientRect(); const relativeY = event.clientY - rect.top; const relativePercentage = relativeY / rect.height * 100; let timeDate = set(Math.floor(24 * (relativePercentage / 100)), 'hour', itemDay); // start of the hour timeDate = startOf(timeDate, 'hour'); onTimeClick?.(timeDate, dayProp ? 'day' : 'week', event); }, []); const timesUI = React.useCallback(dayDate => { // clean up refs.days.current = {}; refs.overlaping.current = {}; const day = dayDate.dayWeek === 0 ? 7 : dayDate.dayWeek; return /*#__PURE__*/React.createElement(React.Fragment, null, times.map(item => /*#__PURE__*/React.createElement(React.Fragment, null, item.weekly && renderTimes(dayDate, item.weekly.days[day]?.values, true, item.weekly.days[day]), item.dates && renderTimes(dayDate, getDates(item), false)))); }, [theme, times, date]); const hours = React.useMemo(() => { return Array.from({ length: 24 }).map((item, index) => set(index, 'hour', date)); }, [date]); const days = React.useMemo(() => { const weekStartDate = set(4, 'hour', startOf(date, 'week')); return Array.from({ length: 7 }).map((_, index) => add(index, 'day', weekStartDate)); }, [date]); return /*#__PURE__*/React.createElement(Line, _extends({ ref: ref, gap: 1.5, flex: true, fullWidth: true, className: classNames([staticClassName('CalendarWeek', theme) && ['onesy-CalendarWeek-root', dayProp && 'onesy-CalendarWeek-prop-day'], className, classes.root]) }, other), /*#__PURE__*/React.createElement(Line, { gap: 0, direction: "row", align: "stretch", flex: true, fullWidth: true, className: classes.days }, /*#__PURE__*/React.createElement(Line, { gap: 0, align: "center", fullWidth: true, className: classNames(['onesy-hours', classes.hours]) }, /*#__PURE__*/React.createElement(Line, { className: classes.dayHeader }), /*#__PURE__*/React.createElement(Line, { gap: 0, flex: true, fullWidth: true, className: classes.hourWrapper }, hours.map((itemHour, index) => /*#__PURE__*/React.createElement(Line, { key: index, gap: 1, align: "unset", className: classes.hour, style: { top: `${100 / 24 * index}%` } }, /*#__PURE__*/React.createElement(Type, { version: "b3", whiteSpace: "nowrap" }, format(itemHour, 'h A', { l })))))), dayProp && /*#__PURE__*/React.createElement(Line, { gap: 0, direction: "column", align: "center", flex: true, className: classNames(['onesy-day', classes.day]) }, /*#__PURE__*/React.createElement(Line, { gap: 0, align: "center", fullWidth: true, className: classes.dayHeader }, /*#__PURE__*/React.createElement(Type, { version: "l1", weight: 200 }, format(date, 'd', { l })), /*#__PURE__*/React.createElement(Line, { align: "center", justify: "center", className: classNames([classes.weekDay, date.year === now.year && date.dayYear === now.dayYear && classes.today]) }, /*#__PURE__*/React.createElement(Type, { version: "h3", weight: 400, align: "center" }, format(date, 'D', { l })))), /*#__PURE__*/React.createElement(Line, { className: classes.dayBody, flex: true, fullWidth: true, onClick: event => onTimeClickMethod(date, event) }, timesUI(date), hours.map((itemGuide, indexGuide) => /*#__PURE__*/React.createElement(Line, { key: indexGuide, className: classes.guide, fullWidth: true, style: { top: `${100 / 24 * indexGuide}%` } })), format(now, formats.date) === format(date, formats.date) && /*#__PURE__*/React.createElement(Line, { className: classes.guidelineHour, fullWidth: true, style: { top: `${(now.hour * 60 + now.minute) / (24 * 60) * 100}%` } }))), !dayProp && days.map((itemDay, index) => { return /*#__PURE__*/React.createElement(Line, { key: index, gap: 0, direction: "column", align: "center", flex: true, className: classNames(['onesy-day', classes.day]) }, /*#__PURE__*/React.createElement(Line, { gap: 0, align: "center", fullWidth: true, className: classes.dayHeader }, /*#__PURE__*/React.createElement(Type, { version: "l1", weight: 200 }, format(itemDay, 'd', { l })), /*#__PURE__*/React.createElement(Line, { align: "center", justify: "center", className: classNames([classes.weekDay, itemDay.year === now.year && itemDay.dayYear === now.dayYear && classes.today]) }, /*#__PURE__*/React.createElement(Type, { version: "h3", weight: 400, align: "center" }, format(itemDay, 'D', { l })))), /*#__PURE__*/React.createElement(Line, { className: classes.dayBody, flex: true, fullWidth: true, onClick: event => onTimeClickMethod(itemDay, event) }, timesUI(itemDay), hours.map((itemGuide, indexGuide) => /*#__PURE__*/React.createElement(Line, { key: indexGuide, className: classes.guide, fullWidth: true, style: { top: `${100 / 24 * indexGuide}%` } })), format(now, formats.date) === format(itemDay, formats.date) && /*#__PURE__*/React.createElement(Line, { className: classes.guidelineHour, fullWidth: true, style: { top: `${(now.hour * 60 + now.minute) / (24 * 60) * 100}%` } }))); }))); }); CalendarWeek.displayName = 'onesy-CalendarWeek'; export default CalendarWeek;