UNPKG

skedify-calendar

Version:
566 lines (464 loc) 22.4 kB
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; var _DAY_MAP, _class, _temp; function _objectWithoutProperties(obj, keys) { var target = {}; for (var i in obj) { if (keys.indexOf(i) >= 0) continue; if (!Object.prototype.hasOwnProperty.call(obj, i)) continue; target[i] = obj[i]; } return target; } function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; } function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; } import React, { Component } from 'react'; import { v4 as uuid } from 'uuid'; import { startOfWeek, addDays, startOfDay, differenceInHours, format, isSameDay, differenceInMinutes, setHours, setMinutes, addMinutes, getHours, getMinutes, differenceInSeconds, addSeconds, subSeconds } from 'date-fns'; import { ComponentMeasurer } from './ComponentMeasurer'; import { DragHandle } from './DragHandle'; import { Stack } from './Stack'; import { isFunction } from '../../utils/isFunction'; import { range } from '../../utils/range'; import { constrain } from '../../utils/constrain'; import { timeStringToSeconds, timeStringToDate } from '../../utils/time'; import { Slot } from './Slot'; import { DEFAULT_RENDER_HEADER, DEFAULT_RENDER_DAY_HEADER, DEFAULT_RENDER_GUTTER_ITEM, DEFAULT_RENDER_EVENT, DEFAULT_RENDER_PLACEHOLDER } from './defaultRenderers'; import { Gutter } from './Gutter'; import { Event } from './Event'; import { LocaleContext, loadLocale } from './LocaleContext'; export var SUNDAY = uuid(); export var MONDAY = uuid(); export var TUESDAY = uuid(); export var WEDNESDAY = uuid(); export var THURSDAY = uuid(); export var FRIDAY = uuid(); export var SATURDAY = uuid(); var PLACEHOLDER_EVENT = uuid(); var DAY_MAP = (_DAY_MAP = {}, _DAY_MAP[SUNDAY] = 0, _DAY_MAP[MONDAY] = 1, _DAY_MAP[TUESDAY] = 2, _DAY_MAP[WEDNESDAY] = 3, _DAY_MAP[THURSDAY] = 4, _DAY_MAP[FRIDAY] = 5, _DAY_MAP[SATURDAY] = 6, _DAY_MAP); var DEFAULT_SLOT_HEIGHT = 20; var ONE_WEEK = 7; function hasTimeChanged(start1, end1, start2, end2) { return '' + start1 !== '' + start2 || '' + end1 !== '' + end2; } var Calendar = (_temp = _class = function (_Component) { _inherits(Calendar, _Component); function Calendar() { _classCallCheck(this, Calendar); for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { args[_key] = arguments[_key]; } var _this = _possibleConstructorReturn(this, _Component.call.apply(_Component, [this].concat(args))); _this.mousePositionToClosestSlot = function (event) { var _this$state = _this.state, gutterWidth = _this$state.gutterWidth, mainElement = _this$state.mainElement; var _this$props = _this.props, step = _this$props.step, minTime = _this$props.minTime, daysToRender = _this$props.daysToRender; var cursor = { top: event.clientY, left: event.clientX }; var _mainElement$rect = mainElement.rect(), mainElementLeft = _mainElement$rect.left, mainElementTop = _mainElement$rect.top; // If the cursor is in the scrollbar, don't create a pseudo event if (cursor.left - mainElementLeft > mainElement.element.scrollWidth) { return undefined; } var position = { left: Math.max(0, cursor.left - mainElementLeft + mainElement.element.scrollLeft - gutterWidth), top: Math.max(0, cursor.top - mainElementTop + mainElement.element.scrollTop) }; var _this$getVisibleRange = _this.getVisibleRange(), start = _this$getVisibleRange.start; var startTime = timeStringToDate(minTime, start); var sizeOfOneSlot = _this.getSizeOfOneSlot(); return addDays(addMinutes(startTime, Math.floor(position.top / sizeOfOneSlot) * step), Math.floor(position.left / ((mainElement.width - gutterWidth) / daysToRender))); }; _this.createPseudoEvent = function (event, handle) { return function (DOMEvent) { DOMEvent.preventDefault(); DOMEvent.stopPropagation(); var step = _this.props.step; var closestSlot = _this.mousePositionToClosestSlot(DOMEvent); var isPlaceholderEvent = event === PLACEHOLDER_EVENT; _this.setState({ handle: handle, pseudoEvent: _extends({}, isPlaceholderEvent ? { start: closestSlot, end: addMinutes(closestSlot, step) } : event, { id: uuid(), isPlaceholderEvent: isPlaceholderEvent, mouseStartPositionDiffInSeconds: differenceInSeconds(closestSlot, event.start), isDirty: false, original: isPlaceholderEvent ? { start: closestSlot, end: closestSlot } : event }) }); }; }; _this.startInteraction = function (event) { var _handlers; var pseudoEvent = _this.state.pseudoEvent; // There is no pseudo event yet // So interaction are a bit useless if (!pseudoEvent) { return; } // There is no pressure meaning // That the mouse is not pressed down if (event.pressure <= 0) { return; } var closestSlot = _this.mousePositionToClosestSlot(event); var _ref = [getHours(closestSlot), getMinutes(closestSlot)], hours = _ref[0], minutes = _ref[1]; var handlers = (_handlers = {}, _handlers[DragHandle.PLACEMENT_BOTTOM] = function (state) { var nextEnd = setHours(setMinutes(state.pseudoEvent.end, minutes), hours); var shouldSwapStartAndEnd = differenceInSeconds(nextEnd, state.pseudoEvent.original.start) >= 0; return { start: shouldSwapStartAndEnd ? state.pseudoEvent.original.start : nextEnd, end: shouldSwapStartAndEnd ? nextEnd : state.pseudoEvent.original.start }; }, _handlers[DragHandle.PLACEMENT_TOP] = function (state) { var nextStart = setHours(setMinutes(state.pseudoEvent.start, minutes), hours); var shouldSwapStartAndEnd = differenceInSeconds(nextStart, state.pseudoEvent.original.end) >= 0; return { start: shouldSwapStartAndEnd ? state.pseudoEvent.original.end : nextStart, end: shouldSwapStartAndEnd ? nextStart : state.pseudoEvent.original.end }; }, _handlers[DragHandle.MOVING_INDICATOR] = function (state) { var nextStart = subSeconds(closestSlot, state.pseudoEvent.mouseStartPositionDiffInSeconds); return { start: nextStart, end: addSeconds(nextStart, differenceInSeconds(state.pseudoEvent.end, state.pseudoEvent.start)) }; }, _handlers); _this.setState(function (state) { return { pseudoEvent: _extends({}, state.pseudoEvent, { isDirty: true }, handlers[state.handle] ? handlers[state.handle](state) : {}) }; }); }; _this.finishInteraction = function () { var pseudoEvent = _this.state.pseudoEvent; var _this$props2 = _this.props, onEventUpdate = _this$props2.onEventUpdate, onEventCreate = _this$props2.onEventCreate, onEventClick = _this$props2.onEventClick, canCreateEvent = _this$props2.canCreateEvent, canUpdateEvent = _this$props2.canUpdateEvent; if (!pseudoEvent) { return; } var event = pseudoEvent.original, start = pseudoEvent.start, end = pseudoEvent.end, isPlaceholderEvent = pseudoEvent.isPlaceholderEvent, isDirty = pseudoEvent.isDirty; if (isPlaceholderEvent) { // In this case, we don't have an original event // Thus, we are working with a new event var newEvent = { start: start, end: end }; if (canCreateEvent(newEvent)) { onEventCreate({ start: start, end: end }); } } else if (isFunction(onEventClick) && !isDirty) { // It is the same start & end, so we probably wanted a click... onEventClick(event); } else if (hasTimeChanged(event.start, event.end, start, end) && canUpdateEvent({ event: event, start: start, end: end })) { // We do have an existing event // Let's update it onEventUpdate({ event: event, start: start, end: end }); } // Let's clean our pseudo event _this.setState({ handle: null, pseudoEvent: null }); }; _this.cancelInteraction = function () { // Let's clean our pseudo event _this.setState({ handle: null, pseudoEvent: null }); }; _this.getAmountOfSlotsToRender = function () { var _this$props3 = _this.props, minTime = _this$props3.minTime, maxTime = _this$props3.maxTime, step = _this$props3.step; var _this$getVisibleRange2 = _this.getVisibleRange(), start = _this$getVisibleRange2.start; var startTime = timeStringToDate(minTime, start); var endTime = timeStringToDate(maxTime, start); return Math.round((Math.abs(differenceInHours(endTime, startTime)) + step / 60) * (60 / step)); }; _this.getSizeOfOneSlot = function () { var mainElement = _this.state.mainElement; if (!mainElement) { return DEFAULT_SLOT_HEIGHT; } return Math.max(Math.max(mainElement.element.scrollHeight - mainElement.element.offsetTop, mainElement.height) / _this.getAmountOfSlotsToRender(), DEFAULT_SLOT_HEIGHT); // 1 slot is equal to the step. Steps are in minutes }; _this.getVisibleRange = function () { var _this$props4 = _this.props, date = _this$props4.date, daysToRender = _this$props4.daysToRender, weekStartsOn = _this$props4.weekStartsOn; var start = daysToRender === ONE_WEEK ? startOfWeek(date, { weekStartsOn: DAY_MAP[weekStartsOn] }) : startOfDay(date); var end = addDays(start, daysToRender - 1); return { start: start, end: end }; }; _this.state = { handle: null, pseudoEvent: null, mainElement: null, gutterWidth: 0, scrollBarWidth: 0 }; return _this; } Calendar.prototype.render = function render() { var _this2 = this; var _props = this.props, step = _props.step, events = _props.events, onEventUpdate = _props.onEventUpdate, onEventCreate = _props.onEventCreate, renderHeader = _props.renderHeader, renderDayHeader = _props.renderDayHeader, renderGutterItem = _props.renderGutterItem, renderEvent = _props.renderEvent, renderPlaceholder = _props.renderPlaceholder, daysToRender = _props.daysToRender, minTime = _props.minTime, locale = _props.locale, canCreateEvent = _props.canCreateEvent, canUpdateEvent = _props.canUpdateEvent; var _state = this.state, pseudoEvent = _state.pseudoEvent, gutterWidth = _state.gutterWidth, scrollBarWidth = _state.scrollBarWidth; var _getVisibleRange = this.getVisibleRange(), start = _getVisibleRange.start, end = _getVisibleRange.end; var isUpdatingExistingEventsAllowed = isFunction(onEventUpdate); var isCreatingNewEventsAllowed = isFunction(onEventCreate); var allEvents = pseudoEvent ? [].concat(events, [pseudoEvent]) : events; var dayHeaderContents = range(daysToRender).map(function (index) { return renderDayHeader(addDays(start, index)); }); var startTime = timeStringToDate(minTime, start); var dayHeaders = dayHeaderContents.every(function (value) { return value === null; }) ? null : React.createElement( Stack, { align: Stack.align.NORMAL, className: 'SkedifyCalendar__WeekHeader' }, React.createElement('div', { key: 'gutter_spot', className: 'SkedifyCalendar__WeekHeader__Gutter', style: { width: gutterWidth } }), React.createElement( Stack, { distribution: Stack.distribution.BETWEEN, className: 'SkedifyCalendar__WeekHeader__Days' }, dayHeaderContents.map(function (contents, index) { return React.createElement( React.Fragment, { key: index }, contents ); }) ), React.createElement('div', { key: 'scrollbar_spot', className: 'SkedifyCalendar__WeekHeader__Scrollbar', style: { width: scrollBarWidth } }) ); return React.createElement( LocaleContext.Provider, { value: loadLocale(locale) }, React.createElement( 'div', { className: 'SkedifyCalendar' }, renderHeader({ start: start, end: end }), dayHeaders, React.createElement(ComponentMeasurer, { onUpdate: function onUpdate(_ref2) { var ref = _ref2.ref, info = _objectWithoutProperties(_ref2, ['ref']); _this2.setState({ mainElement: info, scrollBarWidth: info.element.offsetWidth - info.element.clientWidth }, function () { var _props2 = _this2.props, scrollToTime = _props2.scrollToTime, maxTime = _props2.maxTime; if (!scrollToTime) { return; } var constrainedScrollToTime = constrain(timeStringToSeconds(format(scrollToTime, 'HH:mm:ss')), timeStringToSeconds(minTime), timeStringToSeconds(maxTime)); var startTimeInSeconds = timeStringToSeconds(format(startTime, 'HH:mm:ss')); var sizeOfOneSlot = _this2.getSizeOfOneSlot(); ref.current.scrollTop = sizeOfOneSlot * ((constrainedScrollToTime - startTimeInSeconds) / 60 / step); }); }, render: function render(_ref3) { var ref = _ref3.ref, element = _ref3.element, _ref3$height = _ref3.height, height = _ref3$height === undefined ? 0 : _ref3$height; var sizeOfOneSlot = _this2.getSizeOfOneSlot(); var slotsToRender = _this2.getAmountOfSlotsToRender(); var _ref4 = element || {}, _ref4$scrollHeight = _ref4.scrollHeight, scrollHeight = _ref4$scrollHeight === undefined ? 0 : _ref4$scrollHeight, _ref4$offsetTop = _ref4.offsetTop, offsetTop = _ref4$offsetTop === undefined ? 0 : _ref4$offsetTop; var MINIMUM_POSITION = 0; var MAXIMUM_POSITION = Math.max(scrollHeight - offsetTop, height); return React.createElement( React.Fragment, null, React.createElement( Stack, { ref: ref, distribution: Stack.distribution.AROUND, className: 'SkedifyCalendar__Main', style: { height: 'calc(100% - ' + offsetTop + 'px)' }, onPointerDown: function onPointerDown(DOMEvent) { if (isCreatingNewEventsAllowed) { _this2.createPseudoEvent(PLACEHOLDER_EVENT, DragHandle.PLACEMENT_BOTTOM)(DOMEvent); } }, onPointerMove: _this2.startInteraction, onPointerUp: _this2.finishInteraction, onPointerLeave: _this2.cancelInteraction }, React.createElement(ComponentMeasurer, { onUpdate: function onUpdate(_ref5) { var width = _ref5.width; _this2.setState({ gutterWidth: width }); }, render: function render(_ref6) { var gutterRef = _ref6.ref; return React.createElement(Gutter, { ref: gutterRef, slotsToRender: slotsToRender, sizeOfOneSlot: sizeOfOneSlot, startTime: format(startTime, 'yyyy-MM-dd\'T\'HH:mm:ss.SSSxxx'), step: step, render: renderGutterItem }); } }), range(daysToRender).map(function (index) { var dateToRender = addDays(start, index); var startOfThisDay = timeStringToDate(minTime, dateToRender); var eventsOnThisDay = allEvents.filter(function (event) { return isSameDay(event.start, dateToRender); }); return React.createElement( Stack, { key: index, direction: Stack.direction.VERTICAL, distribution: Stack.distribution.NORMAL, className: 'SkedifyCalendar__Column' }, range(slotsToRender).map(function (slotIndex) { return React.createElement(Slot, { key: slotIndex, height: sizeOfOneSlot }); }), eventsOnThisDay.map(function (event, eventIndex) { var normalPosition = sizeOfOneSlot * (differenceInMinutes(event.start, startOfThisDay) / step); var positionTop = constrain(normalPosition, MINIMUM_POSITION, MAXIMUM_POSITION); var eventHeight = constrain(Math.round(sizeOfOneSlot * differenceInMinutes(event.end, event.start) / step), sizeOfOneSlot, MAXIMUM_POSITION - positionTop); return React.createElement(Event, { key: event.id || eventIndex // assuming an id exist on the event, if not, use the index , event: event, interactionInfo: event === pseudoEvent && event.isPlaceholderEvent ? { get isCreatingAllowed() { return event === pseudoEvent && event.isPlaceholderEvent ? canCreateEvent({ start: event.start, end: event.end }) : false; } } : { // Can we interact with the current event get isInteractable() { return isUpdatingExistingEventsAllowed; }, // Are we interacting with the current event get isInteracting() { return pseudoEvent ? pseudoEvent.original === event : false; }, // Is there another event that is currently being interacted with get isOtherInteracting() { return pseudoEvent !== null && pseudoEvent.original !== event; }, // Is current event the pseudo event get isPseudo() { return pseudoEvent === event; }, get isUpdatingAllowed() { return event === pseudoEvent ? canUpdateEvent({ event: event.original, start: event.start, end: event.end }) : true; } }, onActivate: isUpdatingExistingEventsAllowed ? _this2.createPseudoEvent : undefined, positionTop: positionTop, height: normalPosition < 0 ? eventHeight + normalPosition // We have to subtract the negative part from the height. : eventHeight, render: event === pseudoEvent && event.isPlaceholderEvent ? renderPlaceholder : renderEvent }); }) ); }) ) ); } }) ) ); }; return Calendar; }(Component), _class.MONDAY = MONDAY, _class.TUESDAY = TUESDAY, _class.WEDNESDAY = WEDNESDAY, _class.THURSDAY = THURSDAY, _class.FRIDAY = FRIDAY, _class.SATURDAY = SATURDAY, _class.SUNDAY = SUNDAY, _class.DEFAULT_RENDER_HEADER = DEFAULT_RENDER_HEADER, _class.DEFAULT_RENDER_DAY_HEADER = DEFAULT_RENDER_DAY_HEADER, _class.DEFAULT_RENDER_GUTTER_ITEM = DEFAULT_RENDER_GUTTER_ITEM, _class.DEFAULT_RENDER_EVENT = DEFAULT_RENDER_EVENT, _class.DEFAULT_RENDER_PLACEHOLDER = DEFAULT_RENDER_PLACEHOLDER, _class.defaultProps = { date: new Date(), // The date to be used to render the calendar events: [], // A list of events to render locale: 'en', weekStartsOn: SUNDAY, daysToRender: 7, step: 30, // Time in minutes for 1 slot minTime: '00:00:00', maxTime: '24:00:00', renderHeader: DEFAULT_RENDER_HEADER, renderDayHeader: DEFAULT_RENDER_DAY_HEADER, renderGutterItem: DEFAULT_RENDER_GUTTER_ITEM, renderEvent: DEFAULT_RENDER_EVENT, renderPlaceholder: DEFAULT_RENDER_PLACEHOLDER, canUpdateEvent: function canUpdateEvent() { return true; }, canCreateEvent: function canCreateEvent() { return true; }, onEventClick: function onEventClick() { return undefined; } }, _temp); export default Calendar;