diff --git a/examples/App.js b/examples/App.js index b6dee8809..2f49f9fa8 100644 --- a/examples/App.js +++ b/examples/App.js @@ -1,8 +1,9 @@ import React from 'react' import Api from './Api' import Intro from './Intro.md' -import cn from 'classnames' import { render } from 'react-dom' +import { SlotProvider } from 'react-tackle-box/lib/Slot' +import Layout from 'react-tackle-box/lib/Layout' import localizer from 'react-big-calendar/lib/localizers/globalize' import globalize from 'globalize' @@ -13,6 +14,8 @@ import 'font-awesome/css/font-awesome.min.css' import 'react-big-calendar/lib/less/styles.less' import './styles.less' import './prism.less' +import Card from './Card' +import ExampleControlSlot from './ExampleControlSlot' import Basic from './demos/basic' import Selectable from './demos/selectable' import Cultures from './demos/cultures' @@ -73,65 +76,77 @@ class Example extends React.Component { }[selected] return ( -
-
-
-

- Big Calendar -

-

such enterprise, very business.

-

- - Getting started - - {' | '} - - API documentation - - {' | '} - - github - -

+ +
+
+
+

+ Big Calendar +

+

such enterprise, very business.

+

+ + Getting started + + {' | '} + + API documentation + + {' | '} + + github + +

+
-
-
-
-
- - - - {' View example source code'} - - +
+ + + + + + {EXAMPLES[selected]} + + + {Object.entries(EXAMPLES).map(([key, title]) => ( + this.select(key)} + > + {title} + + ))} + + + + + +
+
- - - {EXAMPLES[selected]} - - - {Object.entries(EXAMPLES).map(([key, title]) => ( - this.select(key)}> - {title} - - ))} - - -
-
-
-
-
-
- +
+
+ +
+
-
-
+
) } } diff --git a/examples/Card.js b/examples/Card.js new file mode 100644 index 000000000..cb44222ba --- /dev/null +++ b/examples/Card.js @@ -0,0 +1,15 @@ +import React from 'react' + +const propTypes = {} + +function Card({ children, className, style }) { + return ( +
+ {children} +
+ ) +} + +Card.propTypes = propTypes + +export default Card diff --git a/examples/ExampleControlSlot.js b/examples/ExampleControlSlot.js new file mode 100644 index 000000000..091c53f01 --- /dev/null +++ b/examples/ExampleControlSlot.js @@ -0,0 +1,4 @@ +import React from 'react' +import createSlot from 'react-tackle-box/lib/Slot' + +export default createSlot() diff --git a/examples/demos/cultures.js b/examples/demos/cultures.js index f54a7935b..8e0e14cfd 100644 --- a/examples/demos/cultures.js +++ b/examples/demos/cultures.js @@ -1,6 +1,9 @@ import React from 'react' import BigCalendar from 'react-big-calendar' import events from '../events' +import Layout from 'react-tackle-box/lib/Layout' + +import ExampleControlSlot from '../ExampleControlSlot' require('globalize/lib/cultures/globalize.culture.en-GB') require('globalize/lib/cultures/globalize.culture.es') @@ -17,21 +20,23 @@ class Cultures extends React.Component { return ( -
- {' '} - -
+ + + {' '} + + + { } let CustomView = ({ localizer }) => ( - + + + The Calendar below implments a custom 3-day week view + + + ) export default CustomView diff --git a/examples/demos/popup.js b/examples/demos/popup.js index a663bec28..94f34df9a 100644 --- a/examples/demos/popup.js +++ b/examples/demos/popup.js @@ -1,13 +1,16 @@ import React from 'react' import BigCalendar from 'react-big-calendar' import events from '../events' +import ExampleControlSlot from '../ExampleControlSlot' let Popup = ({ localizer }) => ( -

- Click the "+x more" link on any calendar day that cannot fit all the days - events to see an inline popup of all the events. -

+ + + Click the "+x more" link on any calendar day that cannot fit all the + days events to see an inline popup of all the events. + + ( - -

- Click an event to see more info, or drag the mouse over the calendar to - select a date/time range. -

- alert(event.title)} - onSelectSlot={slotInfo => - alert( - `selected slot: \n\nstart ${slotInfo.start.toLocaleString()} ` + - `\nend: ${slotInfo.end.toLocaleString()}` + - `\naction: ${slotInfo.action}` - ) - } - /> -
-) +const propTypes = {} + +class Selectable extends React.Component { + constructor(...args) { + super(...args) + + this.state = { events } + } + + handleSelect = ({ start, end }) => { + const title = window.prompt('New Event name') + if (title) + this.setState({ + events: [ + ...this.state.events, + { + start, + end, + title, + }, + ], + }) + } + + render() { + const { localizer } = this.props + return ( + + + + Click an event to see more info, or drag the mouse over the calendar + to select a date/time range. + + + alert(event.title)} + onSelectSlot={this.handleSelect} + /> + + ) + } +} + +Selectable.propTypes = propTypes export default Selectable diff --git a/examples/styles.less b/examples/styles.less index 459a6238e..844d1009d 100644 --- a/examples/styles.less +++ b/examples/styles.less @@ -1,5 +1,4 @@ - -@import "~bootstrap/less/variables.less"; +@import '~bootstrap/less/variables.less'; @blue: #3174ad; @pink: #ad3173; @@ -10,7 +9,8 @@ html { body { font-size: 16px; - font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Helvetica,Arial,sans-serif,"Apple Color"; + font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, + sans-serif, 'Apple Color'; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } @@ -20,7 +20,7 @@ h4 { } a { - transform: all 200ms; + transition: all 200ms; &, &:hover, @@ -63,9 +63,9 @@ a { .example { font-size: 14px; padding: 0 40px; - min-height: ~"calc(100vh - 100px)"; + min-height: ~'calc(100vh - 100px)'; min-height: max-content; - height: ~"calc(100vh - 100px)"; + height: ~'calc(100vh - 100px)'; width: 100%; margin: auto; @@ -97,12 +97,13 @@ a { text-decoration: none; padding: 1.4rem 1rem; white-space: nowrap; - border-radius: .3rem; + border-radius: 0.3rem; &:after { content: ''; position: absolute; - left: 0; right: 0; + left: 0; + right: 0; bottom: 0; height: 4px; } @@ -121,24 +122,18 @@ aside { } h3 > a > code, -h4 > a > code { +h4 > a > code { color: @blue; background: none; padding: 0; } - .examples--header { - padding: 0 40px; - display: flex; - justify-content: space-between; - align-items: center; - - margin-bottom: 15px; + margin: 0 40px; + text-align: center; } .examples--dropdown { - } .dropdown--toggle { @@ -165,7 +160,7 @@ h4 > a > code { } .examples--view-source { - font-size: 90%; + font-size: 80%; } .callout { @@ -189,19 +184,18 @@ pre.shape-prop { code { color: #555; - background-color: rgba(0,0,0,0.04); + background-color: rgba(0, 0, 0, 0.04); } .playgroundStage, .cm-s-neo.CodeMirror { - background-color: #F4F4F4; + background-color: #f4f4f4; height: auto; min-height: 75px; } .CodeMirror { font-size: 12px; - } .cm-s-neo div.CodeMirror-cursor { @@ -217,7 +211,6 @@ code { color: #905; } - .prop-table { font-size: 14 px; } @@ -236,29 +229,24 @@ code { height: auto; } - - .playgroundPreview { position: relative; padding: 40px 15px 15px 15px; } - - .playgroundPreview:before { position: absolute; top: 3px; left: 7px; color: #959595; - border-bottom: 1px solid #eee; + border-bottom: 1px solid #eee; padding: 0 3px; content: 'Result'; } - -.playground { +.playground { position: relative; - margin: 0; + margin: 0; margin-bottom: 20px; border-top: 1px solid #ccc; } @@ -289,9 +277,7 @@ code { } } - .playgroundStage { - } .anchor, @@ -324,3 +310,13 @@ h4 a:focus .anchor-icon { .special-day { background-color: #fec; } + +.card { + background-color: white; + border: 0; + padding: 24px; + border-radius: 2px; + margin-bottom: 20px; + box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.14), 0 3px 1px -2px rgba(0, 0, 0, 0.2), + 0 1px 5px 0 rgba(0, 0, 0, 0.12); +} diff --git a/examples/webpack.config.js b/examples/webpack.config.js index c226ae2a2..0d5bbedfe 100644 --- a/examples/webpack.config.js +++ b/examples/webpack.config.js @@ -28,7 +28,14 @@ module.exports = { rules.js({}), rules.images(), rules.fonts(), - rules.css(), + { + oneOf: [ + Object.assign({}, rules.css({ modules: true }), { + test: /\.module\.css/, + }), + rules.css(), + ], + }, rules.less({ browsers }), { test: /\.md/, diff --git a/package.json b/package.json index 224354bc8..6496ec078 100644 --- a/package.json +++ b/package.json @@ -109,6 +109,7 @@ "prop-types": "^15.5.8", "react-overlays": "^0.7.0", "react-prop-types": "^0.4.0", + "react-tackle-box": "^1.1.1", "uncontrollable": "^4.0.0", "warning": "^2.0.0" } diff --git a/src/TimeGrid.js b/src/TimeGrid.js index be363a191..5a0500f2b 100644 --- a/src/TimeGrid.js +++ b/src/TimeGrid.js @@ -12,6 +12,7 @@ import getWidth from 'dom-helpers/query/width' import TimeGridHeader from './TimeGridHeader' import { notify } from './utils/helpers' import { inRange, sortEvents } from './utils/eventLevels' +import Resources from './utils/Resources' export default class TimeGrid extends Component { static propTypes = { @@ -62,6 +63,10 @@ export default class TimeGrid extends Component { super(props) this.state = { gutterWidth: undefined, isOverflowing: null } + + this.scrollRef = React.createRef() + + this.resources = Resources(props.resources, props.accessors) } componentWillMount() { @@ -83,11 +88,16 @@ export default class TimeGrid extends Component { window.addEventListener('resize', this.handleResize) } + handleScroll = e => { + if (this.scrollRef.current) { + this.scrollRef.current.scrollLeft = e.target.scrollLeft + } + } + handleResize = () => { raf.cancel(this.rafHandle) this.rafHandle = raf(this.checkOverflow) } - componentWillUnmount() { window.clearTimeout(this._timeIndicatorTimeout) window.removeEventListener('resize', this.handleResize) @@ -136,19 +146,21 @@ export default class TimeGrid extends Component { }) } - renderEvents(range, events, today, resources) { + renderEvents(range, events, today) { let { min, max, components, accessors, localizer } = this.props - return range.map((date, idx) => { - let daysEvents = events.filter(event => - dates.inRange(date, accessors.start(event), accessors.end(event), 'day') - ) - - return resources.map((resource, id) => { - const resourceId = accessors.resourceId(resource) - let eventsToDisplay = !resource - ? daysEvents - : daysEvents.filter(event => accessors.resource(event) === resourceId) + const groupedEvents = this.resources.groupEvents(events) + + return this.resources.map(([id, resource], i) => + range.map((date, jj) => { + let daysEvents = (groupedEvents.get(id) || []).filter(event => + dates.inRange( + date, + accessors.start(event), + accessors.end(event), + 'day' + ) + ) return ( ) }) - }) + ) } render() { @@ -216,19 +228,22 @@ export default class TimeGrid extends Component { allDayEvents.sort((a, b) => sortEvents(a, b, accessors)) return ( -
+
-
+
- {this.renderEvents(range, rangeEvents, getNow(), resources || [null])} + {this.renderEvents(range, rangeEvents, getNow())}
diff --git a/src/TimeGridEvent.js b/src/TimeGridEvent.js index e7f47be5a..b8a2e033d 100644 --- a/src/TimeGridEvent.js +++ b/src/TimeGridEvent.js @@ -14,6 +14,8 @@ function TimeGridEvent(props) { continuesEarlier, continuesLater, getters, + onClick, + onDoubleClick, components: { event: Event, eventWrapper: EventWrapper }, } = props let title = accessors.title(event) diff --git a/src/TimeGridHeader.js b/src/TimeGridHeader.js index f94810d5a..158be899c 100644 --- a/src/TimeGridHeader.js +++ b/src/TimeGridHeader.js @@ -12,7 +12,7 @@ class TimeGridHeader extends React.Component { static propTypes = { range: PropTypes.array.isRequired, events: PropTypes.array.isRequired, - resources: PropTypes.array, + resources: PropTypes.object, getNow: PropTypes.func.isRequired, isOverflowing: PropTypes.bool, @@ -33,6 +33,7 @@ class TimeGridHeader extends React.Component { onDoubleClickEvent: PropTypes.func, onDrillDown: PropTypes.func, getDrilldownView: PropTypes.func.isRequired, + scrollRef: PropTypes.any, } handleHeaderClick = (date, view, e) => { @@ -40,27 +41,6 @@ class TimeGridHeader extends React.Component { notify(this.props.onDrillDown, [date, view]) } - renderHeaderResources(range, resources) { - const { accessors, getNow } = this.props - const today = getNow() - - return range.map((date, i) => { - return resources.map((resource, j) => { - return ( -
- {accessors.resourceTitle(resource)} -
- ) - }) - }) - } - renderHeaderCells(range) { let { localizer, @@ -154,6 +134,14 @@ class TimeGridHeader extends React.Component { rtl, resources, range, + events, + getNow, + accessors, + selectable, + components, + getters, + scrollRef, + localizer, isOverflowing, components: { timeGutterHeader: TimeGutterHeader }, } = this.props @@ -163,34 +151,58 @@ class TimeGridHeader extends React.Component { style[rtl ? 'marginLeft' : 'marginRight'] = `${scrollbarSize()}px` } + const groupedEvents = resources.groupEvents(events) + return (
-
+
{TimeGutterHeader && }
-
-
- {this.renderHeaderCells(range)} + {resources.map(([id, resource], idx) => ( +
+ {resource && ( +
+
+ {accessors.resourceTitle(resource)} +
+
+ )} + {/* For rendering only one day no need to show the headers */} + {range.length > 1 && ( +
+ {this.renderHeaderCells(range)} +
+ )} +
- {resources && ( -
- {this.renderHeaderResources(range, resources)} -
- )} - - {resources ? ( -
- {resources.map(resource => this.renderRow(resource))} -
- ) : ( - this.renderRow() - )} -
+ ))}
) } diff --git a/src/addons/README.md b/src/addons/README.md new file mode 100644 index 000000000..87b5a059d --- /dev/null +++ b/src/addons/README.md @@ -0,0 +1,5 @@ +### react-big-calendar Addons + +Addons are community contributed and maintained additional functionality built on top of the core `react-big-calendar` + +* [Drag and Drop](./dragAndDrop/README.md) diff --git a/src/addons/dragAndDrop/EventContainerWrapper.js b/src/addons/dragAndDrop/EventContainerWrapper.js index 06117cd14..e5f2bde28 100644 --- a/src/addons/dragAndDrop/EventContainerWrapper.js +++ b/src/addons/dragAndDrop/EventContainerWrapper.js @@ -11,9 +11,9 @@ import TimeGridEvent from '../../TimeGridEvent' import { dragAccessors } from './common' import NoopWrapper from '../../NoopWrapper' -const pointerInColumn = (node, x, y) => { - const { left, right, top } = getBoundsForNode(node) - return x < right + 10 && x > left && (y == null || y > top) +const pointInColumn = (bounds, { x, y }) => { + const { left, right, top } = bounds + return x < right + 10 && x > left && y > top } const propTypes = {} @@ -28,11 +28,12 @@ class EventContainerWrapper extends React.Component { } static contextTypes = { - onEventDrop: PropTypes.func, - onEventResize: PropTypes.func, - dragAndDropAction: PropTypes.object, - onMove: PropTypes.func, - onResize: PropTypes.func, + draggable: PropTypes.shape({ + onStart: PropTypes.func, + onEnd: PropTypes.func, + onBeginAction: PropTypes.func, + dragAndDropAction: PropTypes.object, + }), } constructor(...args) { @@ -54,8 +55,6 @@ class EventContainerWrapper extends React.Component { } update(event, { startDate, endDate, top, height }) { - this.setState() - const { event: lastEvent } = this.state if ( lastEvent && @@ -72,17 +71,18 @@ class EventContainerWrapper extends React.Component { }) } - handleMove = ({ event }, point, node) => { + handleMove = (point, boundaryBox) => { + const { event } = this.context.draggable.dragAndDropAction const { slotMetrics } = this.props - if (!pointerInColumn(node, point.x, point.y)) { + if (!pointInColumn(boundaryBox, point)) { this.reset() return } let currentSlot = slotMetrics.closestSlotFromPoint( { y: point.y - this.eventOffsetTop, x: point.x }, - getBoundsForNode(node) + boundaryBox ) let end = dates.add( @@ -94,12 +94,12 @@ class EventContainerWrapper extends React.Component { this.update(event, slotMetrics.getRange(currentSlot, end)) } - handleResize({ event, direction }, point, node) { + handleResize(point, boundaryBox) { let start, end const { accessors, slotMetrics } = this.props - const bounds = getBoundsForNode(node) + const { event, direction } = this.context.draggable.dragAndDropAction - let currentSlot = slotMetrics.closestSlotFromPoint(point, bounds) + let currentSlot = slotMetrics.closestSlotFromPoint(point, boundaryBox) if (direction === 'UP') { end = accessors.end(event) start = dates.min(currentSlot, slotMetrics.closestSlotFromDate(end, -1)) @@ -118,86 +118,49 @@ class EventContainerWrapper extends React.Component { )) selector.on('beforeSelect', point => { - const { action } = this.context.dragAndDropAction - const eventNode = getEventNodeFromPoint(node, point) + const { dragAndDropAction } = this.context.draggable + + if (!dragAndDropAction.action) return false + if (dragAndDropAction.action === 'resize') { + return pointInColumn(getBoundsForNode(node), point) + } + const eventNode = getEventNodeFromPoint(node, point) if (!eventNode) return false - this.eventOffsetTop = point.y - getBoundsForNode(eventNode).top - return ( - action === 'move' || - (action === 'resize' && pointerInColumn(node, point.x, point.y)) - ) + this.eventOffsetTop = point.y - getBoundsForNode(eventNode).top }) - let handler = box => { - const { dragAndDropAction } = this.context + selector.on('selecting', box => { + const bounds = getBoundsForNode(node) + const { dragAndDropAction } = this.context.draggable - switch (dragAndDropAction.action) { - case 'move': - this.handleMove(dragAndDropAction, box, node) - break - case 'resize': - this.handleResize(dragAndDropAction, box, node) - break - } - } - - selector.on('selecting', handler) + if (dragAndDropAction.action === 'move') this.handleMove(box, bounds) + if (dragAndDropAction.action === 'resize') this.handleResize(box, bounds) + }) - selector.on('select', () => { - const { dragAndDropAction } = this.context + selector.on('selectStart', () => this.context.draggable.onStart()) - switch (dragAndDropAction.action) { - case 'move': - this.handleEventDrop() - break - case 'resize': - this.handleEventResize() - break - } + selector.on('select', point => { + const bounds = getBoundsForNode(node) - this._isInitialContainer = false + if (!this.state.event || !pointInColumn(bounds, point)) return + this.handleInteractionEnd() }) - selector.on('click', () => { - this._isInitialContainer = false - this.context.onMove(null) - }) + selector.on('click', () => this.context.draggable.onEnd(null)) } - handleEventDrop = () => { - if (!this.state.event) return - + handleInteractionEnd = () => { const { resource } = this.props - const { start, end } = this.state.event - const { dragAndDropAction, onMove, onEventDrop } = this.context - - this.reset() - - onMove(null) - - onEventDrop({ - end, - start, - event: dragAndDropAction.event, - resourceId: resource, - }) - } - - handleEventResize = () => { const { event } = this.state - const { dragAndDropAction, onResize, onEventResize } = this.context - this.reset() - onResize(null) - - onEventResize({ - event: dragAndDropAction.event, + this.context.draggable.onEnd({ start: event.start, end: event.end, + resourceId: resource, }) } diff --git a/src/addons/dragAndDrop/EventWrapper.js b/src/addons/dragAndDrop/EventWrapper.js index 3e3875037..a59f31711 100644 --- a/src/addons/dragAndDrop/EventWrapper.js +++ b/src/addons/dragAndDrop/EventWrapper.js @@ -4,16 +4,16 @@ import cn from 'classnames' import { accessor } from '../../utils/propTypes' import { accessor as get } from '../../utils/accessors' -import BigCalendar from '../../index' - class EventWrapper extends React.Component { static contextTypes = { - components: PropTypes.object, - draggableAccessor: accessor, - resizableAccessor: accessor, - onMove: PropTypes.func.isRequired, - onResize: PropTypes.func.isRequired, - dragAndDropAction: PropTypes.object, + draggable: PropTypes.shape({ + onStart: PropTypes.func, + onEnd: PropTypes.func, + onBeginAction: PropTypes.func, + draggableAccessor: accessor, + resizableAccessor: accessor, + dragAndDropAction: PropTypes.object, + }), } static propTypes = { @@ -32,25 +32,27 @@ class EventWrapper extends React.Component { handleResizeUp = e => { if (e.button !== 0) return e.stopPropagation() - this.context.onResize(this.props.event, 'UP') + this.context.draggable.onBeginAction(this.props.event, 'resize', 'UP') } handleResizeDown = e => { if (e.button !== 0) return e.stopPropagation() - this.context.onResize(this.props.event, 'DOWN') + this.context.draggable.onBeginAction(this.props.event, 'resize', 'DOWN') } handleResizeLeft = e => { if (e.button !== 0) return e.stopPropagation() - this.context.onResize(this.props.event, 'LEFT') + this.context.draggable.onBeginAction(this.props.event, 'resize', 'LEFT') } handleResizeRight = e => { if (e.button !== 0) return e.stopPropagation() - this.context.onResize(this.props.event, 'RIGHT') + this.context.draggable.onBeginAction(this.props.event, 'resize', 'RIGHT') } handleStartDragging = e => { - if (e.button === 0) this.context.onMove(this.props.event) + if (e.button === 0) { + this.context.draggable.onBeginAction(this.props.event, 'move') + } } renderAnchor(direction) { @@ -66,19 +68,7 @@ class EventWrapper extends React.Component { } render() { - const { components } = this.context - const EventWrapper = - components.eventWrapper || BigCalendar.components.eventWrapper - - let { - isResizing, - children, - event, - allDay, - type, - continuesPrior, - continuesAfter, - } = this.props + let { children, event, type, continuesPrior, continuesAfter } = this.props if (event.__isPreview) return React.cloneElement(children, { @@ -128,8 +118,6 @@ class EventWrapper extends React.Component { EndAnchor = !continuesAfter && this.renderAnchor('Down') } - const isDragging = this.context.dragAndDropAction.event === event - /* * props.children is the singular component. * BigCalendar positions the Event abolutely and we @@ -141,11 +129,6 @@ class EventWrapper extends React.Component { children = React.cloneElement(children, { onMouseDown: this.handleStartDragging, onTouchStart: this.handleStartDragging, - className: cn( - children.props.className, - isDragging && 'rbc-addons-dnd-dragging', - isResizing && 'rbc-addons-dnd-resizing' - ), // replace original event child with anchor-embellished child children: (
@@ -157,11 +140,7 @@ class EventWrapper extends React.Component { }) } - return ( - - {children} - - ) + return children } } diff --git a/src/addons/dragAndDrop/README.md b/src/addons/dragAndDrop/README.md new file mode 100644 index 000000000..f4f75cd3a --- /dev/null +++ b/src/addons/dragAndDrop/README.md @@ -0,0 +1,20 @@ +### Drag and Drop + +```js +import BigCalendar from 'react-big-calendar' +import withDragAndDrop from 'react-big-calendar/lib/addons/dragAndDrop' + +import 'react-big-calendar/lib/addons/dragAndDrop/styles.css' + +const DraggableCalendar = withDragAndDrop(BigCalendar) + +/* ... */ + +return ( + +) +``` diff --git a/src/addons/dragAndDrop/WeekWrapper.js b/src/addons/dragAndDrop/WeekWrapper.js index d3379e733..8870204fa 100644 --- a/src/addons/dragAndDrop/WeekWrapper.js +++ b/src/addons/dragAndDrop/WeekWrapper.js @@ -33,12 +33,12 @@ class WeekWrapper extends React.Component { } static contextTypes = { - onEventDrop: PropTypes.func, - onEventResize: PropTypes.func, - - onMove: PropTypes.func, - onResize: PropTypes.func, - dragAndDropAction: PropTypes.object, + draggable: PropTypes.shape({ + onStart: PropTypes.func, + onEnd: PropTypes.func, + dragAndDropAction: PropTypes.object, + onBeginAction: PropTypes.func, + }), } constructor(...args) { @@ -77,7 +77,8 @@ class WeekWrapper extends React.Component { this.setState({ segment }) } - handleMove = ({ event }, { x, y }, node) => { + handleMove = ({ x, y }, node) => { + const { event } = this.context.draggable.dragAndDropAction const metrics = this.props.slotMetrics const { accessors } = this.props @@ -105,7 +106,8 @@ class WeekWrapper extends React.Component { this.update(event, start, end) } - handleResize({ event, direction }, point, node) { + handleResize(point, node) { + const { event, direction } = this.context.draggable.dragAndDropAction const { accessors, slotMetrics: metrics } = this.props let { start, end } = eventTimes(event, accessors) @@ -167,7 +169,7 @@ class WeekWrapper extends React.Component { selector.on('beforeSelect', point => { const { isAllDay } = this.props - const { action } = this.context.dragAndDropAction + const { action } = this.context.draggable.dragAndDropAction return ( action === 'move' || @@ -176,74 +178,35 @@ class WeekWrapper extends React.Component { ) }) - let handler = box => { - const { dragAndDropAction } = this.context - - switch (dragAndDropAction.action) { - case 'move': - this.handleMove(dragAndDropAction, box, node) - break - case 'resize': - this.handleResize(dragAndDropAction, box, node) - break - } - } - - selector.on('selecting', handler) - selector.on('selectStart', handler) - - selector.on('select', box => { - const { dragAndDropAction } = this.context - - switch (dragAndDropAction.action) { - case 'move': - this.handleEventDrop() - break - case 'resize': - this.handleEventResize(box, node) - break - } - }) + selector.on('selecting', box => { + const bounds = getBoundsForNode(node) + const { dragAndDropAction } = this.context.draggable - selector.on('click', () => { - this.context.onMove(null) + if (dragAndDropAction.action === 'move') this.handleMove(box, bounds) + if (dragAndDropAction.action === 'resize') this.handleResize(box, bounds) }) - } - - handleEventResize = (box, node) => { - const { segment } = this.state - - if (!segment || !pointInBox(getBoundsForNode(node), box)) return - const { dragAndDropAction, onResize, onEventResize } = this.context - this.reset() - - onResize(null) + selector.on('selectStart', () => this.context.draggable.onStart()) + selector.on('select', point => { + const bounds = getBoundsForNode(node) - onEventResize({ - event: dragAndDropAction.event, - start: segment.event.start, - end: segment.event.end, + if (!this.state.segment || !pointInBox(bounds, point)) return + this.handleInteractionEnd() }) + selector.on('click', () => this.context.draggable.onEnd(null)) } - handleEventDrop = () => { - const { resourceId } = this.props - const { segment } = this.state - - if (!segment) return - const { dragAndDropAction, onMove, onEventDrop } = this.context + handleInteractionEnd = () => { + const { resourceId, isAllDay } = this.props + const { event } = this.state.segment this.reset() - onMove(null) - - onEventDrop({ + this.context.draggable.onEnd({ + start: event.start, + end: event.end, resourceId, - event: dragAndDropAction.event, - start: segment.event.start, - end: segment.event.end, - isAllDay: true, + isAllDay, }) } diff --git a/src/addons/dragAndDrop/common.js b/src/addons/dragAndDrop/common.js index 6e52746fc..34e17d1f4 100644 --- a/src/addons/dragAndDrop/common.js +++ b/src/addons/dragAndDrop/common.js @@ -1,6 +1,25 @@ import { wrapAccessor } from '../../utils/accessors' +import { createFactory } from 'react' export const dragAccessors = { start: wrapAccessor(e => e.start), end: wrapAccessor(e => e.end), } + +export const nest = (...Components) => { + const factories = Components.filter(Boolean).map(createFactory) + const Nest = ({ children, ...props }) => + factories.reduceRight((child, factory) => factory(props, child), children) + + return Nest +} + +export const mergeComponents = (components = {}, addons) => { + const keys = Object.keys(addons) + const result = { ...components } + + keys.forEach(key => { + result[key] = components[key] ? nest(addons[key]) : addons[key] + }) + return result +} diff --git a/src/addons/dragAndDrop/withDragAndDrop.js b/src/addons/dragAndDrop/withDragAndDrop.js index 20054d660..9830d0e7c 100644 --- a/src/addons/dragAndDrop/withDragAndDrop.js +++ b/src/addons/dragAndDrop/withDragAndDrop.js @@ -6,6 +6,7 @@ import { accessor } from '../../utils/propTypes' import EventWrapper from './EventWrapper' import EventContainerWrapper from './EventContainerWrapper' import WeekWrapper from './WeekWrapper' +import { mergeComponents } from './common' /** * Creates a higher-order component (HOC) supporting drag & drop and optionally resizing @@ -52,11 +53,10 @@ export default function withDragAndDrop(Calendar) { static propTypes = { onEventDrop: PropTypes.func, onEventResize: PropTypes.func, - startAccessor: accessor, - endAccessor: accessor, - allDayAccessor: accessor, + draggableAccessor: accessor, resizableAccessor: accessor, + selectable: PropTypes.oneOf([true, false, 'ignoreEvents']), resizable: PropTypes.bool, components: PropTypes.object, @@ -66,9 +66,6 @@ export default function withDragAndDrop(Calendar) { static defaultProps = { // TODO: pick these up from Calendar.defaultProps components: {}, - startAccessor: 'start', - endAccessor: 'end', - allDayAccessor: 'allDay', draggableAccessor: null, resizableAccessor: null, step: 30, @@ -79,51 +76,71 @@ export default function withDragAndDrop(Calendar) { } static childContextTypes = { - onEventDrop: PropTypes.func, - onEventResize: PropTypes.func, - onMove: PropTypes.func, - onResize: PropTypes.func, - dragAndDropAction: PropTypes.object, + draggable: PropTypes.shape({ + onStart: PropTypes.func, + onEnd: PropTypes.func, + onBeginAction: PropTypes.func, + draggableAccessor: accessor, + resizableAccessor: accessor, + dragAndDropAction: PropTypes.object, + }), + } - components: PropTypes.object, - draggableAccessor: accessor, - resizableAccessor: accessor, - step: PropTypes.number, + constructor(...args) { + super(...args) + + const { components } = this.props + + this.components = mergeComponents(components, { + eventWrapper: EventWrapper, + eventContainerWrapper: EventContainerWrapper, + weekWrapper: WeekWrapper, + }) + + this.state = {} } getChildContext() { return { - onEventDrop: this.props.onEventDrop, - onEventResize: this.props.onEventResize, - step: this.props.step, - components: this.props.components, - draggableAccessor: this.props.draggableAccessor, - resizableAccessor: this.props.resizableAccessor, - - onResize: (event, direction) => - this.setState({ - dragAndDropAction: event - ? { action: 'resize', event, direction } - : {}, - }), - - onMove: event => - this.setState({ - dragAndDropAction: event ? { action: 'move', event } : {}, - }), - - dragAndDropAction: this.state.dragAndDropAction, + draggable: { + onStart: this.handleInteractionStart, + onEnd: this.handleInteractionEnd, + onBeginAction: this.handleBeginAction, + draggableAccessor: this.props.draggableAccessor, + resizableAccessor: this.props.resizableAccessor, + dragAndDropAction: this.state, + }, } } - constructor(...args) { - super(...args) - this.state = { isDragging: false, dragAndDropAction: {} } + handleBeginAction = (event, action, direction) => { + this.setState({ event, action, direction }) + } + + handleInteractionStart = () => { + this.setState({ interacting: true }) + } + + handleInteractionEnd = interactionInfo => { + const { action, event } = this.state + + this.setState({ + action: null, + event: null, + interacting: false, + direction: null, + }) + + if (interactionInfo == null) return + + interactionInfo.event = event + if (action === 'move') this.props.onEventDrop(interactionInfo) + if (action === 'resize') this.props.onEventResize(interactionInfo) } render() { - const { selectable, components, ...props } = this.props - const { dragAndDropAction } = this.state + const { selectable, ...props } = this.props + const { interacting } = this.state delete props.onEventDrop delete props.onEventResize @@ -132,17 +149,10 @@ export default function withDragAndDrop(Calendar) { props.className = cn( props.className, 'rbc-addons-dnd', - dragAndDropAction.action && 'rbc-addons-dnd-is-dragging' + !!interacting && 'rbc-addons-dnd-is-dragging' ) - props.components = { - ...components, - eventWrapper: EventWrapper, - eventContainerWrapper: EventContainerWrapper, - weekWrapper: WeekWrapper, - } - - return + return } } diff --git a/src/less/time-column.less b/src/less/time-column.less index c682ed0a3..e31b16c14 100644 --- a/src/less/time-column.less +++ b/src/less/time-column.less @@ -76,6 +76,44 @@ } } +.rbc-time-view-resources { + .rbc-time-gutter, + .rbc-time-header-gutter { + position: sticky; + left: 0; + background-color: white; + border-right: 1px solid @cell-border; + z-index: 10; + margin-right: -1px; + } + + .rbc-time-header { + overflow: hidden; + } + + .rbc-time-header-content { + min-width: auto; + flex: 1 0 0; + flex-basis: 0px; + } + + .rbc-day-slot { + min-width: 140px; + } + + .rbc-header, + .rbc-day-bg, { + width: 140px; + // min-width: 0; + flex: 1 1 0; + flex-basis: 0 px; + } +} + +.rbc-time-header-content + .rbc-time-header-content { + margin-left: -1px; +} + .rbc-time-slot { flex: 1 0 0; diff --git a/src/less/time-grid.less b/src/less/time-grid.less index 10612c7de..538546aa7 100644 --- a/src/less/time-grid.less +++ b/src/less/time-grid.less @@ -82,6 +82,7 @@ .rbc-time-header-content { flex: 1; + display: flex; min-width: 0; flex-direction: column; border-left: 1px solid @cell-border; diff --git a/src/utils/Resources.js b/src/utils/Resources.js new file mode 100644 index 000000000..b0144a33d --- /dev/null +++ b/src/utils/Resources.js @@ -0,0 +1,23 @@ +export const NONE = {} + +export default function Resources(resources, accessors) { + return { + map(fn) { + if (!resources) return [fn([NONE, null], 0)] + return resources.map((resource, idx) => + fn([accessors.resourceId(resource), resource], idx) + ) + }, + + groupEvents(events) { + const eventsByResource = new Map() + events.forEach(event => { + const id = accessors.resource(event) || NONE + let resourceEvents = eventsByResource.get(id) || [] + resourceEvents.push(event) + eventsByResource.set(id, resourceEvents) + }) + return eventsByResource + }, + } +} diff --git a/yarn.lock b/yarn.lock index a13630134..b07c631b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,10 +2,26 @@ # yarn lockfile v1 +"@4c/layout@^0.1.1": + version "0.1.1" + resolved "https://registry.yarnpkg.com/@4c/layout/-/layout-0.1.1.tgz#6181f06927d716bc177687f462a807503a683264" + dependencies: + "@babel/runtime" "^7.0.0-beta.51" + classnames "^2.2.6" + prop-types "^15.6.2" + prop-types-extra "^1.1.0" + "@babel/parser@7.0.0-beta.53": version "7.0.0-beta.53" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.0.0-beta.53.tgz#1f45eb617bf9463d482b2c04d349d9e4edbf4892" +"@babel/runtime@^7.0.0-beta.51": + version "7.0.0-beta.54" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.0.0-beta.54.tgz#39ebb42723fe7ca4b3e1b00e967e80138d47cadf" + dependencies: + core-js "^2.5.7" + regenerator-runtime "^0.12.0" + "@storybook/addon-actions@3.4.8", "@storybook/addon-actions@^3.4.8": version "3.4.8" resolved "https://registry.yarnpkg.com/@storybook/addon-actions/-/addon-actions-3.4.8.tgz#557bae7c7cc60be9d5199ff972b028de8b574f92" @@ -2160,6 +2176,10 @@ classnames@^2.1.3, classnames@^2.2.5: version "2.2.5" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.5.tgz#fb3801d453467649ef3603c7d61a02bd129bde6d" +classnames@^2.2.6: + version "2.2.6" + resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.2.6.tgz#43935bffdd291f326dad0a205309b38d00f650ce" + clean-css@4.1.x: version "4.1.9" resolved "https://registry.yarnpkg.com/clean-css/-/clean-css-4.1.9.tgz#35cee8ae7687a49b98034f70de00c4edd3826301" @@ -2432,7 +2452,7 @@ core-js@^2.4.0, core-js@^2.4.1, core-js@^2.5.0: version "2.5.3" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.3.tgz#8acc38345824f16d8365b7c9b4259168e8ed603e" -core-js@^2.5.3: +core-js@^2.5.3, core-js@^2.5.7: version "2.5.7" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.7.tgz#f972608ff0cead68b841a16a932d0b183791814e" @@ -7258,6 +7278,13 @@ prop-types-extra@^1.0.1: dependencies: warning "^3.0.0" +prop-types-extra@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/prop-types-extra/-/prop-types-extra-1.1.0.tgz#32609910ea2dcf190366bacd3490d5a6412a605f" + dependencies: + react-is "^16.3.2" + warning "^3.0.0" + prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.8, prop-types@^15.5.9, prop-types@^15.6.0: version "15.6.0" resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.0.tgz#ceaf083022fc46b4a35f69e13ef75aed0d639856" @@ -7551,6 +7578,10 @@ react-inspector@^2.2.2: babel-runtime "^6.26.0" is-dom "^1.0.9" +react-is@^16.3.2: + version "16.4.1" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.4.1.tgz#d624c4650d2c65dbd52c72622bbf389435d9776e" + react-lifecycles-compat@^3.0.0, react-lifecycles-compat@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz#4f1a273afdfc8f3488a8c516bfda78f872352362" @@ -7606,6 +7637,12 @@ react-style-proptype@^3.0.0: dependencies: prop-types "^15.5.4" +react-tackle-box@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/react-tackle-box/-/react-tackle-box-1.1.1.tgz#c71c29ec8e8736401e3f0928e71b8fb8cb92b1c4" + dependencies: + "@4c/layout" "^0.1.1" + react-transition-group@^1.1.2: version "1.2.1" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-1.2.1.tgz#e11f72b257f921b213229a774df46612346c7ca6" @@ -7795,6 +7832,10 @@ regenerator-runtime@^0.11.0: version "0.11.1" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" +regenerator-runtime@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.12.0.tgz#8052ac952d85b10f3425192cd0c53f45cf65c6cb" + regenerator-transform@^0.10.0: version "0.10.1" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.10.1.tgz#1e4996837231da8b7f3cf4114d71b5691a0680dd"