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 ( + + ) } }