skedify-calendar
Version:
Skedify Calendar React component
566 lines (464 loc) • 22.4 kB
JavaScript
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;