react-weekly-schedule
Version:
 
1 lines • 91 kB
Source Map (JSON)
{"version":3,"file":"index.mjs","sources":["src/components/DefaultEventRootComponent.tsx","src/context.ts","src/utils/createPageMapCoordsToContainer.ts","src/hooks/useClickAndDrag.ts","src/hooks/useMousetrap.ts","src/utils/getSpan.ts","src/utils/createGrid.ts","src/utils/cellToDate.ts","src/utils/createMapCellInfoToRecurringTimeRange.ts","src/utils/createMapDateRangeToCells.ts","src/utils/getEarliestTimeRange.ts","src/utils/mergeEvents.ts","src/components/Cell.tsx","src/utils/getTextForDateRange.ts","src/components/EventContent.tsx","src/components/RangeBox.tsx","src/components/Schedule.tsx","src/components/TimeGridScheduler.tsx"],"sourcesContent":["import React from 'react';\nimport { EventRootProps } from '../types';\n\nexport const DefaultEventRootComponent = React.forwardRef<any, EventRootProps>(\n function DefaultEventRootComponent(\n {\n isActive,\n handleDelete,\n cellIndex,\n rangeIndex,\n classes,\n disabled,\n ...props\n },\n ref,\n ) {\n return <div ref={ref} aria-disabled={disabled} {...props} />;\n },\n);\n","import en from 'date-fns/locale/en';\nimport { createContext } from 'react';\n\nexport const SchedulerContext = createContext({ locale: en });\n","export const createPageMapCoordsToContainer = (container: HTMLElement) => {\n return (event: React.MouseEvent | MouseEvent | TouchEvent) => {\n let clientX: number;\n let clientY: number;\n let pageX: number;\n let pageY: number;\n\n if ('changedTouches' in event) {\n ({ clientX, clientY, pageX, pageY } = event.changedTouches[0]);\n } else {\n ({ clientX, clientY, pageX, pageY } = event);\n }\n const { top, left } = container.getBoundingClientRect();\n\n return {\n clientX,\n clientY,\n pageX,\n pageY,\n top,\n left,\n x: clientX - left,\n y: clientY - top,\n };\n };\n};\n","import React, { useCallback, useEffect, useState } from 'react';\nimport { fromEvent, merge, Observable, of } from 'rxjs';\nimport {\n delay,\n filter,\n map,\n mergeMap,\n startWith,\n takeUntil,\n tap,\n} from 'rxjs/operators';\nimport { Rect } from '../types';\nimport { createPageMapCoordsToContainer } from '../utils/createPageMapCoordsToContainer';\n\nconst prevent = tap((e: TouchEvent) => {\n e.preventDefault();\n e.stopPropagation();\n});\n\nexport function useClickAndDrag(\n ref: React.RefObject<HTMLElement>,\n isDisabled?: boolean,\n) {\n const [style, setStyle] = useState<React.CSSProperties>({\n transform: 'translate(0, 0)',\n width: 0,\n height: 0,\n });\n const [box, setBox] = useState<Rect | null>(null);\n const [isDragging, setIsDragging] = useState(false);\n const [hasFinishedDragging, setHasFinishedDragging] = useState(false);\n const container = ref.current;\n\n useEffect(() => {\n if (!container || isDisabled) {\n return;\n }\n\n const mapCoordsToContainer = createPageMapCoordsToContainer(container);\n\n const touchMove$ = fromEvent<TouchEvent>(window, 'touchmove', {\n passive: false,\n }).pipe(prevent);\n\n const touchEnd$ = fromEvent<TouchEvent>(window, 'touchend', {\n passive: true,\n });\n\n const touchStart$ = fromEvent<TouchEvent>(container, 'touchstart', {\n passive: false,\n });\n\n const touchStartWithDelay$ = touchStart$.pipe(\n mergeMap(start =>\n of(start).pipe(\n delay(300),\n takeUntil(touchMove$),\n prevent,\n ),\n ),\n );\n\n const mouseDown$ = fromEvent<MouseEvent>(container, 'mousedown', {\n passive: true,\n }).pipe(filter(event => event.which === 1));\n\n const mouseMove$ = fromEvent<MouseEvent>(window, 'mousemove', {\n passive: true,\n });\n\n const mouseUp$ = fromEvent<MouseEvent>(window, 'mouseup', {\n passive: true,\n });\n\n const dragStart$ = merge(mouseDown$, touchStartWithDelay$).pipe(\n map(mapCoordsToContainer),\n );\n\n const dragEnd$ = merge(mouseUp$, touchEnd$).pipe(\n map(mapCoordsToContainer),\n tap(() => {\n setIsDragging(false);\n setHasFinishedDragging(true);\n }),\n );\n\n const move$ = merge(mouseMove$, touchMove$).pipe(map(mapCoordsToContainer));\n\n const box$: Observable<Rect | null> = dragStart$.pipe(\n tap(() => {\n setIsDragging(true);\n setHasFinishedDragging(false);\n }),\n mergeMap(down => {\n return move$.pipe(\n startWith(down),\n map(\n (move): Rect => {\n const startX = Math.max(down.x, 0);\n const startY = Math.max(down.y, 0);\n const endX = Math.min(move.x, container.scrollWidth);\n const endY = Math.min(move.y, container.scrollHeight);\n const top = Math.min(startY, endY);\n const bottom = Math.max(startY, endY);\n const left = Math.min(startX, endX);\n const right = Math.max(startX, endX);\n\n return {\n startX,\n startY,\n endX,\n endY,\n top,\n bottom,\n left,\n right,\n width: right - left,\n height: bottom - top,\n };\n },\n ),\n takeUntil(dragEnd$),\n );\n }),\n map(rect => {\n return rect.width === 0 && rect.height === 0 ? null : rect;\n }),\n );\n\n const style$ = box$.pipe(\n map(rect => {\n if (rect !== null) {\n const { width, height, left, top } = rect;\n return {\n transform: `translate(${left}px, ${top}px)`,\n width,\n height,\n };\n }\n\n return { display: 'none' };\n }),\n );\n\n const boxSubscriber = box$.subscribe(setBox);\n const styleSubscriber = style$.subscribe(setStyle);\n\n return () => {\n boxSubscriber.unsubscribe();\n styleSubscriber.unsubscribe();\n };\n }, [container, isDisabled]);\n\n const cancel = useCallback(() => {\n setIsDragging(false);\n setHasFinishedDragging(false);\n setBox(null);\n }, []);\n\n return { style, box, isDragging, cancel, hasFinishedDragging };\n}\n","import Mousetrap from 'mousetrap';\nimport { useEffect, useRef } from 'react';\n\n/**\n * Use mousetrap hook\n *\n * @param handlerKey - A key, key combo or array of combos according to Mousetrap documentation.\n * @param handlerCallback - A function that is triggered on key combo catch.\n */\nexport function useMousetrap(\n handlerKey: string | string[],\n handlerCallback: (e: ExtendedKeyboardEvent, combo: string) => void,\n elementOrElementRef: typeof document | React.RefObject<Element | null>,\n) {\n const actionRef = useRef<typeof handlerCallback | null>(null);\n actionRef.current = handlerCallback;\n const element =\n 'current' in elementOrElementRef ? elementOrElementRef.current : document;\n\n useEffect(() => {\n const instance = new Mousetrap(element as Element);\n\n instance.bind(handlerKey, (e, combo) => {\n typeof actionRef.current === 'function' && actionRef.current(e, combo);\n });\n\n return () => {\n instance.unbind(handlerKey);\n };\n }, [handlerKey, element]);\n}\n","export const getSpan = (x1: number, x2: number) => 1 + Math.abs(x2 - x1);\n","import clamp from 'lodash/clamp';\nimport floor from 'lodash/floor';\nimport round from 'lodash/round';\nimport { CellInfo, Grid, Rect } from '../types';\nimport { getSpan } from './getSpan';\n\nexport const createGrid = ({\n totalHeight,\n totalWidth,\n numVerticalCells,\n numHorizontalCells,\n}: {\n totalHeight: number;\n totalWidth: number;\n numVerticalCells: number;\n numHorizontalCells: number;\n}): Grid => {\n const cellHeight = totalHeight / numVerticalCells;\n const cellWidth = totalWidth / numHorizontalCells;\n\n return {\n totalHeight,\n totalWidth,\n numVerticalCells,\n numHorizontalCells,\n cellWidth,\n cellHeight,\n\n getRectFromCell(data: CellInfo) {\n const { endX, startX, endY, startY, spanX, spanY } = data;\n const bottom = endY * this.cellHeight;\n const top = startY * this.cellHeight;\n const left = startX * this.cellWidth;\n const right = endX * this.cellWidth;\n const height = spanY * this.cellHeight;\n const width = spanX * this.cellWidth;\n\n return {\n bottom,\n top,\n left,\n right,\n height,\n width,\n\n // @TODO: check the math\n startX: startX * this.cellWidth,\n endX: endX * this.cellWidth,\n startY: startY * this.cellHeight,\n endY: endY * this.cellHeight,\n };\n },\n\n getCellFromRect(data: Rect) {\n const startX = clamp(\n floor(data.left / this.cellWidth),\n 0,\n numHorizontalCells - 1,\n );\n const startY = clamp(\n round(data.top / this.cellHeight),\n 0,\n numVerticalCells - 1,\n );\n const endX = clamp(\n floor(data.right / this.cellWidth),\n 0,\n numHorizontalCells - 1,\n );\n const endY = clamp(\n round(data.bottom / this.cellHeight),\n 0,\n numVerticalCells - 1,\n );\n const spanX = clamp(getSpan(startX, endX), 1, numHorizontalCells);\n const spanY = clamp(getSpan(startY, endY), 1, numVerticalCells);\n\n return {\n spanX,\n spanY,\n startX,\n startY,\n endX,\n endY,\n };\n },\n };\n};\n","import addDays from 'date-fns/add_days';\nimport addMinutes from 'date-fns/add_minutes';\n\nexport const cellToDate = ({\n startX,\n startY,\n toMin,\n originDate,\n}: {\n startX: number;\n startY: number;\n toMin: (y: number) => number;\n toDay: (x: number) => number;\n originDate: Date;\n}) => addMinutes(addDays(originDate, startX), toMin(startY));\n","import addMinutes from 'date-fns/add_minutes';\nimport compareAsc from 'date-fns/compare_asc';\nimport endOfDay from 'date-fns/end_of_day';\nimport isBefore from 'date-fns/is_before';\nimport min from 'date-fns/min';\nimport range from 'lodash/range';\nimport { DateRange, MapCellInfoToDateRange } from '../types';\nimport { cellToDate } from './cellToDate';\n\nexport type RecurringTimeRange = DateRange[];\n\nexport const createMapCellInfoToRecurringTimeRange: MapCellInfoToDateRange = ({\n fromY: toMin,\n fromX: toDay,\n originDate,\n}) => ({ startX, startY, endX, spanY }) => {\n const result = range(startX, endX + 1)\n .map(i => {\n const startDate = cellToDate({\n startX: i,\n startY,\n toMin,\n toDay,\n originDate,\n });\n let endDate = min(\n addMinutes(startDate, toMin(spanY)),\n endOfDay(startDate),\n );\n\n const range: DateRange = isBefore(startDate, endDate)\n ? [startDate, endDate]\n : [endDate, startDate];\n\n return range;\n })\n .sort((range1, range2) => compareAsc(range1[0], range2[0]));\n\n return result;\n};\n","import differenceInDays from 'date-fns/difference_in_days';\nimport differenceInMinutes from 'date-fns/difference_in_minutes';\nimport isEqual from 'date-fns/is_equal';\nimport startOfDay from 'date-fns/start_of_day';\nimport range from 'lodash/range';\nimport { CellInfo, DateRange } from '../types';\nimport { getSpan } from './getSpan';\n\nexport const createMapDateRangeToCells = ({\n toX = (x: number) => x,\n toY,\n numVerticalCells,\n originDate,\n}: {\n toX: (day: number) => number;\n toY: (min: number) => number;\n numHorizontalCells: number;\n numVerticalCells: number;\n originDate: Date;\n}) => ([start, end]: DateRange): CellInfo[] => {\n const originOfThisDay = startOfDay(start);\n const _startX = toX(differenceInDays(start, originDate));\n const _startY = toY(differenceInMinutes(start, originOfThisDay));\n const _endX = toX(differenceInDays(end, originDate));\n const _endY = toY(differenceInMinutes(end, startOfDay(end))) - 1;\n\n const cells = range(_startX, _endX + 1).map(i => {\n const startX = i;\n const endX = i;\n const atStart = i === _startX;\n const atEnd = i === _endX;\n const startY = !atStart ? 0 : _startY;\n const endY = !atEnd ? numVerticalCells - 1 : _endY;\n const spanX = getSpan(startX, endX);\n const spanY = getSpan(startY, endY);\n\n return {\n startX,\n startY,\n endX,\n endY,\n spanX,\n spanY,\n };\n });\n\n if (isEqual(end, startOfDay(end))) {\n cells.pop();\n }\n\n return cells;\n};\n","import compareAsc from 'date-fns/compare_asc';\nimport setDay from 'date-fns/set_day';\nimport { DateRange } from '../types';\n\nexport function getEarliestTimeRange(\n ranges: DateRange[],\n): DateRange | undefined {\n return [...ranges].sort(([startA], [startB]) =>\n compareAsc(setDay(startA, 0), setDay(startB, 0)),\n )[0];\n}\n","import compareAsc from 'date-fns/compare_asc';\nimport _mergeRanges from 'merge-ranges';\nimport { ScheduleType } from '../types';\n\nexport function mergeRanges(event: ScheduleType): ScheduleType {\n return _mergeRanges(\n [...event].map(d => d.map(c => new Date(c)) as [Date, Date]),\n );\n}\n\nexport function mergeEvents(\n event1: ScheduleType,\n event2: ScheduleType | null,\n): ScheduleType {\n if (event2 === null) {\n return event1;\n }\n\n return mergeRanges([...event1, ...event2]).sort((range1, range2) =>\n compareAsc(range1[0], range2[0]),\n );\n}\n","import classcat from 'classcat';\nimport getMinutes from 'date-fns/get_minutes';\nimport React from 'react';\nimport { CellInfo, ClassNames, DateRange } from '../types';\n\nexport const Cell = React.memo(function Cell({\n timeIndex,\n children,\n classes,\n getDateRangeForVisualGrid,\n onClick,\n}: {\n timeIndex: number;\n classes: ClassNames;\n getDateRangeForVisualGrid(cell: CellInfo): DateRange[];\n children?(options: { start: Date; isHourStart: boolean }): React.ReactNode;\n onClick?: React.MouseEventHandler;\n}) {\n const [[start]] = getDateRangeForVisualGrid({\n startX: 0,\n startY: timeIndex,\n endX: 0,\n endY: timeIndex + 1,\n spanX: 1,\n spanY: 1,\n });\n\n const isHourStart = getMinutes(start) === 0;\n\n return (\n <div\n role=\"button\"\n onClick={onClick}\n className={classcat([\n classes.cell,\n { [classes['is-hour-start']]: isHourStart },\n ])}\n >\n {children && children({ start, isHourStart })}\n </div>\n );\n});\n","import format from 'date-fns/format';\nimport getMinutes from 'date-fns/get_minutes';\nimport isSameDay from 'date-fns/is_same_day';\n\nconst formatTemplate = 'ddd h:mma';\n\nconst dropSame = (\n dates: [Date, Date],\n template: string,\n takeSecond: boolean = false,\n locale: typeof import('date-fns/locale/en'),\n): [string, string] => {\n const [first, second] = dates.map(date => format(date, template, { locale }));\n if (first !== second) {\n return [first, second];\n }\n\n if (takeSecond) {\n return ['', second];\n }\n\n return [first, ''];\n};\n\nconst formatHour = (\n date: Date,\n locale: typeof import('date-fns/locale/en'),\n) => {\n if (getMinutes(date) === 0) {\n return format(date, 'h', { locale });\n }\n\n return format(date, 'h:mm', { locale });\n};\n\ntype Options = {\n dateRange: [Date, Date];\n locale: typeof import('date-fns/locale/en');\n template?: string;\n template2?: string;\n includeDayIfSame?: boolean;\n};\n\nexport const getFormattedComponentsForDateRange = ({\n dateRange,\n locale,\n template,\n template2,\n includeDayIfSame = true,\n}: Options) => {\n const start = dateRange[0];\n const end = dateRange[dateRange.length - 1];\n\n if (isSameDay(start, end) && !template) {\n const [firstM, secondM] = dropSame(dateRange, 'a', true, locale);\n const day = includeDayIfSame ? `${format(start, 'ddd', { locale })} ` : '';\n return [\n `${day}${formatHour(start, {\n locale,\n })}${firstM}`,\n `${formatHour(end, { locale })}${secondM}`,\n ];\n }\n\n const startDateStr = format(start, template || formatTemplate, { locale });\n const endDateStr = format(end, template2 || formatTemplate, { locale });\n\n return [startDateStr, endDateStr];\n};\n\nexport const getTextForDateRange = (options: Options) => {\n return getFormattedComponentsForDateRange(options).join(' – ');\n};\n","// @ts-ignore\nimport VisuallyHidden from '@reach/visually-hidden';\nimport React, { useContext } from 'react';\nimport { SchedulerContext } from '../context';\nimport { ClassNames } from '../types';\nimport {\n getFormattedComponentsForDateRange,\n getTextForDateRange,\n} from '../utils/getTextForDateRange';\n\nexport type EventContentProps = {\n width: number;\n height: number;\n classes: ClassNames;\n dateRange: [Date, Date];\n isStart: boolean;\n isEnd: boolean;\n};\n\nexport const EventContent = React.memo(function EventContent({\n width,\n height,\n classes,\n dateRange,\n isStart,\n isEnd,\n}: EventContentProps) {\n const { locale } = useContext(SchedulerContext);\n const [start, end] = getFormattedComponentsForDateRange({\n dateRange,\n locale,\n includeDayIfSame: false,\n });\n\n return (\n <div\n style={{ width: width - 20, height }}\n className={classes['event-content']}\n >\n <VisuallyHidden>\n {getTextForDateRange({ dateRange, locale })}\n </VisuallyHidden>\n <span aria-hidden className={classes.start}>\n {isStart && start}\n </span>\n <span aria-hidden className={classes.end}>\n {isEnd && end}\n </span>\n </div>\n );\n});\n","import classcat from 'classcat';\nimport invariant from 'invariant';\nimport Resizable, { ResizeCallback } from 're-resizable';\nimport React, {\n useCallback,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport Draggable, { DraggableEventHandler } from 'react-draggable';\nimport { useMousetrap } from '../hooks/useMousetrap';\nimport { CellInfo } from '../types';\nimport { DefaultEventRootComponent } from './DefaultEventRootComponent';\nimport { EventContent } from './EventContent';\nimport { ScheduleProps } from './Schedule';\n\nexport const RangeBox = React.memo(function RangeBox({\n classes,\n grid,\n rangeIndex,\n cellIndex,\n cellArray,\n cell,\n className,\n onChange,\n cellInfoToDateRange,\n isResizable,\n moveAxis,\n onActiveChange,\n onClick,\n getIsActive,\n eventContentComponent: EventContentComponent = EventContent,\n eventRootComponent: EventRootComponent = DefaultEventRootComponent,\n disabled,\n}: ScheduleProps & {\n cellIndex: number;\n cellArray: CellInfo[];\n className?: string;\n rangeIndex: number;\n cell: CellInfo;\n}) {\n const ref = useRef(null);\n const [modifiedCell, setModifiedCell] = useState(cell);\n const originalRect = useMemo(() => grid.getRectFromCell(cell), [cell, grid]);\n const rect = useMemo(() => grid.getRectFromCell(modifiedCell), [\n modifiedCell,\n grid,\n ]);\n\n useEffect(() => {\n setModifiedCell(cell);\n }, [cell]);\n\n const modifiedDateRange = useMemo(() => cellInfoToDateRange(modifiedCell), [\n cellInfoToDateRange,\n modifiedCell,\n ]);\n\n const { top, left, width, height } = rect;\n\n const isStart = cellIndex === 0;\n const isEnd = cellIndex === cellArray.length - 1;\n\n const handleStop = useCallback(() => {\n if (!onChange || disabled) {\n return;\n }\n\n onChange(cellInfoToDateRange(modifiedCell), rangeIndex);\n }, [modifiedCell, rangeIndex, disabled, cellInfoToDateRange, onChange]);\n\n const isActive = useMemo(() => getIsActive({ cellIndex, rangeIndex }), [\n cellIndex,\n rangeIndex,\n getIsActive,\n ]);\n\n useMousetrap(\n 'up',\n () => {\n if (!onChange || disabled || !isActive) {\n return;\n }\n\n if (moveAxis === 'none' || moveAxis === 'x') {\n return;\n }\n\n if (modifiedCell.startY === 0) {\n return;\n }\n\n const newCell = {\n ...modifiedCell,\n startY: modifiedCell.startY - 1,\n endY: modifiedCell.endY - 1,\n };\n\n onChange(cellInfoToDateRange(newCell), rangeIndex);\n },\n ref,\n );\n\n useMousetrap(\n 'shift+up',\n () => {\n if (!onChange || !isResizable || disabled || !isActive) {\n return;\n }\n\n if (\n modifiedCell.endY === modifiedCell.startY ||\n modifiedCell.spanY === 0\n ) {\n return;\n }\n\n const newCell = {\n ...modifiedCell,\n endY: modifiedCell.endY - 1,\n spanY: modifiedCell.spanY - 1,\n };\n\n onChange(cellInfoToDateRange(newCell), rangeIndex);\n },\n ref,\n );\n\n useMousetrap(\n 'down',\n () => {\n if (!onChange || disabled || !isActive) {\n return;\n }\n\n if (moveAxis === 'none' || moveAxis === 'x') {\n return;\n }\n\n if (Math.round(modifiedCell.endY) >= grid.numVerticalCells - 1) {\n return;\n }\n\n const newCell = {\n ...modifiedCell,\n startY: modifiedCell.startY + 1,\n endY: modifiedCell.endY + 1,\n };\n\n onChange(cellInfoToDateRange(newCell), rangeIndex);\n },\n ref,\n );\n\n useMousetrap(\n 'shift+down',\n () => {\n if (!onChange || !isResizable || disabled || !isActive) {\n return;\n }\n\n if (moveAxis === 'none' || moveAxis === 'x') {\n return;\n }\n\n if (Math.round(modifiedCell.endY) >= grid.numVerticalCells - 1) {\n return;\n }\n\n const newCell = {\n ...modifiedCell,\n spanY: modifiedCell.spanY + 1,\n endY: modifiedCell.endY + 1,\n };\n\n onChange(cellInfoToDateRange(newCell), rangeIndex);\n },\n ref,\n );\n\n const handleDrag: DraggableEventHandler = useCallback(\n (event, { y, x }) => {\n if (moveAxis === 'none' || disabled) {\n return;\n }\n\n event.preventDefault();\n event.stopPropagation();\n\n const newRect = {\n ...rect,\n };\n\n if (moveAxis === 'both' || moveAxis === 'y') {\n const startOrEnd1 = y;\n const startOrEnd2 = startOrEnd1 + rect.height;\n const newTop = Math.min(startOrEnd1, startOrEnd2);\n const newBottom = newTop + rect.height;\n newRect.bottom = newBottom;\n newRect.top = newTop;\n }\n\n if (moveAxis === 'both' || moveAxis === 'x') {\n const startOrEnd1 = x;\n const startOrEnd2 = startOrEnd1 + rect.width;\n const newLeft = Math.min(startOrEnd1, startOrEnd2);\n const newRight = newLeft + rect.width;\n newRect.right = newRight;\n newRect.left = newLeft;\n }\n\n const { startY, startX } = grid.getCellFromRect(newRect);\n\n const newCell = {\n ...cell,\n startX: moveAxis === 'y' ? cell.startX : startX,\n endX: moveAxis === 'x' ? startX + cell.spanX - 1 : cell.endX,\n startY: moveAxis === 'x' ? cell.startY : startY,\n endY: moveAxis === 'y' ? startY + cell.spanY - 1 : cell.endY,\n };\n\n invariant(\n newCell.spanY === cell.spanY && newCell.spanX === cell.spanX,\n `Expected the dragged time cell to have the same dimensions`,\n );\n\n setModifiedCell(newCell);\n },\n [grid, rect, moveAxis, disabled, cell, setModifiedCell],\n );\n\n const handleResize: ResizeCallback = useCallback(\n (event, direction, _ref, delta) => {\n if (!isResizable || disabled) {\n return;\n }\n\n event.preventDefault();\n event.stopPropagation();\n\n if (delta.height === 0) {\n return;\n }\n\n const newSize = {\n height: delta.height + rect.height,\n width: delta.width + rect.width + 20,\n };\n\n const newRect = {\n ...originalRect,\n ...newSize,\n };\n\n if (direction.includes('top')) {\n newRect.top -= delta.height;\n } else if (direction.includes('bottom')) {\n newRect.bottom += delta.height;\n }\n\n const { spanY, startY, endY } = grid.getCellFromRect(newRect);\n const newCell = {\n ...cell,\n spanY,\n startY,\n endY,\n };\n\n setModifiedCell(newCell);\n },\n [grid, rect, disabled, isResizable, setModifiedCell, cell, originalRect],\n );\n\n const handleDelete = useCallback(() => {\n if (!onChange || disabled) {\n return;\n }\n\n onChange(undefined, rangeIndex);\n }, [onChange, disabled, rangeIndex]);\n\n const handleOnFocus = useCallback(() => {\n if (!onActiveChange || disabled) {\n return;\n }\n\n onActiveChange([rangeIndex, cellIndex]);\n }, [onActiveChange, disabled, rangeIndex, cellIndex]);\n\n const handleOnClick = useCallback(() => {\n if (!onClick || disabled || !isActive) {\n return;\n }\n\n onClick([rangeIndex, cellIndex]);\n }, [onClick, rangeIndex, disabled, isActive, cellIndex]);\n\n useMousetrap('enter', handleOnClick, ref);\n\n const cancelClasses = useMemo(\n () =>\n classes.handle\n ? classes.handle\n .split(' ')\n .map(className => `.${className}`)\n .join(', ')\n : undefined,\n [classes.handle],\n );\n\n return (\n <Draggable\n axis={moveAxis}\n bounds={{\n top: 0,\n bottom: grid.totalHeight - height,\n left: 0,\n right: grid.totalWidth,\n }}\n position={{ x: left, y: top }}\n onDrag={handleDrag}\n onStop={handleStop}\n cancel={cancelClasses}\n disabled={disabled}\n >\n <EventRootComponent\n role=\"button\"\n disabled={disabled}\n onFocus={handleOnFocus}\n onClick={handleOnClick}\n handleDelete={handleDelete}\n cellIndex={cellIndex}\n rangeIndex={rangeIndex}\n isActive={isActive}\n classes={classes}\n className={classcat([\n classes.event,\n classes['range-boxes'],\n className,\n {\n [classes['is-draggable']]: !disabled && moveAxis !== 'none',\n [classes['is-disabled']]: disabled,\n },\n ])}\n ref={ref}\n style={{ width: width - 20, height }}\n >\n <Resizable\n size={{ ...originalRect, width: originalRect.width - 20 }}\n key={`${rangeIndex}.${cellIndex}.${cellArray.length}.${\n originalRect.top\n }.${originalRect.left}`}\n onResize={handleResize}\n onResizeStop={handleStop}\n handleWrapperClass={classes['handle-wrapper']}\n enable={\n isResizable && !disabled\n ? {\n top: true,\n bottom: true,\n }\n : {}\n }\n handleClasses={{\n bottom: classcat([classes.handle, classes.bottom]),\n bottomLeft: classes.handle,\n bottomRight: classes.handle,\n left: classes.handle,\n right: classes.handle,\n top: classcat([classes.handle, classes.top]),\n topLeft: classes.handle,\n topRight: classes.handle,\n }}\n >\n <EventContentComponent\n width={width}\n height={height}\n classes={classes}\n dateRange={modifiedDateRange}\n isStart={isStart}\n isEnd={isEnd}\n />\n </Resizable>\n </EventRootComponent>\n </Draggable>\n );\n});\n","import React from 'react';\nimport {\n CellInfo,\n ClassNames,\n DateRange,\n Grid,\n OnChangeCallback,\n ScheduleType,\n} from '../types';\nimport { RangeBox } from './RangeBox';\n\nexport type ScheduleProps = {\n classes: ClassNames;\n grid: Grid;\n onChange?: OnChangeCallback;\n isResizable?: boolean;\n isDeletable?: boolean;\n moveAxis: 'none' | 'both' | 'x' | 'y';\n cellInfoToDateRange(cell: CellInfo): DateRange;\n onActiveChange?(index: [number, number] | [null, null]): void;\n onClick?(index: [number, number] | [null, null]): void;\n getIsActive(indexes: { cellIndex: number; rangeIndex: number }): boolean;\n eventContentComponent?: any;\n eventRootComponent?: any;\n disabled?: boolean;\n};\n\nexport const Schedule = React.memo(function Schedule({\n classes,\n ranges,\n grid,\n className,\n onChange,\n isResizable,\n isDeletable,\n moveAxis,\n cellInfoToDateRange,\n dateRangeToCells,\n onActiveChange,\n eventContentComponent,\n eventRootComponent,\n onClick,\n getIsActive,\n disabled,\n}: {\n dateRangeToCells(range: DateRange): CellInfo[];\n ranges: ScheduleType;\n className?: string;\n classes: ClassNames;\n} & ScheduleProps) {\n return (\n <div className={classes['range-boxes']}>\n {ranges.map((dateRange, rangeIndex) => {\n return (\n <span key={rangeIndex}>\n {dateRangeToCells(dateRange).map((cell, cellIndex, cellArray) => {\n return (\n <RangeBox\n classes={classes}\n onActiveChange={onActiveChange}\n key={`${rangeIndex}.${ranges.length}.${cellIndex}.${\n cellArray.length\n }`}\n isResizable={isResizable}\n moveAxis={moveAxis}\n isDeletable={isDeletable}\n cellInfoToDateRange={cellInfoToDateRange}\n cellArray={cellArray}\n cellIndex={cellIndex}\n rangeIndex={rangeIndex}\n className={className}\n onChange={onChange}\n onClick={onClick}\n grid={grid}\n cell={cell}\n getIsActive={getIsActive}\n eventContentComponent={eventContentComponent}\n eventRootComponent={eventRootComponent}\n disabled={disabled}\n />\n );\n })}\n </span>\n );\n })}\n </div>\n );\n});\n","import useComponentSize from '@rehooks/component-size';\nimport classcat from 'classcat';\nimport addDays from 'date-fns/add_days';\nimport addHours from 'date-fns/add_hours';\nimport format from 'date-fns/format';\nimport isDateEqual from 'date-fns/is_equal';\nimport startOfDay from 'date-fns/start_of_day';\nimport invariant from 'invariant';\nimport isEqual from 'lodash/isEqual';\nimport times from 'lodash/times';\nimport React, {\n useCallback,\n useContext,\n useEffect,\n useMemo,\n useRef,\n useState,\n} from 'react';\nimport scrollIntoView from 'scroll-into-view-if-needed';\nimport { SchedulerContext } from '../context';\nimport { useClickAndDrag } from '../hooks/useClickAndDrag';\nimport { useMousetrap } from '../hooks/useMousetrap';\nimport {\n CellInfo,\n ClassNames,\n DateRange,\n Grid,\n OnChangeCallback,\n ScheduleType,\n} from '../types';\nimport { createGrid } from '../utils/createGrid';\nimport {\n createMapCellInfoToRecurringTimeRange,\n RecurringTimeRange,\n} from '../utils/createMapCellInfoToRecurringTimeRange';\nimport { createMapDateRangeToCells } from '../utils/createMapDateRangeToCells';\nimport { getEarliestTimeRange } from '../utils/getEarliestTimeRange';\nimport { getSpan } from '../utils/getSpan';\nimport { mergeEvents, mergeRanges } from '../utils/mergeEvents';\nimport { Cell } from './Cell';\nimport { Schedule, ScheduleProps } from './Schedule';\n\nconst MINS_IN_DAY = 24 * 60;\nconst horizontalPrecision = 1;\nconst toDay = (x: number): number => x * horizontalPrecision;\nconst toX = (days: number): number => days / horizontalPrecision;\nconst DELETE_KEYS = ['del', 'backspace'];\n\nexport const TimeGridScheduler = React.memo(function TimeGridScheduler({\n verticalPrecision = 30,\n visualGridVerticalPrecision = 30,\n cellClickPrecision = visualGridVerticalPrecision,\n style,\n schedule,\n originDate: _originDate = new Date(),\n defaultHours = [9, 15],\n classes,\n className,\n onChange,\n onEventClick,\n eventContentComponent,\n eventRootComponent,\n disabled,\n}: {\n originDate?: Date;\n\n /**\n * The minimum number of minutes a created range can span\n * @default 30\n */\n verticalPrecision?: number;\n\n /**\n * The visual grid increments in minutes.\n * @default 30\n */\n visualGridVerticalPrecision?: number;\n\n /**\n * The minimum number of minutes for an time block\n * created with a single click.\n * @default visualGridVerticalPrecision\n */\n cellClickPrecision?: number;\n\n /** Custom styles applied to the root of the view */\n style?: React.CSSProperties;\n schedule: ScheduleType;\n\n /**\n * A map of class names to the scoped class names\n * The keys are class names like `'root'` and the values\n * are the corresponding class names which can be scoped\n * with CSS Modules, e.g. `'_root_7f2c6'`.\n */\n classes: ClassNames;\n className?: string;\n\n /**\n * The view will initially be scrolled to these hours.\n * Defaults to work hours (9-17).\n * @default [9, 17]\n */\n defaultHours?: [number, number];\n onChange(newSchedule: ScheduleType): void;\n onEventClick?: ScheduleProps['onClick'];\n eventContentComponent?: ScheduleProps['eventContentComponent'];\n eventRootComponent?: ScheduleProps['eventRootComponent'];\n disabled?: boolean;\n}) {\n const { locale } = useContext(SchedulerContext);\n const originDate = useMemo(() => startOfDay(_originDate), [_originDate]);\n const numVerticalCells = MINS_IN_DAY / verticalPrecision;\n const numHorizontalCells = 7 / horizontalPrecision;\n const toMin = useCallback((y: number) => y * verticalPrecision, [\n verticalPrecision,\n ]);\n const toY = useCallback((mins: number): number => mins / verticalPrecision, [\n verticalPrecision,\n ]);\n\n const cellInfoToDateRanges = useMemo(() => {\n return createMapCellInfoToRecurringTimeRange({\n originDate,\n fromY: toMin,\n fromX: toDay,\n });\n }, [toMin, originDate]);\n\n const cellInfoToSingleDateRange = useCallback(\n (cell: CellInfo): DateRange => {\n const [first, ...rest] = cellInfoToDateRanges(cell);\n invariant(\n rest.length === 0,\n `Expected \"cellInfoToSingleDateRange\" to return a single date range, found ${\n rest.length\n } additional ranges instead. This is a bug in @remotelock/react-week-scheduler`,\n );\n\n return first;\n },\n [cellInfoToDateRanges],\n );\n\n const dateRangeToCells = useMemo(() => {\n return createMapDateRangeToCells({\n originDate,\n numVerticalCells,\n numHorizontalCells,\n toX,\n toY,\n });\n }, [toY, numVerticalCells, numHorizontalCells, originDate]);\n\n const root = useRef<HTMLDivElement | null>(null);\n const parent = useRef<HTMLDivElement | null>(null);\n\n const size = useComponentSize(parent);\n const {\n style: dragBoxStyle,\n box,\n isDragging,\n hasFinishedDragging,\n cancel,\n } = useClickAndDrag(parent, disabled);\n const [\n pendingCreation,\n setPendingCreation,\n ] = useState<RecurringTimeRange | null>(null);\n\n const [[totalHeight, totalWidth], setDimensions] = useState([0, 0]);\n\n const numVisualVerticalCells = (24 * 60) / visualGridVerticalPrecision;\n\n useEffect(\n function updateGridDimensionsOnSizeOrCellCountChange() {\n if (!parent.current) {\n setDimensions([0, 0]);\n return;\n }\n\n setDimensions([parent.current.scrollHeight, parent.current.scrollWidth]);\n },\n [size, numVisualVerticalCells],\n );\n\n const grid = useMemo<Grid | null>(() => {\n if (totalHeight === null || totalWidth === null) {\n return null;\n }\n\n return createGrid({\n totalHeight,\n totalWidth,\n numHorizontalCells,\n numVerticalCells,\n });\n }, [totalHeight, totalWidth, numHorizontalCells, numVerticalCells]);\n\n useEffect(\n function updatePendingCreationOnDragBoxUpdate() {\n if (grid === null || box === null) {\n setPendingCreation(null);\n return;\n }\n\n const cell = grid.getCellFromRect(box);\n const dateRanges = cellInfoToDateRanges(cell);\n const event = dateRanges;\n setPendingCreation(event);\n },\n [box, grid, cellInfoToDateRanges, toY],\n );\n\n const [[activeRangeIndex, activeCellIndex], setActive] = useState<\n [number, number] | [null, null]\n >([null, null]);\n\n useEffect(\n function updateScheduleAfterDraggingFinished() {\n if (disabled) {\n return;\n }\n\n if (hasFinishedDragging) {\n onChange(mergeEvents(schedule, pendingCreation));\n setPendingCreation(null);\n }\n },\n [\n hasFinishedDragging,\n disabled,\n onChange,\n setPendingCreation,\n pendingCreation,\n schedule,\n ],\n );\n\n useEffect(\n function clearActiveBlockAfterCreation() {\n if (pendingCreation === null) {\n setActive([null, null]);\n }\n },\n [pendingCreation],\n );\n\n const handleEventChange = useCallback<OnChangeCallback>(\n (newDateRange, rangeIndex) => {\n if (disabled) {\n return;\n }\n\n if (!schedule && newDateRange) {\n onChange([newDateRange]);\n\n return;\n }\n\n let newSchedule = [...schedule];\n\n if (!newDateRange) {\n newSchedule.splice(rangeIndex, 1);\n } else {\n if (\n isDateEqual(newDateRange[0], newSchedule[rangeIndex][0]) &&\n isDateEqual(newDateRange[1], newSchedule[rangeIndex][1])\n ) {\n return;\n }\n newSchedule[rangeIndex] = newDateRange;\n }\n\n newSchedule = mergeRanges(newSchedule);\n\n onChange(newSchedule);\n },\n [schedule, onChange, disabled],\n );\n\n useMousetrap(\n 'esc',\n function cancelOnEsc() {\n if (pendingCreation) {\n cancel();\n }\n },\n document,\n );\n\n const getIsActive = useCallback(\n ({ rangeIndex, cellIndex }) => {\n return rangeIndex === activeRangeIndex && cellIndex === activeCellIndex;\n },\n [activeCellIndex, activeRangeIndex],\n );\n\n const handleDelete = useCallback(\n (e: ExtendedKeyboardEvent) => {\n if (activeRangeIndex === null || disabled) {\n return;\n }\n\n e.preventDefault();\n e.stopPropagation();\n handleEventChange(undefined, activeRangeIndex);\n },\n [activeRangeIndex, disabled, handleEventChange],\n );\n\n useMousetrap(DELETE_KEYS, handleDelete, root);\n\n useEffect(\n function cancelPendingCreationOnSizeChange() {\n cancel();\n },\n [size, cancel],\n );\n\n const getDateRangeForVisualGrid = useMemo(() => {\n return createMapCellInfoToRecurringTimeRange({\n originDate,\n fromX: toDay,\n fromY: y => y * visualGridVerticalPrecision,\n });\n }, [visualGridVerticalPrecision, originDate]);\n\n useEffect(\n function scrollToActiveTimeBlock() {\n if (!document.activeElement) {\n return;\n }\n\n if (!root.current || !root.current.contains(document.activeElement)) {\n return;\n }\n\n scrollIntoView(document.activeElement, {\n scrollMode: 'if-needed',\n block: 'nearest',\n inline: 'nearest',\n });\n },\n [schedule],\n );\n\n const [wasInitialScrollPerformed, setWasInitialScrollPerformed] = useState(\n false,\n );\n\n useEffect(\n function performInitialScroll() {\n if (wasInitialScrollPerformed || !root.current || !grid) {\n return;\n }\n\n const range = dateRangeToCells(\n getEarliestTimeRange(schedule) || [\n addHours(originDate, defaultHours[0]),\n addHours(originDate, defaultHours[1]),\n ],\n );\n const rect = grid.getRectFromCell(range[0]);\n const { top, bottom } = rect;\n\n if (top === 0 && bottom === 0) {\n return;\n }\n\n // IE, Edge do not support it\n if (!('scrollBy' in root.current)) {\n return;\n }\n\n root.current.scrollBy(0, top);\n\n setWasInitialScrollPerformed(true);\n },\n [\n wasInitialScrollPerformed,\n grid,\n schedule,\n defaultHours,\n originDate,\n dateRangeToCells,\n ],\n );\n\n const handleBlur: React.FocusEventHandler = useCallback(\n event => {\n if (!event.target.contains(document.activeElement)) {\n setActive([null, null]);\n }\n },\n [setActive],\n );\n\n const handleCellClick = useCallback(\n (dayIndex: number, timeIndex: number) => (event: React.MouseEvent) => {\n if (!grid || disabled) {\n return;\n }\n\n const spanY = toY(cellClickPrecision);\n const cell = {\n startX: dayIndex,\n startY: timeIndex,\n endX: dayIndex,\n endY: spanY + timeIndex,\n spanY,\n spanX: getSpan(dayIndex, dayIndex),\n };\n\n const dateRanges = cellInfoToDateRanges(cell);\n\n setPendingCreation(dateRanges);\n\n event.stopPropagation();\n event.preventDefault();\n },\n [grid, disabled, toY, cellClickPrecision, cellInfoToDateRanges],\n );\n\n return (\n <div\n ref={root}\n style={style}\n onBlur={handleBlur}\n touch-action={isDragging ? 'none' : undefined}\n className={classcat([\n classes.root,\n classes.theme,\n className,\n { [classes['no-scroll']]: isDragging },\n ])}\n >\n <div className={classes['grid-root']}>\n <div\n aria-hidden\n className={classcat([classes.timeline, classes['sticky-left']])}\n >\n <div className={classes.header}>\n <div className={classes['day-column']}>\n <div className={classcat([classes.cell, classes.title])}>T</div>\n </div>\n </div>\n <div className={classes.calendar}>\n <div className={classes['day-column']}>\n <div className={classes['day-hours']}>\n {times(numVisualVerticalCells).map(timeIndex => {\n return (\n <Cell\n classes={classes}\n getDateRangeForVisualGrid={getDateRangeForVisualGrid}\n key={timeIndex}\n timeIndex={timeIndex}\n >\n {({ start, isHourStart }) => {\n if (isHourStart) {\n return (\n <div className={classes.time}>\n {format(start, 'h a', { locale })}\n </div>\n );\n }\n\n return null;\n }}\n </Cell>\n );\n })}\n </div>\n </div>\n </div>\n </div>\n <div\n className={classcat([\n classes['sticky-top'],\n classes['day-header-row'],\n ])}\n >\n <div\n role=\"presentation\"\n className={classcat([classes.calendar, classes.header])}\n >\n {times(7).map(i => (\n <div\n key={i}\n role=\"presentation\"\n className={classes['day-column']}\n >\n <div className={classcat([classes.cell, classes.title])}>\n {format(addDays(originDate, i), 'ddd', { locale })}\n </div>\n </div>\n ))}\n </div>\n </div>\n <div className={classes['layer-container']}>\n {isDragging && (\n <div className={classes['drag-box']} style={dragBoxStyle}>\n {hasFinishedDragging && <div className={classes.popup} />}\n </div>\n )}\n {grid && pendingCreation && isDragging && (\n <Schedule\n classes={classes}\n dateRangeToCells={dateRangeToCells}\n cellInfoToDateRange={cellInfoToSingleDateRange}\n className={classes['is-pending-creation']}\n ranges={mergeEvents(schedule, pendingCreation)}\n grid={grid}\n moveAxis=\"none\"\n eventContentComponent={eventContentComponent}\n getIsActive={getIsActive}\n />\n )}\n {grid && !pendingCreation && (\n <Schedule\n classes={classes}\n onActiveChange={setActive}\n dateRangeToCells={dateRangeToCells}\n cellInfoToDateRange={cellInfoToSingleDateRange}\n isResizable\n moveAxis=\"y\"\n isDeletable\n onChange={handleEventChange}\n onClick={onEventClick}\n ranges={schedule}\n grid={grid}\n eventContentComponent={eventContentComponent}\n eventRootComponent={eventRootComponent}\n getIsActive={getIsActive}\n disabled={disabled}\n />\n )}\n\n <div ref={parent} role=\"grid\" className={classes.calendar}>\n {times(7).map(dayIndex => {\n return (\n <div\n role=\"gridcell\"\n key={dayIndex}\n className={classes['day-column']}\n >\n <div className={classes['day-hours']}>\n {times(numVisualVerticalCells).map(timeIndex => {\n return (\n <Cell\n classes={classes}\n onClick={handleCellClick(\n dayIndex,\n timeIndex *\n (numVerticalCells / numVisualVerticalCells),\n )}\n getDateRangeForVisualGrid={getDateRangeForVisualGrid}\n key={timeIndex}\n timeIndex={timeIndex}\n />\n );\n })}\n </div>\n </div>\n );\n })}\n </div>\n </div>\n </div>\n </div>\n );\n}, isEqual);\n"],"names":["DefaultEventRootComponent","React","forwardRef","ref","isActive","handleDelete","cellIndex","rangeIndex","classes","disabled","props","SchedulerContext","createContext","locale","en","createPageMapCoordsToContainer","container","event","clientX","clientY","pageX","pageY","changedTouches","getBoundingClientRect","top","left","x","y","prevent","tap","e","preventDefault","stopPropagation","useClickAndDrag","isDisabled","useState","transform","width","height","style","setStyle","box","setBox","isDragging","setIsDragging","hasFinishedDragging","setHasFinishedDragging","current","useEffect","mapCoordsToContainer","touchMove$","fromEvent","window","passive","pipe","touchEnd$","touchStart$","touchStartWithDelay$","mergeMap","start","of","delay","takeUntil","mouseDown$","filter","which","mouseMove$","mouseUp$","dragStart$","merge","map","dragEnd$","move$","box$","down","startWith","move","startX","Math","max","startY","endX","min","scrollWidth","endY","scrollHeight","bottom","right","rect","style$","display","boxSubscriber","subscribe","styleSubscriber","unsubscribe","cancel","useCallback","useMousetrap","handlerKey","handlerCallback","elementOrElementRef","actionRef","useRef","element","document","instance","Mousetrap","bind","combo","unbind","getSpan","x1","x2","abs","createGrid","totalHeight","totalWidth","numVerticalCells","numHorizontalCells","cellHeight","cellWidth","getRectFromCell","data","spanX","spanY","getCellFromRect","clamp","floor","round","cellToDate","toMin","originDate","addMinutes","addDays","createMapCellInfoToRecurringTimeRange","fromY","toDay","fromX","result","range","i","startDate","endDate","endOfDay","isBefore","sort","range1","range2","compareAsc","createMapDateRangeToCells","toX","toY","end","originOfThisDay","startOfDay","_startX","differenceInDays","_startY","differenceInMinutes","_endX","_endY","cells","atStart","atEnd","isEqual","pop","getEarliestTimeRange","ranges","startA","startB","setDay","mergeRanges","_mergeRanges","d","c","Date","mergeEvents","event1","event2","Cell","memo","timeIndex","children","getDateRangeForVisualGrid","onClick","isHourStart","getMinutes","classcat","cell","formatTemplate","dropSame","dates","template","takeSecond","date","format","first","second","formatHour","getFormattedComponentsForDateRange","dateRange","template2","includeDayIfSame","length","isSameDay","firstM","secondM","day","startDateStr","endDateStr","getTextForDateRange","options","join","EventContent","isStart","isEnd","useContext","RangeBox","grid","cellArray","className","onChange","cellInfoToDateRange","isResizable","moveAxis","onActiveChange","getIsActive","eventContentComponent","EventContentComponent","eventRootComponent","EventRootComponent","modifiedCell","setModifiedCell","originalRect","useMemo","modifiedDateRange","handleStop","newCell","handleDrag","newRect","startOrEnd1","startOrEnd2","newTop","newBottom","newLeft","newRight","invariant","handleResize","direction","_ref","delta","newSize","includes","undefined","handleOnFocus","handleOnClick","cancelClasses","handle","split","bottomLeft","bottomRight","topLeft","topRight","Schedule","isDeletable","dateRangeToCells","MINS_IN_DAY","horizontalPrecision","days","DELETE_KEYS","TimeGridScheduler","verticalPrecision","visualGridVerticalPrecision","cellClickPrecision","schedule","_originDate","defaultHours","onEventClick","mins","cellInfoToDateRanges","cellInfoToSing