From b9fdce46ff5d49712090bae8426c400624f10fe4 Mon Sep 17 00:00:00 2001 From: Kamry Bowman Date: Mon, 22 Apr 2019 11:05:02 -0600 Subject: [PATCH] feat(dnd): add onDropFromOutside prop for Dnd Cal (#1290) This PR is meant to resolve issue #1090. ## Basic callback for outside drops The change exposes the `onDropFromOutside` prop on the withDragAndDrop HOC, which takes a callback that fires when an outside draggable item is dropped onto the calendar. The callback receives as a parameter an object with start and end properties that are times based on the drop position and slot size. ![a4be055597d294f257d59a6fa2982f27](https://user-images.githubusercontent.com/37093582/56405067-a6036e00-6227-11e9-9274-b1846b5b0be8.gif) It is worth noting that it is entirely up to the user to handle actual event creation based on the callback. All that this API does is allow `draggable` DOM elements to trigger a callback that receives start and end times for the slot an item was dropped on, and a boolean as to whether it's an all-day event. If the user wants to know which event was dropped, they will have to handle that themselves outside of React-Big-Calendar. An example added to the example App demonstrates how this can be done. ## Optional selective dropping By default, if `onDropFromOutside` prop is passed, all draggable events are droppable on calendar. If the user wishes to discriminate as to whether draggable events are droppable on the calendar, they can pass an additional `onDragOver` callback function. The `onDragOver` callback takes a DragEvent as its sole parameter. If it calls the DragEvent's `preventDefault` method, then the draggable item in question is droppable. If it does not call `preventDefault` during the function call, it will not be droppable. ![e374b60b55809f471b2f275d7f166278](https://user-images.githubusercontent.com/37093582/56405161-3b9efd80-6228-11e9-9b0b-2c925f371eb1.gif) An example was also added to the examples App, this one labelled `Addon: Drag and Drop (from outside calendar). The GIFs show this example in action. I also added the following comments into the withDragAndDrop HOC by way of documentation. ``` * Additionally, this HOC adds the callback props `onDropFromOutside` and `onDragOver`. * By default, the calendar will not respond to outside draggable items being dropped * onto it. However, if `onDropFromOutside` callback is passed, then when draggable * DOM elements are dropped on the calendar, the callback will fire, receiving an * object with start and end times, and an allDay boolean. * * If `onDropFromOutside` is passed, but `onDragOver` is not, any draggable event will be * droppable onto the calendar by default. On the other hand, if an `onDragOver` callback * *is* passed, then it can discriminate as to whether a draggable item is droppable on the * calendar. To designate a draggable item as droppable, call `event.preventDefault` * inside `onDragOver`. If `event.preventDefault` is not called in the `onDragOver` * callback, then the draggable item will not be droppable on the calendar. ``` Hopefully this gives users the flexibility they need, without getting react-big-calendar overly involved with managing outside drag and drop scenarios. Any feedback/discussion/harangues are welcome! --- examples/App.js | 3 + examples/demos/dndOutsideSource.js | 174 ++++++++++++++++++ src/Selection.js | 18 ++ .../dragAndDrop/EventContainerWrapper.js | 26 +++ src/addons/dragAndDrop/WeekWrapper.js | 27 +++ src/addons/dragAndDrop/withDragAndDrop.js | 45 ++++- 6 files changed, 289 insertions(+), 4 deletions(-) create mode 100644 examples/demos/dndOutsideSource.js diff --git a/examples/App.js b/examples/App.js index 5295c2c75..8e33532c6 100644 --- a/examples/App.js +++ b/examples/App.js @@ -25,6 +25,7 @@ import Resource from './demos/resource' import DndResource from './demos/dndresource' import Timeslots from './demos/timeslots' import Dnd from './demos/dnd' +import DndOutsideSource from './demos/dndOutsideSource' import Dropdown from 'react-bootstrap/lib/Dropdown' import MenuItem from 'react-bootstrap/lib/MenuItem' @@ -43,6 +44,7 @@ const EXAMPLES = { customView: 'Custom Calendar Views', resource: 'Resource Scheduling', dnd: 'Addon: Drag and drop', + dndOutsideSource: 'Addon: Drag and drop (from outside calendar)', } const DEFAULT_EXAMPLE = 'basic' @@ -78,6 +80,7 @@ class Example extends React.Component { timeslots: Timeslots, dnd: Dnd, dndresource: DndResource, + dndOutsideSource: DndOutsideSource, }[selected] return ( diff --git a/examples/demos/dndOutsideSource.js b/examples/demos/dndOutsideSource.js new file mode 100644 index 000000000..296aa13ef --- /dev/null +++ b/examples/demos/dndOutsideSource.js @@ -0,0 +1,174 @@ +import React from 'react' +import events from '../events' +import BigCalendar from 'react-big-calendar' +import withDragAndDrop from 'react-big-calendar/lib/addons/dragAndDrop' +import Layout from 'react-tackle-box/Layout' +import Card from '../Card' + +import 'react-big-calendar/lib/addons/dragAndDrop/styles.less' + +const DragAndDropCalendar = withDragAndDrop(BigCalendar) + +const formatName = (name, count) => `${name} ID ${count}` + +class Dnd extends React.Component { + constructor(props) { + super(props) + this.state = { + events: events, + draggedEvent: null, + counters: { + item1: 0, + item2: 0, + }, + } + } + + handleDragStart = name => { + this.setState({ draggedEvent: name }) + } + + customOnDragOver = event => { + // check for undroppable is specific to this example + // and not part of API. This just demonstrates that + // onDragOver can optionally be passed to conditionally + // allow draggable items to be dropped on cal, based on + // whether event.preventDefault is called + if (this.state.draggedEvent !== 'undroppable') { + console.log('preventDefault') + event.preventDefault() + } + } + + onDropFromOutside = ({ start, end, allDay }) => { + const { draggedEvent, counters } = this.state + const event = { + title: formatName(draggedEvent, counters[draggedEvent]), + start, + end, + isAllDay: allDay, + } + const updatedCounters = { + ...counters, + [draggedEvent]: counters[draggedEvent] + 1, + } + this.setState({ draggedEvent: null, counters: updatedCounters }) + this.newEvent(event) + } + + moveEvent({ event, start, end, isAllDay: droppedOnAllDaySlot }) { + const { events } = this.state + + const idx = events.indexOf(event) + let allDay = event.allDay + + if (!event.allDay && droppedOnAllDaySlot) { + allDay = true + } else if (event.allDay && !droppedOnAllDaySlot) { + allDay = false + } + + const updatedEvent = { ...event, start, end, allDay } + + const nextEvents = [...events] + nextEvents.splice(idx, 1, updatedEvent) + + this.setState({ + events: nextEvents, + }) + + // alert(`${event.title} was dropped onto ${updatedEvent.start}`) + } + + resizeEvent = ({ event, start, end }) => { + const { events } = this.state + + const nextEvents = events.map(existingEvent => { + return existingEvent.id == event.id + ? { ...existingEvent, start, end } + : existingEvent + }) + + this.setState({ + events: nextEvents, + }) + + //alert(`${event.title} was resized to ${start}-${end}`) + } + + newEvent(event) { + let idList = this.state.events.map(a => a.id) + let newId = Math.max(...idList) + 1 + let hour = { + id: newId, + title: event.title, + allDay: event.isAllDay, + start: event.start, + end: event.end, + } + this.setState({ + events: this.state.events.concat([hour]), + }) + } + + render() { + return ( +
+ +

Outside Drag Sources

+ {Object.entries(this.state.counters).map(([name, count]) => ( +
this.handleDragStart(name)} + > + {formatName(name, count)} +
+ ))} +
this.handleDragStart('undroppable')} + > + Draggable but not for calendar. +
+
+ +
+ ) + } +} + +export default Dnd diff --git a/src/Selection.js b/src/Selection.js index 4f7569938..f33c1feec 100644 --- a/src/Selection.js +++ b/src/Selection.js @@ -54,6 +54,7 @@ class Selection { this._handleMoveEvent = this._handleMoveEvent.bind(this) this._handleTerminatingEvent = this._handleTerminatingEvent.bind(this) this._keyListener = this._keyListener.bind(this) + this._dropFromOutsideListener = this._dropFromOutsideListener.bind(this) // Fixes an iOS 10 bug where scrolling could not be prevented on the window. // https://github.com/metafizzy/flickity/issues/457#issuecomment-254501356 @@ -64,6 +65,10 @@ class Selection { ) this._onKeyDownListener = addEventListener('keydown', this._keyListener) this._onKeyUpListener = addEventListener('keyup', this._keyListener) + this._onDropFromOutsideListener = addEventListener( + 'drop', + this._dropFromOutsideListener + ) this._addInitialEventListener() } @@ -187,6 +192,19 @@ class Selection { } } + _dropFromOutsideListener(e) { + const { pageX, pageY, clientX, clientY } = getEventCoordinates(e) + + this.emit('dropFromOutside', { + x: pageX, + y: pageY, + clientX: clientX, + clientY: clientY, + }) + + e.preventDefault() + } + _handleInitialEvent(e) { const { clientX, clientY, pageX, pageY } = getEventCoordinates(e) let node = this.container(), diff --git a/src/addons/dragAndDrop/EventContainerWrapper.js b/src/addons/dragAndDrop/EventContainerWrapper.js index d2cfec621..642d36d5e 100644 --- a/src/addons/dragAndDrop/EventContainerWrapper.js +++ b/src/addons/dragAndDrop/EventContainerWrapper.js @@ -31,6 +31,7 @@ class EventContainerWrapper extends React.Component { draggable: PropTypes.shape({ onStart: PropTypes.func, onEnd: PropTypes.func, + onDropFromOutside: PropTypes.func, onBeginAction: PropTypes.func, dragAndDropAction: PropTypes.object, }), @@ -113,6 +114,21 @@ class EventContainerWrapper extends React.Component { this.update(event, slotMetrics.getRange(start, end)) } + handleDropFromOutside = (point, boundaryBox) => { + const { slotMetrics } = this.props + + let start = slotMetrics.closestSlotFromPoint( + { y: point.y, x: point.x }, + boundaryBox + ) + + this.context.draggable.onDropFromOutside({ + start, + end: slotMetrics.nextSlot(start), + allDay: false, + }) + } + _selectable = () => { let node = findDOMNode(this) let selector = (this._selector = new Selection(() => @@ -141,6 +157,16 @@ class EventContainerWrapper extends React.Component { if (dragAndDropAction.action === 'resize') this.handleResize(box, bounds) }) + 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('selectStart', () => this.context.draggable.onStart()) selector.on('select', point => { diff --git a/src/addons/dragAndDrop/WeekWrapper.js b/src/addons/dragAndDrop/WeekWrapper.js index 8870204fa..db19203e7 100644 --- a/src/addons/dragAndDrop/WeekWrapper.js +++ b/src/addons/dragAndDrop/WeekWrapper.js @@ -37,6 +37,7 @@ class WeekWrapper extends React.Component { onStart: PropTypes.func, onEnd: PropTypes.func, dragAndDropAction: PropTypes.object, + onDropFromOutside: PropTypes.func, onBeginAction: PropTypes.func, }), } @@ -106,6 +107,21 @@ class WeekWrapper extends React.Component { this.update(event, start, end) } + handleDropFromOutside = (point, rowBox) => { + if (!this.context.draggable.onDropFromOutside) return + const { slotMetrics: metrics } = this.props + + let start = metrics.getDateForSlot( + getSlotAtX(rowBox, point.x, false, metrics.slots) + ) + + this.context.draggable.onDropFromOutside({ + start, + end: dates.add(start, 1, 'day'), + allDay: false, + }) + } + handleResize(point, node) { const { event, direction } = this.context.draggable.dragAndDropAction const { accessors, slotMetrics: metrics } = this.props @@ -193,6 +209,17 @@ class WeekWrapper extends React.Component { if (!this.state.segment || !pointInBox(bounds, point)) return this.handleInteractionEnd() }) + + 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('click', () => this.context.draggable.onEnd(null)) } diff --git a/src/addons/dragAndDrop/withDragAndDrop.js b/src/addons/dragAndDrop/withDragAndDrop.js index a192f6110..7472ba3cc 100644 --- a/src/addons/dragAndDrop/withDragAndDrop.js +++ b/src/addons/dragAndDrop/withDragAndDrop.js @@ -23,8 +23,8 @@ import { mergeComponents } from './common' * * Set `resizable` to true in your calendar if you want events to be resizable. * - * The HOC adds `onEventDrop`, `onEventResize`, `onDragStart` callback properties if the events are - * moved or resized. They are called with these signatures: + * The HOC adds `onEventDrop`, `onEventResize`, and `onDragStart` callback properties if the events are + * moved or resized. These callbacks are called with these signatures: * * ```js * function onEventDrop({ event, start, end, allDay }) {...} @@ -46,6 +46,23 @@ import { mergeComponents } from './common' * If you care about these corner cases, you can examine the `allDay` param suppled * in the callback to determine how the user dropped or resized the event. * + * Additionally, this HOC adds the callback props `onDropFromOutside` and `onDragOver`. + * By default, the calendar will not respond to outside draggable items being dropped + * onto it. However, if `onDropFromOutside` callback is passed, then when draggable + * DOM elements are dropped on the calendar, the callback will fire, receiving an + * object with start and end times, and an allDay boolean. + * + * If `onDropFromOutside` is passed, but `onDragOver` is not, any draggable event will be + * droppable onto the calendar by default. On the other hand, if an `onDragOver` callback + * *is* passed, then it can discriminate as to whether a draggable item is droppable on the + * calendar. To designate a draggable item as droppable, call `event.preventDefault` + * inside `onDragOver`. If `event.preventDefault` is not called in the `onDragOver` + * callback, then the draggable item will not be droppable on the calendar. + * + * * ```js + * function onDropFromOutside({ start, end, allDay }) {...} + * function onDragOver(DragEvent: event) {...} + * ``` * @param {*} Calendar * @param {*} backend */ @@ -55,6 +72,7 @@ export default function withDragAndDrop(Calendar) { onEventDrop: PropTypes.func, onEventResize: PropTypes.func, onDragStart: PropTypes.func, + onDragOver: PropTypes.func, draggableAccessor: accessor, resizableAccessor: accessor, @@ -82,6 +100,7 @@ export default function withDragAndDrop(Calendar) { onStart: PropTypes.func, onEnd: PropTypes.func, onBeginAction: PropTypes.func, + onDropFromOutside: PropTypes.fun, draggableAccessor: accessor, resizableAccessor: accessor, dragAndDropAction: PropTypes.object, @@ -108,6 +127,7 @@ export default function withDragAndDrop(Calendar) { onStart: this.handleInteractionStart, onEnd: this.handleInteractionEnd, onBeginAction: this.handleBeginAction, + onDropFromOutside: this.props.onDropFromOutside, draggableAccessor: this.props.draggableAccessor, resizableAccessor: this.props.resizableAccessor, dragAndDropAction: this.state, @@ -115,6 +135,10 @@ export default function withDragAndDrop(Calendar) { } } + defaultOnDragOver = event => { + event.preventDefault() + } + handleBeginAction = (event, action, direction) => { const { onDragStart } = this.props this.setState({ event, action, direction }) @@ -147,20 +171,33 @@ export default function withDragAndDrop(Calendar) { } render() { - const { selectable, ...props } = this.props + const { selectable, elementProps, ...props } = this.props const { interacting } = this.state delete props.onEventDrop delete props.onEventResize props.selectable = selectable ? 'ignoreEvents' : false + const elementPropsWithDropFromOutside = this.props.onDropFromOutside + ? { + ...elementProps, + onDragOver: this.props.onDragOver || this.defaultOnDragOver, + } + : elementProps + props.className = cn( props.className, 'rbc-addons-dnd', !!interacting && 'rbc-addons-dnd-is-dragging' ) - return + return ( + + ) } }