From 805ec3aceada66310274199a308c45102fbc7900 Mon Sep 17 00:00:00 2001 From: Andy Cork Date: Fri, 21 Feb 2025 15:36:15 +0000 Subject: [PATCH 01/15] Allow an event to have its length changed with a drag handle --- src/events.tsx | 13 + src/lib/components/events/EventItem.tsx | 48 ++- src/lib/components/events/TodayEvents.tsx | 2 +- src/lib/components/week/WeekTable.tsx | 16 +- src/lib/helpers/generals.tsx | 5 + src/lib/hooks/useEventPermissions.ts | 23 +- src/lib/hooks/useResizeAttributes.ts | 50 +++ src/lib/store/default.ts | 4 + src/lib/store/provider.tsx | 397 +++++++++++++--------- src/lib/store/types.ts | 8 + src/lib/styles/styles.ts | 11 + src/lib/types.ts | 14 + 12 files changed, 424 insertions(+), 167 deletions(-) create mode 100644 src/lib/hooks/useResizeAttributes.ts diff --git a/src/events.tsx b/src/events.tsx index f28a57b3..56fc62fa 100644 --- a/src/events.tsx +++ b/src/events.tsx @@ -148,6 +148,19 @@ export const EVENTS: ProcessedEvent[] = [ }), color: "#dc4552", }, + { + event_id: 11, + title: "Event 11", + subtitle: "This event is not resizable", + start: new Date( + new Date(new Date(new Date().setHours(10)).setMinutes(30)).setDate(new Date().getDate() - 4) + ), + end: new Date( + new Date(new Date(new Date().setHours(12)).setMinutes(30)).setDate(new Date().getDate() - 4) + ), + admin_id: 1, + resizable: false, + }, ]; export const RESOURCES = [ diff --git a/src/lib/components/events/EventItem.tsx b/src/lib/components/events/EventItem.tsx index 69abfa75..5ddfbb15 100644 --- a/src/lib/components/events/EventItem.tsx +++ b/src/lib/components/events/EventItem.tsx @@ -1,15 +1,16 @@ -import { Fragment, MouseEvent, useCallback, useMemo, useState } from "react"; -import { Typography, ButtonBase, useTheme } from "@mui/material"; +import { Fragment, MouseEvent, useCallback, useMemo, useRef, useState } from "react"; +import { Typography, ButtonBase, useTheme, Popper } from "@mui/material"; import { format } from "date-fns"; import { ProcessedEvent } from "../../types"; import ArrowRightRoundedIcon from "@mui/icons-material/ArrowRightRounded"; import ArrowLeftRoundedIcon from "@mui/icons-material/ArrowLeftRounded"; -import { EventItemPaper } from "../../styles/styles"; +import { DragHandle, EventItemPaper } from "../../styles/styles"; import { differenceInDaysOmitTime, getHourFormat } from "../../helpers/generals"; import useStore from "../../hooks/useStore"; import useDragAttributes from "../../hooks/useDragAttributes"; import EventItemPopover from "./EventItemPopover"; import useEventPermissions from "../../hooks/useEventPermissions"; +import useResizeAttributes from "../../hooks/useResizeAttributes"; interface EventItemProps { event: ProcessedEvent; @@ -17,12 +18,27 @@ interface EventItemProps { hasPrev?: boolean; hasNext?: boolean; showdate?: boolean; + minuteHeight?: number; } -const EventItem = ({ event, multiday, hasPrev, hasNext, showdate = true }: EventItemProps) => { +const EventItem = ({ + event, + multiday, + hasPrev, + hasNext, + showdate = true, + minuteHeight, +}: EventItemProps) => { const { direction, locale, hourFormat, eventRenderer, onEventClick, view, disableViewer } = useStore(); + const dragHandleRef = useRef(null); + const [dragTime, setDragTime] = useState(); + const onDragMove = useCallback((time: Date | undefined) => { + setDragTime(time); + }, []); + const dragProps = useDragAttributes(event); + const resizeProps = useResizeAttributes(event, minuteHeight, onDragMove); const [anchorEl, setAnchorEl] = useState(null); const [deleteConfirm, setDeleteConfirm] = useState(false); const theme = useTheme(); @@ -32,7 +48,11 @@ const EventItem = ({ event, multiday, hasPrev, hasNext, showdate = true }: Event const PrevArrow = direction === "rtl" ? ArrowRightRoundedIcon : ArrowLeftRoundedIcon; const hideDates = differenceInDaysOmitTime(event.start, event.end) <= 0 && event.allDay; - const { canDrag } = useEventPermissions(event); + const { canDrag, canResize } = useEventPermissions(event); + const resizable = useMemo( + () => !!canResize && !!minuteHeight && !multiday, + [canResize, minuteHeight, multiday] + ); const triggerViewer = useCallback( (el?: MouseEvent) => { @@ -53,6 +73,7 @@ const EventItem = ({ event, multiday, hasPrev, hasNext, showdate = true }: Event return ( {custom} + ); } @@ -124,9 +145,7 @@ const EventItem = ({ event, multiday, hasPrev, hasNext, showdate = true }: Event if (!disableViewer) { triggerViewer(e); } - if (typeof onEventClick === "function") { - onEventClick(event); - } + onEventClick?.(event); }} focusRipple tabIndex={disableViewer ? -1 : 0} @@ -137,6 +156,7 @@ const EventItem = ({ event, multiday, hasPrev, hasNext, showdate = true }: Event {item} + ); }, [ @@ -159,6 +179,8 @@ const EventItem = ({ event, multiday, hasPrev, hasNext, showdate = true }: Event hasNext, NextArrow, onEventClick, + resizable, + resizeProps, ]); return ( @@ -167,6 +189,16 @@ const EventItem = ({ event, multiday, hasPrev, hasNext, showdate = true }: Event {/* Viewer */} + + + {dragTime ? format(dragTime, hFormat, { locale }) : null} + + ); }; diff --git a/src/lib/components/events/TodayEvents.tsx b/src/lib/components/events/TodayEvents.tsx index 8b20b87b..8d16bd55 100644 --- a/src/lib/components/events/TodayEvents.tsx +++ b/src/lib/components/events/TodayEvents.tsx @@ -80,7 +80,7 @@ const TodayEvents = ({ : "", }} > - + ); })} diff --git a/src/lib/components/week/WeekTable.tsx b/src/lib/components/week/WeekTable.tsx index 510604b1..46a26813 100644 --- a/src/lib/components/week/WeekTable.tsx +++ b/src/lib/components/week/WeekTable.tsx @@ -6,6 +6,7 @@ import { filterMultiDaySlot, filterTodayEvents, getHourFormat, + preventDragEvent, } from "../../helpers/generals"; import { MULTI_DAY_EVENT_HEIGHT } from "../../helpers/constants"; import { DefaultResource, ProcessedEvent } from "../../types"; @@ -119,7 +120,13 @@ const WeekTable = ({ overflowX: "hidden", }} > - + ); }); @@ -170,7 +177,12 @@ const WeekTable = ({ const end = addMinutes(start, step); const field = resourceFields.idField; return ( - + {/* Events of each day - run once on the top hour column */} {i === 0 && ( ): View => { if (state.month) { @@ -126,6 +127,10 @@ export const differenceInDaysOmitTime = (start: Date, end: Date) => { return differenceInDays(endOfDay(addSeconds(end, -1)), startOfDay(start)); }; +export const preventDragEvent = (ev: DragEvent) => { + ev.preventDefault(); +}; + export const convertDateToRRuleDate = (date: Date) => { return datetime( date.getFullYear(), diff --git a/src/lib/hooks/useEventPermissions.ts b/src/lib/hooks/useEventPermissions.ts index 268f09ab..9f7657c1 100644 --- a/src/lib/hooks/useEventPermissions.ts +++ b/src/lib/hooks/useEventPermissions.ts @@ -2,8 +2,15 @@ import { useMemo } from "react"; import { ProcessedEvent } from "../types"; import useStore from "./useStore"; -const useEventPermissions = (event: ProcessedEvent) => { - const { editable, deletable, draggable } = useStore(); +type UseEventPermissions = { + canEdit?: boolean; + canDelete?: boolean; + canDrag?: boolean; + canResize?: boolean; +}; + +const useEventPermissions = (event: ProcessedEvent): UseEventPermissions => { + const { editable, deletable, draggable, resizable } = useStore(); const canEdit = useMemo(() => { // Priority control to event specific editable value @@ -32,10 +39,22 @@ const useEventPermissions = (event: ProcessedEvent) => { return draggable; }, [draggable, event.draggable, canEdit]); + const canResize = useMemo(() => { + if (!canEdit) { + return; + } + // Priority control to event specific draggable value + if (typeof event.resizable !== "undefined") { + return event.resizable; + } + return resizable; + }, [resizable, event.resizable, canEdit]); + return { canEdit, canDelete, canDrag, + canResize, }; }; diff --git a/src/lib/hooks/useResizeAttributes.ts b/src/lib/hooks/useResizeAttributes.ts new file mode 100644 index 00000000..32e485ec --- /dev/null +++ b/src/lib/hooks/useResizeAttributes.ts @@ -0,0 +1,50 @@ +import { DragEvent, useMemo } from "react"; +import { ProcessedEvent } from "../types"; +import useStore from "./useStore"; + +const img = new Image(); +img.style.pointerEvents = "none"; +img.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="; + +const useResizeAttributes = ( + event: ProcessedEvent, + minuteHeight?: number, + onDragMove?: (time: Date | undefined) => void +) => { + const { setCurrentResize, onResize, onResizeEnd } = useStore(); + const handlers = useMemo( + () => + minuteHeight + ? { + draggable: true, + onDragStart: (e: DragEvent) => { + e.stopPropagation(); + setCurrentResize(event); + e.dataTransfer.setDragImage(img, 0, 0); + }, + onDragEnd: (e: DragEvent) => { + setCurrentResize(); + onResizeEnd(e, event, minuteHeight); + onDragMove?.(undefined); + }, + onDrag: (e: DragEvent) => { + e.stopPropagation(); + e.preventDefault(); + onDragMove?.(onResize(e, event, minuteHeight)); + }, + onDragOver: (e: DragEvent) => { + e.stopPropagation(); + e.preventDefault(); + }, + onDragEnter: (e: DragEvent) => { + e.stopPropagation(); + e.preventDefault(); + }, + } + : { draggable: false }, + [event, minuteHeight, onResize, setCurrentResize, onResizeEnd, onDragMove] + ); + return handlers; +}; + +export default useResizeAttributes; diff --git a/src/lib/store/default.ts b/src/lib/store/default.ts index fc06a39a..cc92c7a2 100644 --- a/src/lib/store/default.ts +++ b/src/lib/store/default.ts @@ -130,6 +130,7 @@ export const defaultProps = (props: Partial) => { locale: enUS, deletable: true, editable: true, + resizable: true, hourFormat: "12", draggable: true, agenda, @@ -155,5 +156,8 @@ export const initialStore = { handleGotoDay: () => {}, confirmEvent: () => {}, setCurrentDragged: () => {}, + setCurrentResize: () => {}, onDrop: () => {}, + onResize: () => undefined, + onResizeEnd: () => {}, }; diff --git a/src/lib/store/provider.tsx b/src/lib/store/provider.tsx index e095f241..3786bac0 100644 --- a/src/lib/store/provider.tsx +++ b/src/lib/store/provider.tsx @@ -1,4 +1,4 @@ -import { DragEvent, useEffect, useState } from "react"; +import { DragEvent, useCallback, useEffect, useMemo, useState } from "react"; import { EventActions, ProcessedEvent, SchedulerProps } from "../types"; import { defaultProps, initialStore } from "./default"; import { StoreContext } from "./context"; @@ -20,19 +20,23 @@ export const StoreProvider = ({ children, initial }: Props) => { ...prev, onEventDrop: initial.onEventDrop, customEditor: initial.customEditor, + onEventResize: initial.onEventResize, events: initial.events || [], })); - }, [initial.onEventDrop, initial.customEditor, initial.events]); + }, [initial.onEventDrop, initial.customEditor, initial.events, initial.onEventResize]); - const handleState = (value: SchedulerState[keyof SchedulerState], name: keyof SchedulerState) => { - set((prev) => ({ ...prev, [name]: value })); - }; + const handleState = useCallback( + (value: SchedulerState[keyof SchedulerState], name: keyof SchedulerState) => { + set((prev) => ({ ...prev, [name]: value })); + }, + [] + ); - const getViews = () => { + const getViews = useCallback(() => { return getAvailableViews(state); - }; + }, [state]); - const toggleAgenda = () => { + const toggleAgenda = useCallback(() => { set((prev) => { const newStatus = !prev.agenda; @@ -42,163 +46,248 @@ export const StoreProvider = ({ children, initial }: Props) => { return { ...prev, agenda: newStatus }; }); - }; + }, [state]); - const triggerDialog = (status: boolean, selected?: SelectedRange | ProcessedEvent) => { - const isEvent = selected as ProcessedEvent; + const triggerDialog = useCallback( + (status: boolean, selected?: SelectedRange | ProcessedEvent) => { + const isEvent = selected as ProcessedEvent; - set((prev) => ({ - ...prev, - dialog: status, - selectedRange: isEvent?.event_id - ? undefined - : isEvent || { - start: new Date(), - end: new Date(Date.now() + 60 * 60 * 1000), - }, - selectedEvent: isEvent?.event_id ? isEvent : undefined, - selectedResource: prev.selectedResource || isEvent?.[state.resourceFields?.idField], - })); - }; - - const triggerLoading = (status: boolean) => { - // Trigger if not out-sourced by props - if (typeof initial.loading === "undefined") { - set((prev) => ({ ...prev, loading: status })); - } - }; - - const handleGotoDay = (day: Date) => { - const currentViews = getViews(); - let view: View | undefined; - if (currentViews.includes("day")) { - view = "day"; - set((prev) => ({ ...prev, view: "day", selectedDate: day })); - } else if (currentViews.includes("week")) { - view = "week"; - set((prev) => ({ ...prev, view: "week", selectedDate: day })); - } else { - console.warn("No Day/Week views available"); - } - - if (!!view && state.onViewChange && typeof state.onViewChange === "function") { - state.onViewChange(view, state.agenda); - } - - if (!!view && state.onSelectedDateChange && typeof state.onSelectedDateChange === "function") { - state.onSelectedDateChange(day); - } - }; - - const confirmEvent = (event: ProcessedEvent | ProcessedEvent[], action: EventActions) => { - let updatedEvents: ProcessedEvent[]; - if (action === "edit") { - if (Array.isArray(event)) { - updatedEvents = state.events.map((e) => { - const exist = event.find((ex) => ex.event_id === e.event_id); - return exist ? { ...e, ...exist } : e; - }); + set((prev) => ({ + ...prev, + dialog: status, + selectedRange: isEvent?.event_id + ? undefined + : isEvent || { + start: new Date(), + end: new Date(Date.now() + 60 * 60 * 1000), + }, + selectedEvent: isEvent?.event_id ? isEvent : undefined, + selectedResource: prev.selectedResource || isEvent?.[state.resourceFields?.idField], + })); + }, + [state.resourceFields?.idField] + ); + + const triggerLoading = useCallback( + (status: boolean) => { + // Trigger if not out-sourced by props + if (typeof initial.loading === "undefined") { + set((prev) => ({ ...prev, loading: status })); + } + }, + [initial.loading] + ); + + const handleGotoDay = useCallback( + (day: Date) => { + const currentViews = getViews(); + let view: View | undefined; + if (currentViews.includes("day")) { + view = "day"; + set((prev) => ({ ...prev, view: "day", selectedDate: day })); + } else if (currentViews.includes("week")) { + view = "week"; + set((prev) => ({ ...prev, view: "week", selectedDate: day })); } else { - updatedEvents = state.events.map((e) => - e.event_id === event.event_id ? { ...e, ...event } : e - ); + console.warn("No Day/Week views available"); } - } else { - updatedEvents = state.events.concat(event); - } - set((prev) => ({ ...prev, events: updatedEvents })); - }; - const setCurrentDragged = (event?: ProcessedEvent) => { - set((prev) => ({ ...prev, currentDragged: event })); - }; - - const onDrop = async ( - event: DragEvent, - eventId: string, - startTime: Date, - resKey?: string, - resVal?: string | number - ) => { - // Get dropped event - const droppedEvent = state.events.find((e) => { - if (typeof e.event_id === "number") { - return e.event_id === +eventId; + if (!!view && state.onViewChange && typeof state.onViewChange === "function") { + state.onViewChange(view, state.agenda); } - return e.event_id === eventId; - }) as ProcessedEvent; - - // Check if has resource and if is multiple - const resField = state.fields.find((f) => f.name === resKey); - const isMultiple = !!resField?.config?.multiple; - let newResource = resVal as string | number | string[] | number[]; - if (resField) { - const eResource = droppedEvent[resKey as string]; - const currentRes = arraytizeFieldVal(resField, eResource, droppedEvent).value; - if (isMultiple) { - // if dropped on already owned resource - if (currentRes.includes(resVal)) { - // Omit if dropped on same time slot for multiple event - if (isEqual(droppedEvent.start, startTime)) { - return; - } - newResource = currentRes; + + if ( + !!view && + state.onSelectedDateChange && + typeof state.onSelectedDateChange === "function" + ) { + state.onSelectedDateChange(day); + } + }, + [getViews, state] + ); + + const confirmEvent = useCallback( + (event: ProcessedEvent | ProcessedEvent[], action: EventActions) => { + let updatedEvents: ProcessedEvent[]; + if (action === "edit") { + if (Array.isArray(event)) { + updatedEvents = state.events.map((e) => { + const exist = event.find((ex) => ex.event_id === e.event_id); + return exist ? { ...e, ...exist } : e; + }); } else { - // if have multiple resource ? add other : move to other - newResource = currentRes.length > 1 ? [...currentRes, resVal] : [resVal]; + updatedEvents = state.events.map((e) => + e.event_id === event.event_id ? { ...e, ...event } : e + ); } + } else { + updatedEvents = state.events.concat(event); } - } + set((prev) => ({ ...prev, events: updatedEvents })); + }, + [state.events] + ); - // Omit if dropped on same time slot for non multiple events - if (isEqual(droppedEvent.start, startTime)) { - if (!newResource || (!isMultiple && newResource === droppedEvent[resKey as string])) { - return; + const setCurrentDragged = useCallback((event?: ProcessedEvent) => { + set((prev) => ({ ...prev, currentDragged: event })); + }, []); + + const setCurrentResize = useCallback((event?: ProcessedEvent) => { + set((prev) => ({ ...prev, currentResize: event })); + }, []); + + const onDrop = useCallback( + async ( + event: DragEvent, + eventId: string, + startTime: Date, + resKey?: string, + resVal?: string | number + ) => { + // Get dropped event + const droppedEvent = state.events.find((e) => { + if (typeof e.event_id === "number") { + return e.event_id === +eventId; + } + return e.event_id === eventId; + }) as ProcessedEvent; + + // Check if has resource and if is multiple + const resField = state.fields.find((f) => f.name === resKey); + const isMultiple = !!resField?.config?.multiple; + let newResource = resVal as string | number | string[] | number[]; + if (resField) { + const eResource = droppedEvent[resKey as string]; + const currentRes = arraytizeFieldVal(resField, eResource, droppedEvent).value; + if (isMultiple) { + // if dropped on already owned resource + if (currentRes.includes(resVal)) { + // Omit if dropped on same time slot for multiple event + if (isEqual(droppedEvent.start, startTime)) { + return; + } + newResource = currentRes; + } else { + // if have multiple resource ? add other : move to other + newResource = currentRes.length > 1 ? [...currentRes, resVal] : [resVal]; + } + } + } + + // Omit if dropped on same time slot for non multiple events + if (isEqual(droppedEvent.start, startTime)) { + if (!newResource || (!isMultiple && newResource === droppedEvent[resKey as string])) { + return; + } + } + + // Update event time according to original duration & update resources/owners + const diff = differenceInMinutes(droppedEvent.end, droppedEvent.start); + const updatedEvent: ProcessedEvent = { + ...droppedEvent, + start: startTime, + end: addMinutes(startTime, diff), + recurring: undefined, + [resKey as string]: newResource || "", + }; + + // Local + if (!state.onEventDrop || typeof state.onEventDrop !== "function") { + return confirmEvent(updatedEvent, "edit"); + } + // Remote + try { + triggerLoading(true); + const _event = await state.onEventDrop(event, startTime, updatedEvent, droppedEvent); + if (_event) { + confirmEvent(_event, "edit"); + } + } finally { + triggerLoading(false); + } + }, + [confirmEvent, state, triggerLoading] + ); + + const onResize = useCallback( + (ev: DragEvent, event: ProcessedEvent, minuteHeight: number): Date | undefined => { + const eventItem = ev.currentTarget.closest("div.rs__event__item") as HTMLDivElement | null; + if (eventItem) { + const { top } = eventItem.getBoundingClientRect(); + const diff = ev.clientY - top; + const minutes = diff / minuteHeight; + eventItem.style.height = `${diff}px`; + return addMinutes(event.start, minutes); } - } - - // Update event time according to original duration & update resources/owners - const diff = differenceInMinutes(droppedEvent.end, droppedEvent.start); - const updatedEvent: ProcessedEvent = { - ...droppedEvent, - start: startTime, - end: addMinutes(startTime, diff), - recurring: undefined, - [resKey as string]: newResource || "", - }; - - // Local - if (!state.onEventDrop || typeof state.onEventDrop !== "function") { - return confirmEvent(updatedEvent, "edit"); - } - // Remote - try { - triggerLoading(true); - const _event = await state.onEventDrop(event, startTime, updatedEvent, droppedEvent); - if (_event) { - confirmEvent(_event, "edit"); + }, + [] + ); + + const onResizeEnd = useCallback( + (ev: DragEvent, event: ProcessedEvent, minuteHeight: number) => { + const eventItem = ev.currentTarget.closest("div.rs__event__item") as HTMLDivElement | null; + if (eventItem) { + const { height } = eventItem.getBoundingClientRect(); + const minutes = height / minuteHeight; + + const updatedEvent: ProcessedEvent = { + ...event, + end: addMinutes(event.start, minutes), + recurring: undefined, + }; + + // Local + if (!state.onEventResize || typeof state.onEventResize !== "function") { + return confirmEvent(updatedEvent, "edit"); + } + // Remote + try { + triggerLoading(true); + const _event = state.onEventResize(ev, updatedEvent, event); + if (_event) { + confirmEvent(_event, "edit"); + } + } finally { + triggerLoading(false); + } } - } finally { - triggerLoading(false); - } - }; - - return ( - - {children} - + }, + [confirmEvent, state, triggerLoading] ); + + const value = useMemo( + () => ({ + ...state, + handleState, + getViews, + toggleAgenda, + triggerDialog, + triggerLoading, + handleGotoDay, + confirmEvent, + setCurrentDragged, + setCurrentResize, + onDrop, + onResize, + onResizeEnd, + }), + [ + confirmEvent, + getViews, + handleGotoDay, + handleState, + onDrop, + onResize, + setCurrentDragged, + setCurrentResize, + state, + toggleAgenda, + triggerDialog, + triggerLoading, + onResizeEnd, + ] + ); + + return {children}; }; diff --git a/src/lib/store/types.ts b/src/lib/store/types.ts index dd1e0d0b..e4c83743 100644 --- a/src/lib/store/types.ts +++ b/src/lib/store/types.ts @@ -10,6 +10,7 @@ export interface SchedulerState extends SchedulerProps { selectedEvent?: ProcessedEvent; selectedResource?: DefaultResource["assignee"]; currentDragged?: ProcessedEvent; + currentResize?: ProcessedEvent; enableAgenda?: boolean; } @@ -22,6 +23,7 @@ export interface Store extends SchedulerState { handleGotoDay(day: Date): void; confirmEvent(event: ProcessedEvent | ProcessedEvent[], action: EventActions): void; setCurrentDragged(event?: ProcessedEvent): void; + setCurrentResize(event?: ProcessedEvent): void; onDrop( event: DragEvent, eventId: string, @@ -29,4 +31,10 @@ export interface Store extends SchedulerState { resourceKey?: string, resourceVal?: string | number ): void; + onResize( + ev: DragEvent, + event: ProcessedEvent, + minuteHeight: number + ): Date | undefined; + onResizeEnd(ev: DragEvent, event: ProcessedEvent, minuteHeight: number): void; } diff --git a/src/lib/styles/styles.ts b/src/lib/styles/styles.ts index 041e0cce..2d2fc7ba 100644 --- a/src/lib/styles/styles.ts +++ b/src/lib/styles/styles.ts @@ -177,6 +177,7 @@ export const EventItemPaper = styled(Paper)<{ disabled?: boolean }>(({ disabled display: "block", cursor: disabled ? "not-allowed" : "pointer", overflow: "hidden", + position: "relative", "& .MuiButtonBase-root": { width: "100%", height: "100%", @@ -238,3 +239,13 @@ export const TimeIndicatorBar = styled("div")(({ theme }) => ({ width: "100%", }, })); + +export const DragHandle = styled("div")(({ draggable }) => ({ + position: "absolute", + bottom: 0, + left: 0, + width: "100%", + cursor: draggable ? "ns-resize" : "unset", + backgroundColor: "rgba(0,0,0,0.0001)", + height: "5px", +})); diff --git a/src/lib/types.ts b/src/lib/types.ts index 35e8ad97..1ff3d34e 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -63,6 +63,7 @@ interface CalendarEvent { editable?: boolean; deletable?: boolean; draggable?: boolean; + resizable?: boolean; allDay?: boolean; /** * @default " " @@ -295,6 +296,14 @@ export interface SchedulerProps { updatedEvent: ProcessedEvent, originalEvent: ProcessedEvent ): Promise; + /** + * Triggered when event is resized. + */ + onEventResize?( + event: DragEvent, + updatedEvent: ProcessedEvent, + originalEvent: ProcessedEvent + ): ProcessedEvent | void; /** * */ @@ -318,6 +327,11 @@ export interface SchedulerProps { * @default true */ draggable?: boolean; + /** + * If event is resizable, applied to all events globally, overridden by event specific resizable prop + * @default true + */ + resizable?: boolean; /** * Triggered when the `selectedDate` prop changes by navigation date picker or `today` button. */ From c399ec9335be5b3c1968c80cc8c07883e67067ea Mon Sep 17 00:00:00 2001 From: Andy Cork Date: Fri, 21 Feb 2025 16:02:06 +0000 Subject: [PATCH 02/15] Tidy the dates --- src/events.tsx | 114 +++++++++++++++++-------------------------------- 1 file changed, 40 insertions(+), 74 deletions(-) diff --git a/src/events.tsx b/src/events.tsx index 56fc62fa..17a9a619 100644 --- a/src/events.tsx +++ b/src/events.tsx @@ -2,13 +2,27 @@ import { RRule } from "rrule"; import { ProcessedEvent } from "./lib/types"; import { convertDateToRRuleDate } from "./lib/helpers/generals"; +const createDate = (startHour: number, startMinutes?: number, days?: number, months?: number) => { + let date = new Date(new Date().setHours(startHour)).setMinutes(0); + if (startMinutes) { + date = new Date(date).setMinutes(startMinutes); + } + if (days) { + date = new Date(date).setDate(new Date().getDate() + days); + } + if (months) { + date = new Date(date).setMonth(new Date().getMonth() + months); + } + return new Date(date); +}; + export const EVENTS: ProcessedEvent[] = [ { event_id: 1, title: "Event 1 (Disabled)", subtitle: "This event is disabled", - start: new Date(new Date(new Date().setHours(9)).setMinutes(0)), - end: new Date(new Date(new Date().setHours(10)).setMinutes(0)), + start: createDate(9), + end: createDate(10), disabled: true, admin_id: [1, 2, 3, 4], }, @@ -16,8 +30,8 @@ export const EVENTS: ProcessedEvent[] = [ event_id: 2, title: "Event 2", subtitle: "This event is draggable", - start: new Date(new Date(new Date().setHours(10)).setMinutes(0)), - end: new Date(new Date(new Date().setHours(12)).setMinutes(0)), + start: createDate(10), + end: createDate(12), admin_id: 2, color: "#50b500", agendaAvatar: "E", @@ -26,8 +40,8 @@ export const EVENTS: ProcessedEvent[] = [ event_id: 3, title: "Event 3", subtitle: "This event is not editable", - start: new Date(new Date(new Date().setHours(11)).setMinutes(0)), - end: new Date(new Date(new Date().setHours(12)).setMinutes(0)), + start: createDate(11), + end: createDate(12), admin_id: 1, editable: false, deletable: false, @@ -35,12 +49,8 @@ export const EVENTS: ProcessedEvent[] = [ { event_id: 4, title: "Event 4", - start: new Date( - new Date(new Date(new Date().setHours(9)).setMinutes(30)).setDate(new Date().getDate() - 2) - ), - end: new Date( - new Date(new Date(new Date().setHours(11)).setMinutes(0)).setDate(new Date().getDate() - 2) - ), + start: createDate(9, 30, -2), + end: createDate(11, 0, -2), admin_id: [2, 3], color: "#900000", allDay: true, @@ -49,12 +59,8 @@ export const EVENTS: ProcessedEvent[] = [ event_id: 5, title: "Event 5", subtitle: "This event is editable", - start: new Date( - new Date(new Date(new Date().setHours(10)).setMinutes(30)).setDate(new Date().getDate() - 2) - ), - end: new Date( - new Date(new Date(new Date().setHours(14)).setMinutes(0)).setDate(new Date().getDate() - 2) - ), + start: createDate(10, 30, -2), + end: createDate(14, 0, -2), admin_id: 2, editable: true, }, @@ -62,10 +68,8 @@ export const EVENTS: ProcessedEvent[] = [ event_id: 6, title: "Event 6", subtitle: "This event is all day", - start: new Date( - new Date(new Date(new Date().setHours(20)).setMinutes(30)).setDate(new Date().getDate() - 3) - ), - end: new Date(new Date(new Date().setHours(23)).setMinutes(0)), + start: createDate(20, 30, -3), + end: createDate(23), admin_id: 2, allDay: true, sx: { color: "purple" }, @@ -74,12 +78,8 @@ export const EVENTS: ProcessedEvent[] = [ event_id: 7, title: "Event 7 (Not draggable)", subtitle: "This event is not draggable", - start: new Date( - new Date(new Date(new Date().setHours(10)).setMinutes(30)).setDate(new Date().getDate() - 3) - ), - end: new Date( - new Date(new Date(new Date().setHours(14)).setMinutes(30)).setDate(new Date().getDate() - 3) - ), + start: createDate(10, 30, -3), + end: createDate(14, 30, -3), admin_id: 1, draggable: false, color: "#8000cc", @@ -88,63 +88,33 @@ export const EVENTS: ProcessedEvent[] = [ event_id: 8, title: "Event 8", subtitle: "This event has a custom color", - start: new Date( - new Date(new Date(new Date().setHours(10)).setMinutes(30)).setDate(new Date().getDate() + 30) - ), - end: new Date( - new Date(new Date(new Date().setHours(14)).setMinutes(30)).setDate(new Date().getDate() + 30) - ), + start: createDate(10, 30, 30), + end: createDate(14, 30, 30), admin_id: 1, color: "#8000cc", }, { event_id: 9, title: "Event 9", - subtitle: `This event is a recurring weekly until ${new Date( - new Date().setMonth( - new Date( - new Date(new Date(new Date().setHours(11)).setMinutes(0)).setDate( - new Date().getDate() + 1 - ) - ).getMonth() + 1 - ) - ).toDateString()}`, - start: new Date( - new Date(new Date(new Date().setHours(10)).setMinutes(0)).setDate(new Date().getDate() + 1) - ), - end: new Date( - new Date(new Date(new Date().setHours(11)).setMinutes(0)).setDate(new Date().getDate() + 1) - ), + subtitle: `This event is a recurring weekly until ${createDate(11, 0, 1, 1).toDateString()}`, + start: createDate(10, 0, 1), + end: createDate(11, 0, 1), recurring: new RRule({ freq: RRule.WEEKLY, - dtstart: convertDateToRRuleDate( - new Date( - new Date(new Date(new Date().setHours(10)).setMinutes(0)).setDate( - new Date().getDate() - 20 - ) - ) - ), - until: new Date( - new Date().setMonth( - new Date( - new Date(new Date(new Date().setHours(11)).setMinutes(0)).setDate( - new Date().getDate() + 1 - ) - ).getMonth() + 1 - ) - ), + dtstart: convertDateToRRuleDate(createDate(11, 0, -20)), + until: createDate(11, 0, 1, 1), }), }, { event_id: 10, title: "Event 10", subtitle: "This event is a recurring hourly 3 times", - start: new Date(new Date(new Date().setHours(14)).setMinutes(15)), - end: new Date(new Date(new Date().setHours(14)).setMinutes(45)), + start: createDate(14, 15), + end: createDate(14, 45), recurring: new RRule({ freq: RRule.HOURLY, count: 3, - dtstart: convertDateToRRuleDate(new Date(new Date(new Date().setHours(14)).setMinutes(15))), + dtstart: convertDateToRRuleDate(createDate(14, 15)), }), color: "#dc4552", }, @@ -152,12 +122,8 @@ export const EVENTS: ProcessedEvent[] = [ event_id: 11, title: "Event 11", subtitle: "This event is not resizable", - start: new Date( - new Date(new Date(new Date().setHours(10)).setMinutes(30)).setDate(new Date().getDate() - 4) - ), - end: new Date( - new Date(new Date(new Date().setHours(12)).setMinutes(30)).setDate(new Date().getDate() - 4) - ), + start: createDate(10, 30, -4), + end: createDate(12, 30, -4), admin_id: 1, resizable: false, }, From 47196b8952230cb51dde957f018696430b7b1a1f Mon Sep 17 00:00:00 2001 From: Andy Cork Date: Fri, 21 Feb 2025 16:22:51 +0000 Subject: [PATCH 03/15] Tidy date using mutation --- src/events.tsx | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/events.tsx b/src/events.tsx index 17a9a619..d7f52709 100644 --- a/src/events.tsx +++ b/src/events.tsx @@ -2,17 +2,18 @@ import { RRule } from "rrule"; import { ProcessedEvent } from "./lib/types"; import { convertDateToRRuleDate } from "./lib/helpers/generals"; -const createDate = (startHour: number, startMinutes?: number, days?: number, months?: number) => { - let date = new Date(new Date().setHours(startHour)).setMinutes(0); - if (startMinutes) { - date = new Date(date).setMinutes(startMinutes); - } - if (days) { - date = new Date(date).setDate(new Date().getDate() + days); - } - if (months) { - date = new Date(date).setMonth(new Date().getMonth() + months); - } +const createDate = ( + startHour: number, + startMinutes: number = 0, + days: number = 0, + months: number = 0 +) => { + const date = new Date(); + date.setHours(startHour); + date.setMinutes(startMinutes); + date.setDate(date.getDate() + days); + date.setMonth(date.getMonth() + months); + return new Date(date); }; From 48fe362a8f3334365e96db51007e37461bf25fe7 Mon Sep 17 00:00:00 2001 From: Andy Cork Date: Fri, 21 Feb 2025 16:29:56 +0000 Subject: [PATCH 04/15] No need to return new Date() --- src/events.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/events.tsx b/src/events.tsx index d7f52709..b13859e0 100644 --- a/src/events.tsx +++ b/src/events.tsx @@ -14,7 +14,7 @@ const createDate = ( date.setDate(date.getDate() + days); date.setMonth(date.getMonth() + months); - return new Date(date); + return date; }; export const EVENTS: ProcessedEvent[] = [ From ae51fcb66a4e3e1edc6d33182a3d7b6eb1f81dae Mon Sep 17 00:00:00 2001 From: Andy Cork Date: Sat, 22 Feb 2025 10:48:00 +0000 Subject: [PATCH 05/15] Don't resize below 1 minute length --- src/lib/store/provider.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/store/provider.tsx b/src/lib/store/provider.tsx index 3786bac0..9bfe118b 100644 --- a/src/lib/store/provider.tsx +++ b/src/lib/store/provider.tsx @@ -215,7 +215,7 @@ export const StoreProvider = ({ children, initial }: Props) => { const eventItem = ev.currentTarget.closest("div.rs__event__item") as HTMLDivElement | null; if (eventItem) { const { top } = eventItem.getBoundingClientRect(); - const diff = ev.clientY - top; + const diff = Math.max(ev.clientY - top, minuteHeight); const minutes = diff / minuteHeight; eventItem.style.height = `${diff}px`; return addMinutes(event.start, minutes); From 949e49ab07a55656f7c5cd0433d0600e49605c6b Mon Sep 17 00:00:00 2001 From: Andy Cork Date: Sat, 22 Feb 2025 20:31:11 +0000 Subject: [PATCH 06/15] Fix couple minor bugs in resize: * Always call onResize * Move Drag Image to constants --- src/lib/helpers/constants.ts | 4 ++++ src/lib/hooks/useResizeAttributes.ts | 10 ++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/lib/helpers/constants.ts b/src/lib/helpers/constants.ts index 3529a2cd..5da38f3c 100644 --- a/src/lib/helpers/constants.ts +++ b/src/lib/helpers/constants.ts @@ -2,3 +2,7 @@ export const BORDER_HEIGHT = 1; export const MULTI_DAY_EVENT_HEIGHT = 28; export const MONTH_NUMBER_HEIGHT = 27; export const MONTH_BAR_HEIGHT = 23; + +export const DRAG_IMAGE = new Image(); +DRAG_IMAGE.style.pointerEvents = "none"; +DRAG_IMAGE.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="; diff --git a/src/lib/hooks/useResizeAttributes.ts b/src/lib/hooks/useResizeAttributes.ts index 32e485ec..26fe6f07 100644 --- a/src/lib/hooks/useResizeAttributes.ts +++ b/src/lib/hooks/useResizeAttributes.ts @@ -1,10 +1,7 @@ import { DragEvent, useMemo } from "react"; import { ProcessedEvent } from "../types"; import useStore from "./useStore"; - -const img = new Image(); -img.style.pointerEvents = "none"; -img.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="; +import { DRAG_IMAGE } from "../helpers/constants"; const useResizeAttributes = ( event: ProcessedEvent, @@ -20,7 +17,7 @@ const useResizeAttributes = ( onDragStart: (e: DragEvent) => { e.stopPropagation(); setCurrentResize(event); - e.dataTransfer.setDragImage(img, 0, 0); + e.dataTransfer.setDragImage(DRAG_IMAGE, 0, 0); }, onDragEnd: (e: DragEvent) => { setCurrentResize(); @@ -30,7 +27,8 @@ const useResizeAttributes = ( onDrag: (e: DragEvent) => { e.stopPropagation(); e.preventDefault(); - onDragMove?.(onResize(e, event, minuteHeight)); + const date = onResize(e, event, minuteHeight); + onDragMove?.(date); }, onDragOver: (e: DragEvent) => { e.stopPropagation(); From 206183c8ac28281719537c563b6267f5a2c6237c Mon Sep 17 00:00:00 2001 From: Andy Cork Date: Thu, 20 Mar 2025 13:01:22 +0000 Subject: [PATCH 07/15] Merge upstream --- eslint.config.js | 2 +- package.json | 7 ++-- src/App.tsx | 17 +++++++--- src/Page1.tsx | 27 ++++++++++++++++ src/events.tsx | 13 ++++++-- src/index.tsx | 9 +++++- src/lib/helpers/generals.tsx | 11 ------- src/lib/store/default.ts | 62 +++++++++++++++++------------------- src/lib/views/Day.tsx | 3 +- src/lib/views/Month.tsx | 3 +- src/lib/views/Week.tsx | 3 +- vite.config.js | 14 ++------ 12 files changed, 101 insertions(+), 70 deletions(-) create mode 100644 src/Page1.tsx diff --git a/eslint.config.js b/eslint.config.js index 88ee9b94..7d67287f 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -2,7 +2,7 @@ import react from "eslint-plugin-react"; import tseslint, { configs as tseslintConfigs } from "typescript-eslint"; import globals from "globals"; import js from "@eslint/js"; -import reactHooks from "eslint-plugin-react-hooks"; +import * as reactHooks from "eslint-plugin-react-hooks"; import reactRefresh from "eslint-plugin-react-refresh"; import pluginImport from "eslint-plugin-import"; import pluginJsxA11y from "eslint-plugin-jsx-a11y"; diff --git a/package.json b/package.json index 461fd083..f115bc7e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@aldabil/react-scheduler", - "version": "3.0.3", + "version": "3.0.5", "description": "React scheduler component based on Material-UI & date-fns", "files": [ "*" @@ -69,6 +69,7 @@ "@types/jest": "^29.5.14", "@types/react": "^19.0.10", "@types/react-dom": "^19.0.4", + "@types/rollup-plugin-peer-deps-external": "^2", "@typescript-eslint/parser": "^8.24.1", "@vitejs/plugin-react": "^4.3.4", "date-fns": ">=4.1.0", @@ -78,7 +79,7 @@ "eslint-plugin-jsx-a11y": "^6.10.2", "eslint-plugin-promise": "^7.2.1", "eslint-plugin-react": "^7.37.4", - "eslint-plugin-react-hooks": "^5.1.0", + "eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-refresh": "^0.4.19", "globals": "^16.0.0", "husky": "^9.1.7", @@ -88,6 +89,8 @@ "prettier": "^3.5.1", "react": ">=19.0.0", "react-dom": "^19.0.0", + "react-router": "^7.3.0", + "rollup-plugin-peer-deps-external": "^2.2.4", "rrule": "^2.8.1", "ts-jest": "^29.2.5", "ts-node": "^10.9.2", diff --git a/src/App.tsx b/src/App.tsx index c5db2aea..ad042867 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -2,16 +2,23 @@ import { Scheduler } from "./lib"; import { EVENTS } from "./events"; import { useRef } from "react"; import { SchedulerRef } from "./lib/types"; +import { Link } from "react-router"; function App() { const calendarRef = useRef(null); return ( - + <> +
+ Go to page 1 +
+ + + ); } diff --git a/src/Page1.tsx b/src/Page1.tsx new file mode 100644 index 00000000..d7a9a61a --- /dev/null +++ b/src/Page1.tsx @@ -0,0 +1,27 @@ +import { Scheduler } from "./lib"; +import { EVENTS } from "./events"; +import { useRef } from "react"; +import { SchedulerRef } from "./lib/types"; +import { Link } from "react-router"; + +const events = EVENTS.slice(3, 6); + +function Page1() { + const calendarRef = useRef(null); + + return ( + <> +
+ Go to home +
+ + + + ); +} + +export default Page1; diff --git a/src/events.tsx b/src/events.tsx index b13859e0..b98ee1ea 100644 --- a/src/events.tsx +++ b/src/events.tsx @@ -1,6 +1,5 @@ -import { RRule } from "rrule"; +import { datetime, RRule } from "rrule"; import { ProcessedEvent } from "./lib/types"; -import { convertDateToRRuleDate } from "./lib/helpers/generals"; const createDate = ( startHour: number, @@ -184,3 +183,13 @@ export const generateRandomEvents = (total = 300) => { return events; }; + +function convertDateToRRuleDate(date: Date) { + return datetime( + date.getFullYear(), + date.getMonth() + 1, + date.getDate(), + date.getHours(), + date.getMinutes() + ); +} diff --git a/src/index.tsx b/src/index.tsx index 2ebc200c..f9a1df50 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -2,13 +2,20 @@ import * as React from "react"; import { CssBaseline, ThemeProvider, createTheme } from "@mui/material"; import { createRoot } from "react-dom/client"; import App from "./App"; +import { BrowserRouter, Route, Routes } from "react-router"; +import Page1 from "./Page1"; const root = createRoot(document.getElementById("root") as HTMLElement); root.render( - + + + } /> + } /> + + ); diff --git a/src/lib/helpers/generals.tsx b/src/lib/helpers/generals.tsx index b0522187..f441c7b4 100644 --- a/src/lib/helpers/generals.tsx +++ b/src/lib/helpers/generals.tsx @@ -21,7 +21,6 @@ import { SchedulerProps, } from "../types"; import { StateEvent } from "../views/Editor"; -import { datetime } from "rrule"; import { DragEvent } from "react"; export const getOneView = (state: Partial): View => { @@ -131,16 +130,6 @@ export const preventDragEvent = (ev: DragEvent) => { ev.preventDefault(); }; -export const convertDateToRRuleDate = (date: Date) => { - return datetime( - date.getFullYear(), - date.getMonth() + 1, - date.getDate(), - date.getHours(), - date.getMinutes() - ); -}; - export const convertRRuleDateToDate = (rruleDate: Date) => { return new Date( rruleDate.getUTCFullYear(), diff --git a/src/lib/store/default.ts b/src/lib/store/default.ts index cc92c7a2..8c0e96d5 100644 --- a/src/lib/store/default.ts +++ b/src/lib/store/default.ts @@ -88,19 +88,19 @@ const defaultViews = (props: Partial) => { export const defaultProps = (props: Partial) => { // We're pulling values out of props that we don't want to // pass on, so there are 'unused' ones here. - /* eslint-disable @typescript-eslint/no-unused-vars */ const { - month, - week, - day, translations, resourceFields, view, agenda, selectedDate, + resourceViewMode, + direction, + dialogMaxWidth, + hourFormat, ...otherProps } = props; - /* eslint-enable @typescript-eslint/no-unused-vars */ + const views = defaultViews(props); const defaultView = view || "week"; const initialView = views[defaultView] ? defaultView : getOneView(views); @@ -110,34 +110,30 @@ export const defaultProps = (props: Partial) => { resourceFields: Object.assign(defaultResourceFields, resourceFields), view: initialView, selectedDate: getTimeZonedDate(selectedDate || new Date(), props.timeZone), - ...Object.assign( - { - height: 600, - navigation: true, - disableViewNavigator: false, - events: [], - fields: [], - loading: undefined, - customEditor: undefined, - onConfirm: undefined, - onDelete: undefined, - viewerExtraComponent: undefined, - resources: [], - resourceHeaderComponent: undefined, - resourceViewMode: "default", - direction: "ltr", - dialogMaxWidth: "md", - locale: enUS, - deletable: true, - editable: true, - resizable: true, - hourFormat: "12", - draggable: true, - agenda, - enableAgenda: typeof agenda === "undefined" || agenda, - }, - otherProps - ), + height: 600, + navigation: true, + disableViewNavigator: false, + events: [], + fields: [], + loading: undefined, + customEditor: undefined, + onConfirm: undefined, + onDelete: undefined, + viewerExtraComponent: undefined, + resources: [], + resourceHeaderComponent: undefined, + resourceViewMode: resourceViewMode || "default", + direction: direction || "ltr", + dialogMaxWidth: dialogMaxWidth || "md", + locale: enUS, + deletable: true, + editable: true, + resizable: true, + hourFormat: hourFormat || "12", + draggable: true, + agenda, + enableAgenda: typeof agenda === "undefined" || agenda, + ...otherProps, }; }; diff --git a/src/lib/views/Day.tsx b/src/lib/views/Day.tsx index e84d70b2..6e6ad399 100644 --- a/src/lib/views/Day.tsx +++ b/src/lib/views/Day.tsx @@ -94,7 +94,8 @@ const Day = () => { } finally { triggerLoading(false); } - }, [triggerLoading, START_TIME, END_TIME, getRemoteEvents, handleState]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getRemoteEvents]); useEffect(() => { if (getRemoteEvents instanceof Function) { diff --git a/src/lib/views/Month.tsx b/src/lib/views/Month.tsx index ecdd82ea..4c6454f9 100644 --- a/src/lib/views/Month.tsx +++ b/src/lib/views/Month.tsx @@ -63,7 +63,8 @@ const Month = () => { } finally { triggerLoading(false); } - }, [triggerLoading, eachWeekStart, daysList.length, getRemoteEvents, handleState]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [daysList.length, getRemoteEvents]); useEffect(() => { if (getRemoteEvents instanceof Function) { diff --git a/src/lib/views/Week.tsx b/src/lib/views/Week.tsx index 3ac36fb6..62aac917 100644 --- a/src/lib/views/Week.tsx +++ b/src/lib/views/Week.tsx @@ -69,7 +69,8 @@ const Week = () => { } finally { triggerLoading(false); } - }, [triggerLoading, getRemoteEvents, weekStart, weekEnd, handleState]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [getRemoteEvents]); useEffect(() => { if (getRemoteEvents instanceof Function) { diff --git a/vite.config.js b/vite.config.js index 3701c813..300be830 100644 --- a/vite.config.js +++ b/vite.config.js @@ -4,7 +4,7 @@ import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; import dts from "vite-plugin-dts"; import tsconfigPaths from "vite-tsconfig-paths"; -import { peerDependencies } from "./package.json"; +import peerDepsExternal from "rollup-plugin-peer-deps-external"; const __dirname = dirname(fileURLToPath(import.meta.url)); @@ -15,6 +15,7 @@ export default defineConfig(() => ({ configNames: ["tsconfig.json"], }), dts({ tsconfigPath: "./tsconfig.build.json" }), + peerDepsExternal(), ], server: { port: 3000, @@ -31,17 +32,6 @@ export default defineConfig(() => ({ name: "Scheduler", formats: ["es"], }, - rollupOptions: { - external: (path) => { - const nodeModules = path.includes("node_modules"); - const isPeer = Object.keys(peerDependencies).some((dep) => path.startsWith(dep)); - const isExternal = nodeModules || isPeer; - return isExternal; - }, - output: { - globals: (path) => path, - }, - }, copyPublicDir: false, }, resolve: { From 476402dcfcf7bcd129aafd7c8a13acdeabbfe8b7 Mon Sep 17 00:00:00 2001 From: Andy <56588504+vespasianvs@users.noreply.github.com> Date: Thu, 20 Mar 2025 13:18:24 +0000 Subject: [PATCH 08/15] Update package.json Add postinstall so can be installed from the repo --- package.json | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index f115bc7e..b82a7cc9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@aldabil/react-scheduler", - "version": "3.0.5", + "version": "3.0.6", "description": "React scheduler component based on Material-UI & date-fns", "files": [ "*" @@ -20,14 +20,15 @@ "pre:commit": "lint-staged", "test": "jest", "test:watch": "jest --watch", - "test:ci": "jest --ci" + "test:ci": "jest --ci", + "postinstall": "npm run build" }, "exports": { "./types": { - "import": "./types.d.ts" + "import": "./dist/types.d.ts" }, ".": { - "import": "./index.js" + "import": "./dist/index.js" } }, "lint-staged": { From 3ffb35a02bd54fecdaca4248f5d7e3c6673dd001 Mon Sep 17 00:00:00 2001 From: Andy Cork Date: Thu, 20 Mar 2025 13:40:18 +0000 Subject: [PATCH 09/15] Revert package.json --- package.json | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index b82a7cc9..b1e5192e 100644 --- a/package.json +++ b/package.json @@ -20,15 +20,14 @@ "pre:commit": "lint-staged", "test": "jest", "test:watch": "jest --watch", - "test:ci": "jest --ci", - "postinstall": "npm run build" + "test:ci": "jest --ci" }, "exports": { "./types": { - "import": "./dist/types.d.ts" + "import": "./types.d.ts" }, ".": { - "import": "./dist/index.js" + "import": "./index.js" } }, "lint-staged": { From e3e36befd269daada2c1d8b276e37afbf8fb8b4b Mon Sep 17 00:00:00 2001 From: Andy Cork Date: Fri, 21 Mar 2025 10:13:57 +0000 Subject: [PATCH 10/15] Update package.json and readme for publish --- README.md | 9 +++++---- package.json | 6 +++--- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 2fcdc3eb..e042a63d 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,15 @@ # React Scheduler Component -[![npm package](https://img.shields.io/npm/v/@aldabil/react-scheduler/latest.svg)](https://www.npmjs.com/package/@aldabil/react-scheduler) -[![Twitter URL](https://img.shields.io/twitter/url?label=%40aldabil&style=social&url=https%3A%2F%2Ftwitter.com%2Fintent%2Ffollow%3Fscreen_name%3Daldabil21)](https://twitter.com/intent/follow?screen_name=aldabil21) +[![npm package](https://img.shields.io/npm/v/@wearemothership/react-scheduler/latest.svg)](https://www.npmjs.com/package/@wearemothership/react-scheduler) + +Based on the fantastic work of [@aldabil/react-scheduler](https://github.com/wearemothership/react-scheduler.git) > :warning: **Notice**: This component uses `mui`/`emotion`/`date-fns`. if your project is not already using these libs, this component may not be suitable. ## Installation ```jsx -npm i @aldabil/react-scheduler +npm i @wearemothership/react-scheduler ``` If you plan to use `recurring` events in your scheduler, install `rrule` [package](https://www.npmjs.com/package/rrule) @@ -16,7 +17,7 @@ If you plan to use `recurring` events in your scheduler, install `rrule` [packag ## Usage ```jsx -import { Scheduler } from "@aldabil/react-scheduler"; +import { Scheduler } from "@wearemothership/react-scheduler"; ``` ## Example diff --git a/package.json b/package.json index b1e5192e..f9d56887 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "@aldabil/react-scheduler", + "name": "@wearemothership/react-scheduler", "version": "3.0.6", "description": "React scheduler component based on Material-UI & date-fns", "files": [ @@ -40,7 +40,7 @@ }, "repository": { "type": "git", - "url": "https://github.com/aldabil21/react-scheduler.git" + "url": "https://github.com/wearemothership/react-scheduler.git" }, "keywords": [ "react", @@ -51,7 +51,7 @@ "author": "Aldabil", "license": "MIT", "bugs": { - "url": "https://github.com/aldabil21/react-scheduler/issues" + "url": "https://github.com/wearemothership/react-scheduler/issues" }, "devDependencies": { "@emotion/react": "^11.14.0", From d240c4221bdd37cb487bfbb13462823e4f393e21 Mon Sep 17 00:00:00 2001 From: Andy Cork Date: Fri, 21 Mar 2025 10:15:26 +0000 Subject: [PATCH 11/15] Correct URL --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e042a63d..f0eb23bb 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![npm package](https://img.shields.io/npm/v/@wearemothership/react-scheduler/latest.svg)](https://www.npmjs.com/package/@wearemothership/react-scheduler) -Based on the fantastic work of [@aldabil/react-scheduler](https://github.com/wearemothership/react-scheduler.git) +Based on the fantastic work of [@aldabil/react-scheduler](https://github.com/aldabil/react-scheduler.git) > :warning: **Notice**: This component uses `mui`/`emotion`/`date-fns`. if your project is not already using these libs, this component may not be suitable. From f7df99d876748c56d8a10228a43cec856557ab2d Mon Sep 17 00:00:00 2001 From: Andy Cork Date: Fri, 21 Mar 2025 10:17:06 +0000 Subject: [PATCH 12/15] Try again with the URL! --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f0eb23bb..a094aef5 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![npm package](https://img.shields.io/npm/v/@wearemothership/react-scheduler/latest.svg)](https://www.npmjs.com/package/@wearemothership/react-scheduler) -Based on the fantastic work of [@aldabil/react-scheduler](https://github.com/aldabil/react-scheduler.git) +Based on the fantastic work of [@aldabil/react-scheduler](https://github.com/aldabil21/react-scheduler) > :warning: **Notice**: This component uses `mui`/`emotion`/`date-fns`. if your project is not already using these libs, this component may not be suitable. From 510a563032c7986b6d7f7ce35be0b43121747b5d Mon Sep 17 00:00:00 2001 From: Andy Cork Date: Fri, 21 Mar 2025 15:45:42 +0000 Subject: [PATCH 13/15] Move events accurately using drag and drop --- src/lib/SchedulerComponent.tsx | 1 + src/lib/components/events/EventItem.tsx | 24 ++-- src/lib/components/events/TodayEvents.tsx | 2 +- src/lib/components/week/WeekTable.tsx | 15 +-- src/lib/hooks/useCellAttributes.ts | 11 +- src/lib/hooks/useDragAttributes.ts | 133 +++++++++++++++++++++- src/lib/hooks/useResizeAttributes.ts | 3 +- src/lib/store/default.ts | 1 + src/lib/store/provider.tsx | 8 +- src/lib/store/types.ts | 4 +- src/lib/types.ts | 4 +- src/lib/views/Day.tsx | 36 +++--- src/lib/views/Month.tsx | 4 + src/lib/views/Week.tsx | 34 +++--- 14 files changed, 220 insertions(+), 60 deletions(-) diff --git a/src/lib/SchedulerComponent.tsx b/src/lib/SchedulerComponent.tsx index dbe3e396..a992e87e 100644 --- a/src/lib/SchedulerComponent.tsx +++ b/src/lib/SchedulerComponent.tsx @@ -67,6 +67,7 @@ const SchedulerComponent = forwardRef(function SchedulerC flexDirection: resourceViewMode === "vertical" ? "column" : undefined, }} data-testid="grid" + id="rs__grid" > {Views} diff --git a/src/lib/components/events/EventItem.tsx b/src/lib/components/events/EventItem.tsx index 5ddfbb15..40df4348 100644 --- a/src/lib/components/events/EventItem.tsx +++ b/src/lib/components/events/EventItem.tsx @@ -18,19 +18,19 @@ interface EventItemProps { hasPrev?: boolean; hasNext?: boolean; showdate?: boolean; - minuteHeight?: number; } -const EventItem = ({ - event, - multiday, - hasPrev, - hasNext, - showdate = true, - minuteHeight, -}: EventItemProps) => { - const { direction, locale, hourFormat, eventRenderer, onEventClick, view, disableViewer } = - useStore(); +const EventItem = ({ event, multiday, hasPrev, hasNext, showdate = true }: EventItemProps) => { + const { + direction, + locale, + hourFormat, + eventRenderer, + onEventClick, + view, + disableViewer, + minuteHeight, + } = useStore(); const dragHandleRef = useRef(null); const [dragTime, setDragTime] = useState(); const onDragMove = useCallback((time: Date | undefined) => { @@ -38,7 +38,7 @@ const EventItem = ({ }, []); const dragProps = useDragAttributes(event); - const resizeProps = useResizeAttributes(event, minuteHeight, onDragMove); + const resizeProps = useResizeAttributes(event, onDragMove); const [anchorEl, setAnchorEl] = useState(null); const [deleteConfirm, setDeleteConfirm] = useState(false); const theme = useTheme(); diff --git a/src/lib/components/events/TodayEvents.tsx b/src/lib/components/events/TodayEvents.tsx index 8d16bd55..8b20b87b 100644 --- a/src/lib/components/events/TodayEvents.tsx +++ b/src/lib/components/events/TodayEvents.tsx @@ -80,7 +80,7 @@ const TodayEvents = ({ : "", }} > - + ); })} diff --git a/src/lib/components/week/WeekTable.tsx b/src/lib/components/week/WeekTable.tsx index 46a26813..750274be 100644 --- a/src/lib/components/week/WeekTable.tsx +++ b/src/lib/components/week/WeekTable.tsx @@ -32,7 +32,7 @@ type Props = { daysList: Date[]; hours: Date[]; cellHeight: number; - minutesHeight: number; + minuteHeight: number; resource?: DefaultResource; resourcedEvents: ProcessedEvent[]; }; @@ -41,7 +41,7 @@ const WeekTable = ({ daysList, hours, cellHeight, - minutesHeight, + minuteHeight, resourcedEvents, resource, }: Props) => { @@ -120,13 +120,7 @@ const WeekTable = ({ overflowX: "hidden", }} > - + ); }); @@ -145,6 +139,7 @@ const WeekTable = ({ {daysList.map((date, i) => ( @@ -188,7 +183,7 @@ const WeekTable = ({ { if (editable) { @@ -39,22 +42,22 @@ export const useCellAttributes = ({ start, end, resourceKey, resourceVal }: Prop }, onDragOver: (e: DragEvent) => { e.preventDefault(); - if (currentDragged) { + if (currentDragged && view === "month") { e.currentTarget.style.backgroundColor = alpha(theme.palette.secondary.main, 0.3); } }, onDragEnter: (e: DragEvent) => { - if (currentDragged) { + if (currentDragged && view === "month") { e.currentTarget.style.backgroundColor = alpha(theme.palette.secondary.main, 0.3); } }, onDragLeave: (e: DragEvent) => { - if (currentDragged) { + if (currentDragged && view === "month") { e.currentTarget.style.backgroundColor = ""; } }, onDrop: (e: DragEvent) => { - if (currentDragged && currentDragged.event_id) { + if (currentDragged && currentDragged.event_id && view === "month") { e.preventDefault(); e.currentTarget.style.backgroundColor = ""; const zonedStart = revertTimeZonedDate(start, timeZone); diff --git a/src/lib/hooks/useDragAttributes.ts b/src/lib/hooks/useDragAttributes.ts index 1e738628..f833a235 100644 --- a/src/lib/hooks/useDragAttributes.ts +++ b/src/lib/hooks/useDragAttributes.ts @@ -2,19 +2,150 @@ import { DragEvent } from "react"; import { ProcessedEvent } from "../types"; import { useTheme } from "@mui/material"; import useStore from "./useStore"; +import { addMinutes, format } from "date-fns"; + +const img = new Image(); +img.src = "data:image/gif;base64,R0lGODlhAQABAIAAAAUEBAAAACwAAAAAAQABAAACAkQBADs="; + +let startPos: [number, number] | undefined; +const timeCell = document.createElement("div"); +timeCell.style.position = "absolute"; +timeCell.style.top = "-20px"; +timeCell.style.width = "100%"; +timeCell.style.textAlign = "center"; +timeCell.style.fontSize = "small"; + +const findRsCell = ( + bounds: DOMRect, + cells: NodeListOf, + withMiddle = false +): Element | null => { + const middle = withMiddle ? (bounds.right - bounds.left) / 2 + bounds.left : bounds.left; + let found: Element | null = null; + for (const cell of cells) { + const cellRect = cell.getBoundingClientRect(); + if ( + cellRect.top < bounds.top && + cellRect.bottom > bounds.top && + cellRect.left < middle && + cellRect.right > middle + ) { + found = cell; + break; + } + } + return found; +}; const useDragAttributes = (event: ProcessedEvent) => { - const { setCurrentDragged } = useStore(); + const { setCurrentDragged, minuteHeight, currentDragged, onDrop } = useStore(); const theme = useTheme(); + const headerRect = document.querySelector(".rs__header")?.getBoundingClientRect(); + const gridRect = document.querySelector("#rs__grid")?.getBoundingClientRect(); return { draggable: true, onDragStart: (e: DragEvent) => { e.stopPropagation(); setCurrentDragged(event); + e.dataTransfer.setDragImage(img, 0, 0); + startPos = [e.clientX, e.clientY]; e.currentTarget.style.backgroundColor = theme.palette.error.main; }, + onDrag: (e: DragEvent) => { + if (currentDragged && startPos) { + if (currentDragged.allDay) { + const cell = e.currentTarget.closest(".rs__multi_day") as HTMLElement; + if (cell) { + let diff = e.clientX - startPos[0]; + cell.style.transform = `translateX(${diff}px)`; + const header = cell.closest(".rs__header") as HTMLElement; + const headers = header?.parentElement?.querySelectorAll(".rs__header"); + if (headers) { + let rect = cell.getBoundingClientRect(); + const leftmost = headers.item(0).getBoundingClientRect().left; + const rightmost = headers.item(headers.length - 1).getBoundingClientRect().right; + if (rect.left < leftmost) { + diff += leftmost - rect.left; + } + if (rect.right > rightmost) { + diff += rightmost - rect.right; + } + cell.style.transform = `translateX(${diff}px)`; + rect = cell.getBoundingClientRect(); + const found = findRsCell(rect, headers) as HTMLElement; + const dateString = found?.dataset.date; + if (dateString) { + timeCell.dataset.time = dateString; + } + } + } + } else { + const cell = e.currentTarget.closest(".rs__event__item") as HTMLElement; + if (cell && headerRect && gridRect) { + const diff = [e.clientX - startPos[0], e.clientY - startPos[1]]; + cell.style.transform = `translate(${diff[0]}px, ${diff[1]}px)`; + let rect = cell.getBoundingClientRect(); + if (rect.left < headerRect.left) { + diff[0] += headerRect.left - rect.left; + } + if (rect.right > gridRect.right) { + diff[0] += gridRect.right - rect.right; + } + if (rect.top < headerRect.bottom) { + diff[1] += headerRect.bottom - rect.top; + } + if (rect.bottom > gridRect.bottom) { + diff[1] += gridRect.bottom - rect.bottom; + } + + cell.style.transform = `translate(${diff[0]}px, ${diff[1]}px)`; + rect = cell.getBoundingClientRect(); + const timeCells = document.querySelectorAll(".rs__cell:not(.rs__header)"); + const found = findRsCell(rect, timeCells, true); + + if (found && minuteHeight && timeCell) { + cell.appendChild(timeCell); + const button = found.querySelector("& > button") as HTMLButtonElement; + const fRect = found.getBoundingClientRect(); + const dateString = button?.dataset.start; + const topDiff = rect.top - fRect.top; + const mins = topDiff / minuteHeight; + if (dateString) { + const dd = new Date(dateString); + const newDD = addMinutes(dd, mins); + timeCell.dataset.time = newDD.toString(); + timeCell.innerText = format(newDD, "Pp"); + } + } + } + } + } + }, onDragEnd: (e: DragEvent) => { + if (currentDragged) { + if (currentDragged.allDay) { + const cell = e.currentTarget.closest(".rs__multi_day") as HTMLElement; + if (cell) { + cell.style.transform = "unset"; + } + const dateString = timeCell.dataset.time; + if (dateString) { + onDrop(e, currentDragged.event_id.toString(), new Date(dateString)); + } + } else { + const cell = e.currentTarget.closest(".rs__event__item") as HTMLElement; + if (cell) { + cell.removeChild(timeCell); + cell.style.transform = "unset"; + } + const dateString = timeCell.dataset.time; + if (dateString) { + onDrop(e, currentDragged.event_id.toString(), new Date(dateString)); + } + } + } setCurrentDragged(); + startPos = undefined; e.currentTarget.style.backgroundColor = event.color || theme.palette.primary.main; }, onDragOver: (e: DragEvent) => { diff --git a/src/lib/hooks/useResizeAttributes.ts b/src/lib/hooks/useResizeAttributes.ts index 26fe6f07..555ba9a3 100644 --- a/src/lib/hooks/useResizeAttributes.ts +++ b/src/lib/hooks/useResizeAttributes.ts @@ -5,10 +5,9 @@ import { DRAG_IMAGE } from "../helpers/constants"; const useResizeAttributes = ( event: ProcessedEvent, - minuteHeight?: number, onDragMove?: (time: Date | undefined) => void ) => { - const { setCurrentResize, onResize, onResizeEnd } = useStore(); + const { setCurrentResize, onResize, onResizeEnd, minuteHeight } = useStore(); const handlers = useMemo( () => minuteHeight diff --git a/src/lib/store/default.ts b/src/lib/store/default.ts index 8c0e96d5..c4ad045a 100644 --- a/src/lib/store/default.ts +++ b/src/lib/store/default.ts @@ -152,6 +152,7 @@ export const initialStore = { handleGotoDay: () => {}, confirmEvent: () => {}, setCurrentDragged: () => {}, + setMinuteHeight: () => {}, setCurrentResize: () => {}, onDrop: () => {}, onResize: () => undefined, diff --git a/src/lib/store/provider.tsx b/src/lib/store/provider.tsx index 9bfe118b..f048b629 100644 --- a/src/lib/store/provider.tsx +++ b/src/lib/store/provider.tsx @@ -137,9 +137,13 @@ export const StoreProvider = ({ children, initial }: Props) => { set((prev) => ({ ...prev, currentResize: event })); }, []); + const setMinuteHeight = useCallback((height?: number) => { + set((prev) => ({ ...prev, minuteHeight: height })); + }, []); + const onDrop = useCallback( async ( - event: DragEvent, + event: DragEvent, eventId: string, startTime: Date, resKey?: string, @@ -268,6 +272,7 @@ export const StoreProvider = ({ children, initial }: Props) => { confirmEvent, setCurrentDragged, setCurrentResize, + setMinuteHeight, onDrop, onResize, onResizeEnd, @@ -281,6 +286,7 @@ export const StoreProvider = ({ children, initial }: Props) => { onResize, setCurrentDragged, setCurrentResize, + setMinuteHeight, state, toggleAgenda, triggerDialog, diff --git a/src/lib/store/types.ts b/src/lib/store/types.ts index e4c83743..db48a701 100644 --- a/src/lib/store/types.ts +++ b/src/lib/store/types.ts @@ -6,6 +6,7 @@ export type SelectedRange = { start: Date; end: Date }; export interface SchedulerState extends SchedulerProps { dialog: boolean; + minuteHeight?: number; selectedRange?: SelectedRange; selectedEvent?: ProcessedEvent; selectedResource?: DefaultResource["assignee"]; @@ -24,8 +25,9 @@ export interface Store extends SchedulerState { confirmEvent(event: ProcessedEvent | ProcessedEvent[], action: EventActions): void; setCurrentDragged(event?: ProcessedEvent): void; setCurrentResize(event?: ProcessedEvent): void; + setMinuteHeight(height?: number): void; onDrop( - event: DragEvent, + event: DragEvent, eventId: string, droppedStartTime: Date, resourceKey?: string, diff --git a/src/lib/types.ts b/src/lib/types.ts index 1ff3d34e..b3c6cf33 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -48,7 +48,7 @@ export interface CellRenderedProps { onDragOver(e: DragEvent): void; onDragEnter(e: DragEvent): void; onDragLeave(e: DragEvent): void; - onDrop(e: DragEvent): void; + onDrop(e: DragEvent): void; } interface CalendarEvent { event_id: number | string; @@ -291,7 +291,7 @@ export interface SchedulerProps { * Triggered when event is dropped on time slot. */ onEventDrop?( - event: DragEvent, + event: DragEvent, droppedOn: Date, updatedEvent: ProcessedEvent, originalEvent: ProcessedEvent diff --git a/src/lib/views/Day.tsx b/src/lib/views/Day.tsx index 6e6ad399..dc33a1de 100644 --- a/src/lib/views/Day.tsx +++ b/src/lib/views/Day.tsx @@ -1,4 +1,4 @@ -import { useEffect, useCallback, Fragment, JSX } from "react"; +import { useEffect, useCallback, Fragment, JSX, useMemo } from "react"; import { Typography } from "@mui/material"; import { format, @@ -50,6 +50,7 @@ const Day = () => { getRemoteEvents, triggerLoading, handleState, + setMinuteHeight, resources, resourceFields, resourceViewMode, @@ -63,19 +64,28 @@ const Day = () => { } = useStore(); const { startHour, endHour, step, cellRenderer, headRenderer, hourRenderer } = day!; - const START_TIME = set(selectedDate, { hours: startHour, minutes: 0, seconds: 0 }); - const END_TIME = set(selectedDate, { hours: endHour, minutes: -step, seconds: 0 }); - const hours = eachMinuteOfInterval( - { - start: START_TIME, - end: END_TIME, - }, - { step: step } - ); - const CELL_HEIGHT = calcCellHeight(height, hours.length); - const MINUTE_HEIGHT = calcMinuteHeight(CELL_HEIGHT, step); + const hFormat = getHourFormat(hourFormat); + const [hours, CELL_HEIGHT, MINUTE_HEIGHT, START_TIME, END_TIME] = useMemo(() => { + const START_TIME = set(selectedDate, { hours: startHour, minutes: 0, seconds: 0 }); + const END_TIME = set(selectedDate, { hours: endHour, minutes: -step, seconds: 0 }); + const hours = eachMinuteOfInterval( + { + start: START_TIME, + end: END_TIME, + }, + { step: step } + ); + const CELL_HEIGHT = calcCellHeight(height, hours.length); + const MINUTE_HEIGHT = calcMinuteHeight(CELL_HEIGHT, step); + return [hours, CELL_HEIGHT, MINUTE_HEIGHT, START_TIME, END_TIME]; + }, [endHour, height, selectedDate, startHour, step]); + + useEffect(() => { + setMinuteHeight(MINUTE_HEIGHT); + }, [MINUTE_HEIGHT, setMinuteHeight]); + const fetchEvents = useCallback(async () => { try { triggerLoading(true); @@ -220,8 +230,8 @@ const Day = () => { }, [ CELL_HEIGHT, - MINUTE_HEIGHT, START_TIME, + MINUTE_HEIGHT, agenda, cellRenderer, direction, diff --git a/src/lib/views/Month.tsx b/src/lib/views/Month.tsx index 4c6454f9..c45affb8 100644 --- a/src/lib/views/Month.tsx +++ b/src/lib/views/Month.tsx @@ -27,6 +27,7 @@ const Month = () => { getRemoteEvents, triggerLoading, handleState, + setMinuteHeight, resources, resourceFields, fields, @@ -36,6 +37,9 @@ const Month = () => { const { weekStartOn, weekDays } = month!; const monthStart = startOfMonth(selectedDate); const monthEnd = endOfMonth(selectedDate); + useEffect(() => { + setMinuteHeight(); + }, [setMinuteHeight]); const eachWeekStart = eachWeekOfInterval( { start: monthStart, diff --git a/src/lib/views/Week.tsx b/src/lib/views/Week.tsx index 62aac917..80a9f527 100644 --- a/src/lib/views/Week.tsx +++ b/src/lib/views/Week.tsx @@ -1,4 +1,4 @@ -import { useEffect, useCallback, JSX } from "react"; +import { useEffect, useCallback, JSX, useMemo } from "react"; import { startOfWeek, addDays, eachMinuteOfInterval, endOfDay, startOfDay, set } from "date-fns"; import { CellRenderedProps, DayHours, DefaultResource } from "../types"; import { WeekDays } from "./Month"; @@ -34,23 +34,31 @@ const Week = () => { resourceFields, fields, agenda, + setMinuteHeight, } = useStore(); const { weekStartOn, weekDays, startHour, endHour, step } = week!; const _weekStart = startOfWeek(selectedDate, { weekStartsOn: weekStartOn }); const daysList = weekDays.map((d) => addDays(_weekStart, d)); const weekStart = startOfDay(daysList[0]); const weekEnd = endOfDay(daysList[daysList.length - 1]); - const START_TIME = set(selectedDate, { hours: startHour, minutes: 0, seconds: 0 }); - const END_TIME = set(selectedDate, { hours: endHour, minutes: -step, seconds: 0 }); - const hours = eachMinuteOfInterval( - { - start: START_TIME, - end: END_TIME, - }, - { step } - ); - const CELL_HEIGHT = calcCellHeight(height, hours.length); - const MINUTE_HEIGHT = calcMinuteHeight(CELL_HEIGHT, step); + const [hours, CELL_HEIGHT, MINUTE_HEIGHT] = useMemo(() => { + const START_TIME = set(selectedDate, { hours: startHour, minutes: 0, seconds: 0 }); + const END_TIME = set(selectedDate, { hours: endHour, minutes: -step, seconds: 0 }); + const hours = eachMinuteOfInterval( + { + start: START_TIME, + end: END_TIME, + }, + { step } + ); + const CELL_HEIGHT = calcCellHeight(height, hours.length); + const MINUTE_HEIGHT = calcMinuteHeight(CELL_HEIGHT, step); + return [hours, CELL_HEIGHT, MINUTE_HEIGHT]; + }, [endHour, height, selectedDate, startHour, step]); + + useEffect(() => { + setMinuteHeight(MINUTE_HEIGHT); + }, [MINUTE_HEIGHT, setMinuteHeight]); const fetchEvents = useCallback(async () => { try { @@ -94,7 +102,7 @@ const Week = () => { resource={resource} hours={hours} cellHeight={CELL_HEIGHT} - minutesHeight={MINUTE_HEIGHT} + minuteHeight={MINUTE_HEIGHT} daysList={daysList} /> ); From d344b880d247095ca4d375fb7aace7369ce3c9fe Mon Sep 17 00:00:00 2001 From: Andy Cork Date: Fri, 21 Mar 2025 15:50:39 +0000 Subject: [PATCH 14/15] Font size --- src/lib/hooks/useDragAttributes.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/hooks/useDragAttributes.ts b/src/lib/hooks/useDragAttributes.ts index f833a235..046842a9 100644 --- a/src/lib/hooks/useDragAttributes.ts +++ b/src/lib/hooks/useDragAttributes.ts @@ -13,7 +13,7 @@ timeCell.style.position = "absolute"; timeCell.style.top = "-20px"; timeCell.style.width = "100%"; timeCell.style.textAlign = "center"; -timeCell.style.fontSize = "small"; +timeCell.style.fontSize = "12px"; const findRsCell = ( bounds: DOMRect, From 508959fff02e296261b2933843ff4e577964b847 Mon Sep 17 00:00:00 2001 From: Andy Cork Date: Fri, 21 Mar 2025 15:56:25 +0000 Subject: [PATCH 15/15] 3.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f9d56887..66b9bc4e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@wearemothership/react-scheduler", - "version": "3.0.6", + "version": "3.1.0", "description": "React scheduler component based on Material-UI & date-fns", "files": [ "*"