diff --git a/package.json b/package.json index b157e9836..94f99b974 100644 --- a/package.json +++ b/package.json @@ -130,7 +130,7 @@ "dependencies": { "@babel/runtime": "^7.1.5", "clsx": "^1.0.4", - "date-arithmetic": "^4.0.1", + "date-arithmetic": "^4.1.0", "dom-helpers": "^5.1.0", "invariant": "^2.2.4", "lodash": "^4.17.11", diff --git a/src/addons/dragAndDrop/EventContainerWrapper.js b/src/addons/dragAndDrop/EventContainerWrapper.js index a700b0ec4..a0227146d 100644 --- a/src/addons/dragAndDrop/EventContainerWrapper.js +++ b/src/addons/dragAndDrop/EventContainerWrapper.js @@ -1,7 +1,6 @@ import PropTypes from 'prop-types' import React from 'react' import * as dates from '../../utils/dates' -import { findDOMNode } from 'react-dom' import { DnDContext } from './DnDContext' import Selection, { @@ -9,15 +8,9 @@ import Selection, { getEventNodeFromPoint, } from '../../Selection' import TimeGridEvent from '../../TimeGridEvent' -import { dragAccessors } from './common' +import { dragAccessors, eventTimes, pointInColumn } from './common' import NoopWrapper from '../../NoopWrapper' -const pointInColumn = (bounds, { x, y }) => { - const { left, right, top } = bounds - return x < right + 10 && x > left && y > top -} -const propTypes = {} - class EventContainerWrapper extends React.Component { static propTypes = { accessors: PropTypes.object.isRequired, @@ -33,6 +26,7 @@ class EventContainerWrapper extends React.Component { constructor(...args) { super(...args) this.state = {} + this.ref = React.createRef() } componentDidMount() { @@ -65,43 +59,31 @@ class EventContainerWrapper extends React.Component { }) } - handleMove = (point, boundaryBox) => { + handleMove = (point, bounds) => { + if (!pointInColumn(bounds, point)) return this.reset() const { event } = this.context.draggable.dragAndDropAction const { accessors, slotMetrics } = this.props - if (!pointInColumn(boundaryBox, point)) { - this.reset() - return - } - - let currentSlot = slotMetrics.closestSlotFromPoint( + const newSlot = slotMetrics.closestSlotFromPoint( { y: point.y - this.eventOffsetTop, x: point.x }, - boundaryBox + bounds ) - let eventStart = accessors.start(event) - let eventEnd = accessors.end(event) - let end = dates.add( - currentSlot, - dates.diff(eventStart, eventEnd, 'minutes'), - 'minutes' - ) - - this.update(event, slotMetrics.getRange(currentSlot, end, false, true)) + const { duration } = eventTimes(event, accessors) + let newEnd = dates.add(newSlot, duration, 'milliseconds') + this.update(event, slotMetrics.getRange(newSlot, newEnd, false, true)) } - handleResize(point, boundaryBox) { - let start, end + handleResize(point, bounds) { const { accessors, slotMetrics } = this.props const { event, direction } = this.context.draggable.dragAndDropAction + const newTime = slotMetrics.closestSlotFromPoint(point, bounds) - let currentSlot = slotMetrics.closestSlotFromPoint(point, boundaryBox) + let { start, end } = eventTimes(event, accessors) if (direction === 'UP') { - end = accessors.end(event) - start = dates.min(currentSlot, slotMetrics.closestSlotFromDate(end, -1)) + start = dates.min(newTime, slotMetrics.closestSlotFromDate(end, -1)) } else if (direction === 'DOWN') { - start = accessors.start(event) - end = dates.max(currentSlot, slotMetrics.closestSlotFromDate(start)) + end = dates.max(newTime, slotMetrics.closestSlotFromDate(start)) } this.update(event, slotMetrics.getRange(start, end)) @@ -124,10 +106,11 @@ class EventContainerWrapper extends React.Component { } _selectable = () => { - let node = findDOMNode(this) + let wrapper = this.ref.current + let node = wrapper.children[0] let isBeingDragged = false let selector = (this._selector = new Selection(() => - node.closest('.rbc-time-view') + wrapper.closest('.rbc-time-view') )) selector.on('beforeSelect', point => { @@ -141,6 +124,12 @@ class EventContainerWrapper extends React.Component { const eventNode = getEventNodeFromPoint(node, point) if (!eventNode) return false + // eventOffsetTop is distance from the top of the event to the initial + // mouseDown position. We need this later to compute the new top of the + // event during move operations, since the final location is really a + // delta from this point. note: if we want to DRY this with WeekWrapper, + // probably better just to capture the mouseDown point here and do the + // placement computation in handleMove()... this.eventOffsetTop = point.y - getBoundsForNode(eventNode).top }) @@ -154,19 +143,14 @@ class EventContainerWrapper extends React.Component { selector.on('dropFromOutside', point => { if (!this.context.draggable.onDropFromOutside) return - const bounds = getBoundsForNode(node) - if (!pointInColumn(bounds, point)) return - this.handleDropFromOutside(point, bounds) }) selector.on('dragOver', point => { if (!this.context.draggable.dragFromOutsideItem) return - const bounds = getBoundsForNode(node) - this.handleDropFromOutside(point, bounds) }) @@ -212,7 +196,7 @@ class EventContainerWrapper extends React.Component { this._selector = null } - render() { + renderContent() { const { children, accessors, @@ -223,7 +207,6 @@ class EventContainerWrapper extends React.Component { } = this.props let { event, top, height } = this.state - if (!event) return children const events = children.props.children @@ -263,8 +246,10 @@ class EventContainerWrapper extends React.Component { ), }) } -} -EventContainerWrapper.propTypes = propTypes + render() { + return
{this.renderContent()}
+ } +} export default EventContainerWrapper diff --git a/src/addons/dragAndDrop/WeekWrapper.js b/src/addons/dragAndDrop/WeekWrapper.js index 9d25616c7..0a6041895 100644 --- a/src/addons/dragAndDrop/WeekWrapper.js +++ b/src/addons/dragAndDrop/WeekWrapper.js @@ -1,28 +1,16 @@ import PropTypes from 'prop-types' import React from 'react' +import EventRow from '../../EventRow' +import Selection, { + getBoundsForNode, + getEventNodeFromPoint, +} from '../../Selection' import * as dates from '../../utils/dates' -import { getSlotAtX, pointInBox } from '../../utils/selection' -import { findDOMNode } from 'react-dom' - import { eventSegments } from '../../utils/eventLevels' -import Selection, { getBoundsForNode } from '../../Selection' -import EventRow from '../../EventRow' -import { dragAccessors } from './common' +import { getSlotAtX, pointInBox } from '../../utils/selection' +import { dragAccessors, eventTimes } from './common' import { DnDContext } from './DnDContext' -const propTypes = {} - -const eventTimes = (event, accessors) => { - let start = accessors.start(event) - let end = accessors.end(event) - - const isZeroDuration = - dates.eq(start, end, 'minutes') && start.getMinutes() === 0 - // make zero duration midnight events at least one day long - if (isZeroDuration) end = dates.add(end, 1, 'day') - return { start, end } -} - class WeekWrapper extends React.Component { static propTypes = { isAllDay: PropTypes.bool, @@ -31,6 +19,7 @@ class WeekWrapper extends React.Component { getters: PropTypes.object.isRequired, components: PropTypes.object.isRequired, resourceId: PropTypes.any, + rtl: PropTypes.bool, } static contextType = DnDContext @@ -38,6 +27,7 @@ class WeekWrapper extends React.Component { constructor(...args) { super(...args) this.state = {} + this.ref = React.createRef() } componentDidMount() { @@ -71,42 +61,34 @@ class WeekWrapper extends React.Component { this.setState({ segment }) } - handleMove = ({ x, y }, node, draggedEvent) => { + handleMove = (point, bounds, draggedEvent) => { + if (!pointInBox(bounds, point)) return this.reset() const event = this.context.draggable.dragAndDropAction.event || draggedEvent - const metrics = this.props.slotMetrics - const { accessors } = this.props - - if (!event) return - - let rowBox = getBoundsForNode(node) - - if (!pointInBox(rowBox, { x, y })) { - this.reset() - return - } + const { accessors, slotMetrics, rtl } = this.props - // Make sure to maintain the time of the start date while moving it to the new slot - let start = dates.merge( - metrics.getDateForSlot(getSlotAtX(rowBox, x, false, metrics.slots)), - accessors.start(event) + const slot = getSlotAtX( + bounds, + point.x - this.eventOffsetLeft, + rtl, + slotMetrics.slots ) - let end = dates.add( - start, - dates.diff(accessors.start(event), accessors.end(event), 'minutes'), - 'minutes' - ) + const date = slotMetrics.getDateForSlot(slot) + // Adjust the dates, but maintain the times when moving + let { start, duration } = eventTimes(event, accessors) + start = dates.merge(date, start) + const end = dates.add(start, duration, 'milliseconds') + // TODO: when dragging a multi-row event, only the first row is animating this.update(event, start, end) } - handleDropFromOutside = (point, rowBox) => { + handleDropFromOutside = (point, bounds) => { if (!this.context.draggable.onDropFromOutside) return - const { slotMetrics: metrics } = this.props + const { slotMetrics, rtl } = this.props - let start = metrics.getDateForSlot( - getSlotAtX(rowBox, point.x, false, metrics.slots) - ) + const slot = getSlotAtX(bounds, point.x, rtl, slotMetrics.slots) + const start = slotMetrics.getDateForSlot(slot) this.context.draggable.onDropFromOutside({ start, @@ -115,103 +97,103 @@ class WeekWrapper extends React.Component { }) } - handleDragOverFromOutside = ({ x, y }, node) => { + handleDragOverFromOutside = (point, node) => { if (!this.context.draggable.dragFromOutsideItem) return - - this.handleMove( - { x, y }, - node, - this.context.draggable.dragFromOutsideItem() - ) + this.handleMove(point, node, this.context.draggable.dragFromOutsideItem()) } - handleResize(point, node) { + // TODO: when resizing RIGHT, the mouse has to make it all the way to the + // very end of the slot before it jumps... + handleResize(point, bounds) { const { event, direction } = this.context.draggable.dragAndDropAction - const { accessors, slotMetrics: metrics } = this.props + const { accessors, slotMetrics, rtl } = this.props - let { start, end } = eventTimes(event, accessors) + let { start, end, allDay } = eventTimes(event, accessors) let originalStart = start let originalEnd = end - let rowBox = getBoundsForNode(node) - let cursorInRow = pointInBox(rowBox, point) + const slot = getSlotAtX(bounds, point.x, rtl, slotMetrics.slots) + const date = slotMetrics.getDateForSlot(slot) + let cursorInRow = pointInBox(bounds, point) if (direction === 'RIGHT') { if (cursorInRow) { - if (metrics.last < start) return this.reset() - end = metrics.getDateForSlot( - getSlotAtX(rowBox, point.x, false, metrics.slots) - ) + if (slotMetrics.last < start) return this.reset() + end = date } else if ( - dates.inRange(start, metrics.first, metrics.last) || - (rowBox.bottom < point.y && +metrics.first > +start) + dates.inRange(start, slotMetrics.first, slotMetrics.last) || + (bounds.bottom < point.y && dates.gt(slotMetrics.first, start)) ) { - end = dates.add(metrics.last, 1, 'milliseconds') + end = dates.add(slotMetrics.last, 1, 'milliseconds') } else { this.setState({ segment: null }) return } - end = dates.merge(end, accessors.end(event)) + end = dates.merge(end, originalEnd) if (dates.lt(end, start)) { end = originalEnd } } else if (direction === 'LEFT') { - // inbetween Row if (cursorInRow) { - if (metrics.first > end) return this.reset() - - start = metrics.getDateForSlot( - getSlotAtX(rowBox, point.x, false, metrics.slots) - ) + if (slotMetrics.first > end) return this.reset() + start = date } else if ( - dates.inRange(end, metrics.first, metrics.last) || - (rowBox.top > point.y && +metrics.last < +end) + dates.inRange(end, slotMetrics.first, slotMetrics.last) || + (bounds.top > point.y && dates.lt(slotMetrics.last, end)) ) { - start = dates.add(metrics.first, -1, 'milliseconds') + start = dates.add(slotMetrics.first, -1, 'milliseconds') } else { this.reset() return } - start = dates.merge(start, accessors.start(event)) + start = dates.merge(start, originalStart) if (dates.gt(start, end)) { start = originalStart } } + if (allDay) end = dates.add(end, 1, 'day') this.update(event, start, end) } _selectable = () => { - let node = findDOMNode(this).closest('.rbc-month-row, .rbc-allday-cell') - let container = node.closest('.rbc-month-view, .rbc-time-view') + let wrapper = this.ref.current + let node = wrapper.closest('.rbc-month-row, .rbc-allday-cell') + let container = wrapper.closest('.rbc-month-view, .rbc-time-view') let selector = (this._selector = new Selection(() => container)) selector.on('beforeSelect', point => { const { isAllDay } = this.props const { action } = this.context.draggable.dragAndDropAction + const eventNode = getEventNodeFromPoint(node, point) + + // eventOffsetLeft is distance from the left of the event to the initial + // mouseDown position. We need this later to compute the new top of the + // event during move operations, since the final location is really a + // delta from this point. note: if we want to DRY this with + // EventContainerWrapper, probably better just to capture the mouseDown + // point here and do the placement computation in handleMove()... + this.eventOffsetLeft = point.x - getBoundsForNode(eventNode).left return ( action === 'move' || - (action === 'resize' && - (!isAllDay || pointInBox(getBoundsForNode(node), point))) + (action === 'resize' && (!isAllDay || pointInBox(eventNode, point))) ) }) selector.on('selecting', box => { const bounds = getBoundsForNode(node) const { dragAndDropAction } = this.context.draggable - if (dragAndDropAction.action === 'move') this.handleMove(box, bounds) if (dragAndDropAction.action === 'resize') this.handleResize(box, bounds) }) selector.on('selectStart', () => this.context.draggable.onStart()) + selector.on('select', point => { const bounds = getBoundsForNode(node) - if (!this.state.segment) return - if (!pointInBox(bounds, point)) { this.reset() } else { @@ -221,17 +203,13 @@ class WeekWrapper extends React.Component { selector.on('dropFromOutside', point => { if (!this.context.draggable.onDropFromOutside) return - const bounds = getBoundsForNode(node) - if (!pointInBox(bounds, point)) return - this.handleDropFromOutside(point, bounds) }) selector.on('dragOverFromOutside', point => { if (!this.context.draggable.dragFromOutsideItem) return - const bounds = getBoundsForNode(node) this.handleDragOverFromOutside(point, bounds) @@ -271,7 +249,7 @@ class WeekWrapper extends React.Component { let { segment } = this.state return ( -
+
{children} {segment && ( @@ -291,6 +269,4 @@ class WeekWrapper extends React.Component { } } -WeekWrapper.propTypes = propTypes - export default WeekWrapper diff --git a/src/addons/dragAndDrop/common.js b/src/addons/dragAndDrop/common.js index 29f51264f..8e1e53320 100644 --- a/src/addons/dragAndDrop/common.js +++ b/src/addons/dragAndDrop/common.js @@ -1,12 +1,13 @@ import { wrapAccessor } from '../../utils/accessors' import { createFactory } from 'react' +import * as dates from '../../utils/dates' export const dragAccessors = { start: wrapAccessor(e => e.start), end: wrapAccessor(e => e.end), } -export const nest = (...Components) => { +function nest(...Components) { const factories = Components.filter(Boolean).map(createFactory) const Nest = ({ children, ...props }) => factories.reduceRight((child, factory) => factory(props, child), children) @@ -14,12 +15,39 @@ export const nest = (...Components) => { return Nest } -export const mergeComponents = (components = {}, addons) => { +export function mergeComponents(components = {}, addons) { const keys = Object.keys(addons) const result = { ...components } keys.forEach(key => { - result[key] = components[key] ? nest(components[key], addons[key]) : addons[key] + result[key] = components[key] + ? nest(components[key], addons[key]) + : addons[key] }) return result } + +export function pointInColumn(bounds, point) { + const { left, right, top } = bounds + const { x, y } = point + return x < right + 10 && x > left && y > top +} + +/** + * Get start, end, allDay and duration of an event using the provided accessors. + * Fixes up problematic case of malformed allDay events (those missing + * allDay=true or allDay events where the end date isn't exclusive) + */ +export function eventTimes(event, accessors) { + const start = accessors.start(event) + let end = accessors.end(event) + let allDay = accessors.allDay(event) + const duration = dates.diff(start, end, 'milliseconds') + + // make zero duration midnight events at least one day long + if (duration === 0 && start.getMinutes() === 0) { + end = dates.add(end, 1, 'day') + allDay = true + } + return { start, end, allDay, duration } +}