Skip to content

Commit b9fdce4

Browse files
kamry-bowmanjquense
authored andcommitted
feat(dnd): add onDropFromOutside prop for Dnd Cal (jquense#1290)
This PR is meant to resolve issue jquense#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!
1 parent 0fa2c30 commit b9fdce4

File tree

6 files changed

+289
-4
lines changed

6 files changed

+289
-4
lines changed

examples/App.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import Resource from './demos/resource'
2525
import DndResource from './demos/dndresource'
2626
import Timeslots from './demos/timeslots'
2727
import Dnd from './demos/dnd'
28+
import DndOutsideSource from './demos/dndOutsideSource'
2829
import Dropdown from 'react-bootstrap/lib/Dropdown'
2930
import MenuItem from 'react-bootstrap/lib/MenuItem'
3031

@@ -43,6 +44,7 @@ const EXAMPLES = {
4344
customView: 'Custom Calendar Views',
4445
resource: 'Resource Scheduling',
4546
dnd: 'Addon: Drag and drop',
47+
dndOutsideSource: 'Addon: Drag and drop (from outside calendar)',
4648
}
4749

4850
const DEFAULT_EXAMPLE = 'basic'
@@ -78,6 +80,7 @@ class Example extends React.Component {
7880
timeslots: Timeslots,
7981
dnd: Dnd,
8082
dndresource: DndResource,
83+
dndOutsideSource: DndOutsideSource,
8184
}[selected]
8285

8386
return (

examples/demos/dndOutsideSource.js

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
import React from 'react'
2+
import events from '../events'
3+
import BigCalendar from 'react-big-calendar'
4+
import withDragAndDrop from 'react-big-calendar/lib/addons/dragAndDrop'
5+
import Layout from 'react-tackle-box/Layout'
6+
import Card from '../Card'
7+
8+
import 'react-big-calendar/lib/addons/dragAndDrop/styles.less'
9+
10+
const DragAndDropCalendar = withDragAndDrop(BigCalendar)
11+
12+
const formatName = (name, count) => `${name} ID ${count}`
13+
14+
class Dnd extends React.Component {
15+
constructor(props) {
16+
super(props)
17+
this.state = {
18+
events: events,
19+
draggedEvent: null,
20+
counters: {
21+
item1: 0,
22+
item2: 0,
23+
},
24+
}
25+
}
26+
27+
handleDragStart = name => {
28+
this.setState({ draggedEvent: name })
29+
}
30+
31+
customOnDragOver = event => {
32+
// check for undroppable is specific to this example
33+
// and not part of API. This just demonstrates that
34+
// onDragOver can optionally be passed to conditionally
35+
// allow draggable items to be dropped on cal, based on
36+
// whether event.preventDefault is called
37+
if (this.state.draggedEvent !== 'undroppable') {
38+
console.log('preventDefault')
39+
event.preventDefault()
40+
}
41+
}
42+
43+
onDropFromOutside = ({ start, end, allDay }) => {
44+
const { draggedEvent, counters } = this.state
45+
const event = {
46+
title: formatName(draggedEvent, counters[draggedEvent]),
47+
start,
48+
end,
49+
isAllDay: allDay,
50+
}
51+
const updatedCounters = {
52+
...counters,
53+
[draggedEvent]: counters[draggedEvent] + 1,
54+
}
55+
this.setState({ draggedEvent: null, counters: updatedCounters })
56+
this.newEvent(event)
57+
}
58+
59+
moveEvent({ event, start, end, isAllDay: droppedOnAllDaySlot }) {
60+
const { events } = this.state
61+
62+
const idx = events.indexOf(event)
63+
let allDay = event.allDay
64+
65+
if (!event.allDay && droppedOnAllDaySlot) {
66+
allDay = true
67+
} else if (event.allDay && !droppedOnAllDaySlot) {
68+
allDay = false
69+
}
70+
71+
const updatedEvent = { ...event, start, end, allDay }
72+
73+
const nextEvents = [...events]
74+
nextEvents.splice(idx, 1, updatedEvent)
75+
76+
this.setState({
77+
events: nextEvents,
78+
})
79+
80+
// alert(`${event.title} was dropped onto ${updatedEvent.start}`)
81+
}
82+
83+
resizeEvent = ({ event, start, end }) => {
84+
const { events } = this.state
85+
86+
const nextEvents = events.map(existingEvent => {
87+
return existingEvent.id == event.id
88+
? { ...existingEvent, start, end }
89+
: existingEvent
90+
})
91+
92+
this.setState({
93+
events: nextEvents,
94+
})
95+
96+
//alert(`${event.title} was resized to ${start}-${end}`)
97+
}
98+
99+
newEvent(event) {
100+
let idList = this.state.events.map(a => a.id)
101+
let newId = Math.max(...idList) + 1
102+
let hour = {
103+
id: newId,
104+
title: event.title,
105+
allDay: event.isAllDay,
106+
start: event.start,
107+
end: event.end,
108+
}
109+
this.setState({
110+
events: this.state.events.concat([hour]),
111+
})
112+
}
113+
114+
render() {
115+
return (
116+
<div>
117+
<Card
118+
className="examples--header"
119+
style={{
120+
display: 'flex',
121+
justifyContent: 'center',
122+
flexWrap: 'wrap',
123+
}}
124+
>
125+
<h4 style={{ color: 'gray', width: '100%' }}>Outside Drag Sources</h4>
126+
{Object.entries(this.state.counters).map(([name, count]) => (
127+
<div
128+
style={{
129+
border: '2px solid gray',
130+
borderRadius: '4px',
131+
width: '100px',
132+
margin: '10px',
133+
}}
134+
draggable="true"
135+
key={name}
136+
onDragStart={() => this.handleDragStart(name)}
137+
>
138+
{formatName(name, count)}
139+
</div>
140+
))}
141+
<div
142+
style={{
143+
border: '2px solid gray',
144+
borderRadius: '4px',
145+
width: '100px',
146+
margin: '10px',
147+
}}
148+
draggable="true"
149+
key={name}
150+
onDragStart={() => this.handleDragStart('undroppable')}
151+
>
152+
Draggable but not for calendar.
153+
</div>
154+
</Card>
155+
<DragAndDropCalendar
156+
selectable
157+
localizer={this.props.localizer}
158+
events={this.state.events}
159+
onEventDrop={this.moveEvent}
160+
onDropFromOutside={this.onDropFromOutside}
161+
onDragOver={this.customOnDragOver}
162+
resizable
163+
onEventResize={this.resizeEvent}
164+
onSelectSlot={this.newEvent}
165+
onD
166+
defaultView={BigCalendar.Views.MONTH}
167+
defaultDate={new Date(2015, 3, 12)}
168+
/>
169+
</div>
170+
)
171+
}
172+
}
173+
174+
export default Dnd

src/Selection.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ class Selection {
5454
this._handleMoveEvent = this._handleMoveEvent.bind(this)
5555
this._handleTerminatingEvent = this._handleTerminatingEvent.bind(this)
5656
this._keyListener = this._keyListener.bind(this)
57+
this._dropFromOutsideListener = this._dropFromOutsideListener.bind(this)
5758

5859
// Fixes an iOS 10 bug where scrolling could not be prevented on the window.
5960
// https://github.com/metafizzy/flickity/issues/457#issuecomment-254501356
@@ -64,6 +65,10 @@ class Selection {
6465
)
6566
this._onKeyDownListener = addEventListener('keydown', this._keyListener)
6667
this._onKeyUpListener = addEventListener('keyup', this._keyListener)
68+
this._onDropFromOutsideListener = addEventListener(
69+
'drop',
70+
this._dropFromOutsideListener
71+
)
6772
this._addInitialEventListener()
6873
}
6974

@@ -187,6 +192,19 @@ class Selection {
187192
}
188193
}
189194

195+
_dropFromOutsideListener(e) {
196+
const { pageX, pageY, clientX, clientY } = getEventCoordinates(e)
197+
198+
this.emit('dropFromOutside', {
199+
x: pageX,
200+
y: pageY,
201+
clientX: clientX,
202+
clientY: clientY,
203+
})
204+
205+
e.preventDefault()
206+
}
207+
190208
_handleInitialEvent(e) {
191209
const { clientX, clientY, pageX, pageY } = getEventCoordinates(e)
192210
let node = this.container(),

src/addons/dragAndDrop/EventContainerWrapper.js

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class EventContainerWrapper extends React.Component {
3131
draggable: PropTypes.shape({
3232
onStart: PropTypes.func,
3333
onEnd: PropTypes.func,
34+
onDropFromOutside: PropTypes.func,
3435
onBeginAction: PropTypes.func,
3536
dragAndDropAction: PropTypes.object,
3637
}),
@@ -113,6 +114,21 @@ class EventContainerWrapper extends React.Component {
113114
this.update(event, slotMetrics.getRange(start, end))
114115
}
115116

117+
handleDropFromOutside = (point, boundaryBox) => {
118+
const { slotMetrics } = this.props
119+
120+
let start = slotMetrics.closestSlotFromPoint(
121+
{ y: point.y, x: point.x },
122+
boundaryBox
123+
)
124+
125+
this.context.draggable.onDropFromOutside({
126+
start,
127+
end: slotMetrics.nextSlot(start),
128+
allDay: false,
129+
})
130+
}
131+
116132
_selectable = () => {
117133
let node = findDOMNode(this)
118134
let selector = (this._selector = new Selection(() =>
@@ -141,6 +157,16 @@ class EventContainerWrapper extends React.Component {
141157
if (dragAndDropAction.action === 'resize') this.handleResize(box, bounds)
142158
})
143159

160+
selector.on('dropFromOutside', point => {
161+
if (!this.context.draggable.onDropFromOutside) return
162+
163+
const bounds = getBoundsForNode(node)
164+
165+
if (!pointInColumn(bounds, point)) return
166+
167+
this.handleDropFromOutside(point, bounds)
168+
})
169+
144170
selector.on('selectStart', () => this.context.draggable.onStart())
145171

146172
selector.on('select', point => {

src/addons/dragAndDrop/WeekWrapper.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ class WeekWrapper extends React.Component {
3737
onStart: PropTypes.func,
3838
onEnd: PropTypes.func,
3939
dragAndDropAction: PropTypes.object,
40+
onDropFromOutside: PropTypes.func,
4041
onBeginAction: PropTypes.func,
4142
}),
4243
}
@@ -106,6 +107,21 @@ class WeekWrapper extends React.Component {
106107
this.update(event, start, end)
107108
}
108109

110+
handleDropFromOutside = (point, rowBox) => {
111+
if (!this.context.draggable.onDropFromOutside) return
112+
const { slotMetrics: metrics } = this.props
113+
114+
let start = metrics.getDateForSlot(
115+
getSlotAtX(rowBox, point.x, false, metrics.slots)
116+
)
117+
118+
this.context.draggable.onDropFromOutside({
119+
start,
120+
end: dates.add(start, 1, 'day'),
121+
allDay: false,
122+
})
123+
}
124+
109125
handleResize(point, node) {
110126
const { event, direction } = this.context.draggable.dragAndDropAction
111127
const { accessors, slotMetrics: metrics } = this.props
@@ -193,6 +209,17 @@ class WeekWrapper extends React.Component {
193209
if (!this.state.segment || !pointInBox(bounds, point)) return
194210
this.handleInteractionEnd()
195211
})
212+
213+
selector.on('dropFromOutside', point => {
214+
if (!this.context.draggable.onDropFromOutside) return
215+
216+
const bounds = getBoundsForNode(node)
217+
218+
if (!pointInBox(bounds, point)) return
219+
220+
this.handleDropFromOutside(point, bounds)
221+
})
222+
196223
selector.on('click', () => this.context.draggable.onEnd(null))
197224
}
198225

0 commit comments

Comments
 (0)