UNPKG

tonkean-react-calendar-timeline

Version:
1,354 lines (1,191 loc) 37.4 kB
import PropTypes from 'prop-types' import React, { Component } from 'react' import moment from 'moment' import Items from './items/Items' import InfoLabel from './layout/InfoLabel' import Sidebar from './layout/Sidebar' import Header from './layout/Header' import VerticalLines from './lines/VerticalLines' import GroupRows from './row/GroupRows' import TodayLine from './lines/TodayLine' import CursorLine from './lines/CursorLine' import ScrollElement from './scroll/ScrollElement' import windowResizeDetector from '../resize-detector/window' import { getMinUnit, getNextUnit, stack, forcestack, nostack, calculateDimensions, getGroupOrders, getVisibleItems } from './utility/calendar' import { getParentPosition } from './utility/dom-helpers' import { _get, _length } from './utility/generic' import { defaultKeys, defaultTimeSteps, defaultHeaderLabelFormats, defaultSubHeaderLabelFormats } from './default-config' export default class ReactCalendarTimeline extends Component { static propTypes = { groups: PropTypes.oneOfType([PropTypes.array, PropTypes.object]).isRequired, items: PropTypes.oneOfType([PropTypes.array, PropTypes.object]).isRequired, sidebarWidth: PropTypes.number, sidebarContent: PropTypes.node, rightSidebarWidth: PropTypes.number, rightSidebarContent: PropTypes.node, dragSnap: PropTypes.number, minResizeWidth: PropTypes.number, stickyOffset: PropTypes.number, stickyHeader: PropTypes.bool, lineHeight: PropTypes.number, headerLabelGroupHeight: PropTypes.number, headerLabelHeight: PropTypes.number, itemHeightRatio: PropTypes.number, minimumWidthForItemContentVisibility: PropTypes.number, minGroupHeight: PropTypes.number, minZoom: PropTypes.number, maxZoom: PropTypes.number, clickTolerance: PropTypes.number, canChangeGroup: PropTypes.bool, canMove: PropTypes.bool, canResize: PropTypes.oneOf([true, false, 'left', 'right', 'both']), useResizeHandle: PropTypes.bool, canSelect: PropTypes.bool, stackItems: PropTypes.oneOf([true, false, 'force']), traditionalZoom: PropTypes.bool, showCursorLine: PropTypes.bool, itemTouchSendsClick: PropTypes.bool, onItemMove: PropTypes.func, onItemResize: PropTypes.func, onItemClick: PropTypes.func, onItemSelect: PropTypes.func, onItemDeselect: PropTypes.func, onCanvasClick: PropTypes.func, onItemDoubleClick: PropTypes.func, onItemContextMenu: PropTypes.func, onCanvasDoubleClick: PropTypes.func, onCanvasContextMenu: PropTypes.func, onCanvasMouseEnter: PropTypes.func, onCanvasMouseLeave: PropTypes.func, onCanvasMouseMove: PropTypes.func, onZoom: PropTypes.func, moveResizeValidator: PropTypes.func, itemRenderer: PropTypes.func, groupRenderer: PropTypes.func, style: PropTypes.object, keys: PropTypes.shape({ groupIdKey: PropTypes.string, groupTitleKey: PropTypes.string, groupRightTitleKey: PropTypes.string, itemIdKey: PropTypes.string, itemTitleKey: PropTypes.string, itemDivTitleKey: PropTypes.string, itemGroupKey: PropTypes.string, itemTimeStartKey: PropTypes.string, itemTimeEndKey: PropTypes.string }), headerRef: PropTypes.func, timeSteps: PropTypes.shape({ second: PropTypes.number, minute: PropTypes.number, hour: PropTypes.number, day: PropTypes.number, month: PropTypes.number, year: PropTypes.number }), defaultTimeStart: PropTypes.object, defaultTimeEnd: PropTypes.object, visibleTimeStart: PropTypes.number, visibleTimeEnd: PropTypes.number, onTimeChange: PropTypes.func, onBoundsChange: PropTypes.func, selected: PropTypes.array, headerLabelFormats: PropTypes.shape({ yearShort: PropTypes.string, yearLong: PropTypes.string, monthShort: PropTypes.string, monthMedium: PropTypes.string, monthMediumLong: PropTypes.string, monthLong: PropTypes.string, dayShort: PropTypes.string, dayLong: PropTypes.string, hourShort: PropTypes.string, hourMedium: PropTypes.string, hourMediumLong: PropTypes.string, hourLong: PropTypes.string }), subHeaderLabelFormats: PropTypes.shape({ yearShort: PropTypes.string, yearLong: PropTypes.string, monthShort: PropTypes.string, monthMedium: PropTypes.string, monthLong: PropTypes.string, dayShort: PropTypes.string, dayMedium: PropTypes.string, dayMediumLong: PropTypes.string, dayLong: PropTypes.string, hourShort: PropTypes.string, hourLong: PropTypes.string, minuteShort: PropTypes.string, minuteLong: PropTypes.string }), resizeDetector: PropTypes.shape({ addListener: PropTypes.func, removeListener: PropTypes.func }), children: PropTypes.node } static defaultProps = { sidebarWidth: 150, rightSidebarWidth: 0, dragSnap: 1000 * 60 * 15, // 15min minResizeWidth: 20, stickyOffset: 0, stickyHeader: true, lineHeight: 30, headerLabelGroupHeight: 30, headerLabelHeight: 30, itemHeightRatio: 0.65, minimumWidthForItemContentVisibility: 25, minZoom: 60 * 60 * 1000, // 1 hour maxZoom: 5 * 365.24 * 86400 * 1000, // 5 years clickTolerance: 3, // how many pixels can we drag for it to be still considered a click? canChangeGroup: true, canMove: true, canResize: 'right', useResizeHandle: false, canSelect: true, stackItems: null, traditionalZoom: false, showCursorLine: false, onItemMove: null, onItemResize: null, onItemClick: null, onItemSelect: null, onItemDeselect: null, onCanvasClick: null, onItemDoubleClick: null, onItemContextMenu: null, onCanvasMouseEnter: null, onCanvasMouseLeave: null, onCanvasMouseMove: null, onZoom: null, moveResizeValidator: null, dayBackground: null, defaultTimeStart: null, defaultTimeEnd: null, itemTouchSendsClick: false, style: {}, keys: defaultKeys, timeSteps: defaultTimeSteps, headerRef: () => {}, // if you pass in visibleTimeStart and visibleTimeEnd, you must also pass onTimeChange(visibleTimeStart, visibleTimeEnd), // which needs to update the props visibleTimeStart and visibleTimeEnd to the ones passed visibleTimeStart: null, visibleTimeEnd: null, onTimeChange: function( visibleTimeStart, visibleTimeEnd, updateScrollCanvas ) { updateScrollCanvas(visibleTimeStart, visibleTimeEnd) }, // called when the canvas area of the calendar changes onBoundsChange: null, children: null, headerLabelFormats: defaultHeaderLabelFormats, subHeaderLabelFormats: defaultSubHeaderLabelFormats, selected: null } static childContextTypes = { getTimelineContext: PropTypes.func } getChildContext() { return { getTimelineContext: () => { return this.getTimelineContext() } } } getTimelineContext = () => { const { width, visibleTimeStart, visibleTimeEnd, canvasTimeStart } = this.state const zoom = visibleTimeEnd - visibleTimeStart const canvasTimeEnd = canvasTimeStart + zoom * 3 //TODO: Performance //prob wanna memoize this so we ensure that if no items changed, //we return same context reference return { timelineWidth: width, visibleTimeStart, visibleTimeEnd, canvasTimeStart, canvasTimeEnd } } constructor(props) { super(props) let visibleTimeStart = null let visibleTimeEnd = null if (this.props.defaultTimeStart && this.props.defaultTimeEnd) { visibleTimeStart = this.props.defaultTimeStart.valueOf() visibleTimeEnd = this.props.defaultTimeEnd.valueOf() } else if (this.props.visibleTimeStart && this.props.visibleTimeEnd) { visibleTimeStart = this.props.visibleTimeStart visibleTimeEnd = this.props.visibleTimeEnd } else { //throwing an error because neither default or visible time props provided throw new Error( 'You must provide either "defaultTimeStart" and "defaultTimeEnd" or "visibleTimeStart" and "visibleTimeEnd" to initialize the Timeline' ) } this.state = { width: 1000, visibleTimeStart: visibleTimeStart, visibleTimeEnd: visibleTimeEnd, canvasTimeStart: visibleTimeStart - (visibleTimeEnd - visibleTimeStart), selectedItem: null, dragTime: null, dragGroupTitle: null, resizeTime: null, topOffset: 0, resizingItem: null, resizingEdge: null } const { dimensionItems, height, groupHeights, groupTops } = this.stackItems( props.items, props.groups, this.state.canvasTimeStart, this.state.visibleTimeStart, this.state.visibleTimeEnd, this.state.width ) /* eslint-disable react/no-direct-mutation-state */ this.state.dimensionItems = dimensionItems this.state.height = height this.state.groupHeights = groupHeights this.state.groupTops = groupTops /* eslint-enable */ } componentDidMount() { this.resize(this.props) if (this.props.resizeDetector && this.props.resizeDetector.addListener) { this.props.resizeDetector.addListener(this) } windowResizeDetector.addListener(this) this.lastTouchDistance = null } componentWillUnmount() { if (this.props.resizeDetector && this.props.resizeDetector.addListener) { this.props.resizeDetector.removeListener(this) } windowResizeDetector.removeListener(this) } resize = (props = this.props) => { const { width: containerWidth, top: containerTop } = this.container.getBoundingClientRect() let width = containerWidth - props.sidebarWidth - props.rightSidebarWidth const { headerLabelGroupHeight, headerLabelHeight } = props const headerHeight = headerLabelGroupHeight + headerLabelHeight const { dimensionItems, height, groupHeights, groupTops } = this.stackItems( props.items, props.groups, this.state.canvasTimeStart, this.state.visibleTimeStart, this.state.visibleTimeEnd, width ) // this is needed by dragItem since it uses pageY from the drag events // if this was in the context of the scrollElement, this would not be necessary const topOffset = containerTop + window.pageYOffset + headerHeight this.setState({ width, topOffset, dimensionItems, height, groupHeights, groupTops }) this.scrollComponent.scrollLeft = width } onScroll = scrollX => { const canvasTimeStart = this.state.canvasTimeStart const zoom = this.state.visibleTimeEnd - this.state.visibleTimeStart const width = this.state.width const visibleTimeStart = canvasTimeStart + zoom * scrollX / width if (scrollX < this.state.width * 0.5) { this.setState({ canvasTimeStart: this.state.canvasTimeStart - zoom }) } if (scrollX > this.state.width * 1.5) { this.setState({ canvasTimeStart: this.state.canvasTimeStart + zoom }) } if ( this.state.visibleTimeStart !== visibleTimeStart || this.state.visibleTimeEnd !== visibleTimeStart + zoom ) { this.props.onTimeChange( visibleTimeStart, visibleTimeStart + zoom, this.updateScrollCanvas ) } this.setState({ currentScrollLeft: scrollX }) } componentWillReceiveProps(nextProps) { const { visibleTimeStart, visibleTimeEnd, items, groups, sidebarWidth } = nextProps if (visibleTimeStart && visibleTimeEnd) { this.updateScrollCanvas( visibleTimeStart, visibleTimeEnd, items !== this.props.items || groups !== this.props.groups, items, groups ) } else if (items !== this.props.items || groups !== this.props.groups) { this.updateDimensions(items, groups) } // resize if the sidebar width changed if (sidebarWidth !== this.props.sidebarWidth && items && groups) { this.resize(nextProps) } } updateDimensions(items, groups) { const { canvasTimeStart, visibleTimeStart, visibleTimeEnd, width } = this.state const { dimensionItems, height, groupHeights, groupTops } = this.stackItems( items, groups, canvasTimeStart, visibleTimeStart, visibleTimeEnd, width ) this.setState({ dimensionItems, height, groupHeights, groupTops }) } // called when the visible time changes updateScrollCanvas = ( visibleTimeStart, visibleTimeEnd, forceUpdateDimensions, updatedItems, updatedGroups ) => { const oldCanvasTimeStart = this.state.canvasTimeStart const oldZoom = this.state.visibleTimeEnd - this.state.visibleTimeStart const newZoom = visibleTimeEnd - visibleTimeStart const items = updatedItems || this.props.items const groups = updatedGroups || this.props.groups let newState = { visibleTimeStart: visibleTimeStart, visibleTimeEnd: visibleTimeEnd } let resetCanvas = false const canKeepCanvas = visibleTimeStart >= oldCanvasTimeStart + oldZoom * 0.5 && visibleTimeStart <= oldCanvasTimeStart + oldZoom * 1.5 && visibleTimeEnd >= oldCanvasTimeStart + oldZoom * 1.5 && visibleTimeEnd <= oldCanvasTimeStart + oldZoom * 2.5 // if new visible time is in the right canvas area if (canKeepCanvas) { // but we need to update the scroll const newScrollLeft = Math.round( this.state.width * (visibleTimeStart - oldCanvasTimeStart) / newZoom ) if (this.scrollComponent.scrollLeft !== newScrollLeft) { resetCanvas = true } } else { resetCanvas = true } if (resetCanvas) { // Todo: need to calculate new dimensions newState.canvasTimeStart = visibleTimeStart - newZoom this.scrollComponent.scrollLeft = this.state.width if (this.props.onBoundsChange) { this.props.onBoundsChange( newState.canvasTimeStart, newState.canvasTimeStart + newZoom * 3 ) } } if (resetCanvas || forceUpdateDimensions) { const canvasTimeStart = newState.canvasTimeStart ? newState.canvasTimeStart : oldCanvasTimeStart const { dimensionItems, height, groupHeights, groupTops } = this.stackItems( items, groups, canvasTimeStart, visibleTimeStart, visibleTimeEnd, this.state.width ) newState.dimensionItems = dimensionItems newState.height = height newState.groupHeights = groupHeights newState.groupTops = groupTops } this.setState(newState, () => { // are we changing zoom? Well then let's report it // need to wait until state is set so that we get current // timeline context if (this.props.onZoom && oldZoom !== newZoom) { this.props.onZoom(this.getTimelineContext()) } }) } handleWheelZoom = (speed, xPosition, deltaY) => { this.changeZoom(1.0 + speed * deltaY / 500, xPosition / this.state.width) } changeZoom = (scale, offset = 0.5) => { const { minZoom, maxZoom } = this.props const oldZoom = this.state.visibleTimeEnd - this.state.visibleTimeStart const newZoom = Math.min( Math.max(Math.round(oldZoom * scale), minZoom), maxZoom ) // min 1 min, max 20 years const newVisibleTimeStart = Math.round( this.state.visibleTimeStart + (oldZoom - newZoom) * offset ) this.props.onTimeChange( newVisibleTimeStart, newVisibleTimeStart + newZoom, this.updateScrollCanvas ) } showPeriod = (from, unit) => { let visibleTimeStart = from.valueOf() let visibleTimeEnd = moment(from) .add(1, unit) .valueOf() let zoom = visibleTimeEnd - visibleTimeStart // can't zoom in more than to show one hour if (zoom < 360000) { return } // clicked on the big header and already focused here, zoom out if ( unit !== 'year' && this.state.visibleTimeStart === visibleTimeStart && this.state.visibleTimeEnd === visibleTimeEnd ) { let nextUnit = getNextUnit(unit) visibleTimeStart = from.startOf(nextUnit).valueOf() visibleTimeEnd = moment(visibleTimeStart).add(1, nextUnit) zoom = visibleTimeEnd - visibleTimeStart } this.props.onTimeChange( visibleTimeStart, visibleTimeStart + zoom, this.updateScrollCanvas ) } selectItem = (item, clickType, e) => { if ( this.state.selectedItem === item || (this.props.itemTouchSendsClick && clickType === 'touch') ) { if (item && this.props.onItemClick) { const time = this.timeFromItemEvent(e) this.props.onItemClick(item, e, time) } } else { this.setState({ selectedItem: item }) if (item && this.props.onItemSelect) { const time = this.timeFromItemEvent(e) this.props.onItemSelect(item, e, time) } else if (item === null && this.props.onItemDeselect) { this.props.onItemDeselect(e) // this isnt in the docs. Is this function even used? } } } doubleClickItem = (item, e) => { if (this.props.onItemDoubleClick) { const time = this.timeFromItemEvent(e) this.props.onItemDoubleClick(item, e, time) } } contextMenuClickItem = (item, e) => { if (this.props.onItemContextMenu) { const time = this.timeFromItemEvent(e) this.props.onItemContextMenu(item, e, time) } } // TODO: this is very similar to timeFromItemEvent, aside from which element to get offsets // from. Look to consolidate the logic for determining coordinate to time // as well as generalizing how we get time from click on the canvas getTimeFromRowClickEvent = e => { const { dragSnap } = this.props const { width, visibleTimeStart, visibleTimeEnd } = this.state // get coordinates relative to the component const parentPosition = getParentPosition(e.currentTarget) const x = e.clientX - parentPosition.x // calculate the x (time) coordinate taking the dragSnap into account let time = Math.round( visibleTimeStart + x / width * (visibleTimeEnd - visibleTimeStart) ) time = Math.floor(time / dragSnap) * dragSnap return time } timeFromItemEvent = e => { const { width, visibleTimeStart, visibleTimeEnd } = this.state const { dragSnap } = this.props const scrollComponent = this.scrollComponent const { left: scrollX } = scrollComponent.getBoundingClientRect() const xRelativeToTimeline = e.clientX - scrollX const relativeItemPosition = xRelativeToTimeline / width const zoom = visibleTimeEnd - visibleTimeStart const timeOffset = relativeItemPosition * zoom let time = Math.round(visibleTimeStart + timeOffset) time = Math.floor(time / dragSnap) * dragSnap return time } dragItem = (item, dragTime, newGroupOrder) => { let newGroup = this.props.groups[newGroupOrder] const keys = this.props.keys this.setState({ draggingItem: item, dragTime: dragTime, newGroupOrder: newGroupOrder, dragGroupTitle: newGroup ? _get(newGroup, keys.groupTitleKey) : '' }) } dropItem = (item, dragTime, newGroupOrder) => { this.setState({ draggingItem: null, dragTime: null, dragGroupTitle: null }) if (this.props.onItemMove) { this.props.onItemMove(item, dragTime, newGroupOrder) } } resizingItem = (item, resizeTime, edge) => { this.setState({ resizingItem: item, resizingEdge: edge, resizeTime: resizeTime }) } resizedItem = (item, resizeTime, edge, timeDelta) => { this.setState({ resizingItem: null, resizingEdge: null, resizeTime: null }) if (this.props.onItemResize && timeDelta !== 0) { this.props.onItemResize(item, resizeTime, edge) } } handleScrollMouseEnter = e => { const { showCursorLine } = this.props if (showCursorLine) { this.setState({ mouseOverCanvas: true }) } if (this.props.onCanvasMouseEnter) { this.props.onCanvasMouseEnter(e) } } handleScrollMouseLeave = e => { const { showCursorLine } = this.props if (showCursorLine) { this.setState({ mouseOverCanvas: false }) } if (this.props.onCanvasMouseLeave) { this.props.onCanvasMouseLeave(e) } } handleScrollMouseMove = e => { const { showCursorLine } = this.props const { canvasTimeStart, width, visibleTimeStart, visibleTimeEnd, cursorTime } = this.state const zoom = visibleTimeEnd - visibleTimeStart const canvasTimeEnd = canvasTimeStart + zoom * 3 const canvasWidth = width * 3 const { pageX } = e const ratio = (canvasTimeEnd - canvasTimeStart) / canvasWidth const boundingRect = this.scrollComponent.getBoundingClientRect() let timePosition = visibleTimeStart + ratio * (pageX - boundingRect.left) if (this.props.dragSnap) { timePosition = Math.round(timePosition / this.props.dragSnap) * this.props.dragSnap } if (this.props.onCanvasMouseMove) { this.props.onCanvasMouseMove(e) } if (cursorTime !== timePosition && showCursorLine) { this.setState({ cursorTime: timePosition, mouseOverCanvas: true }) } } todayLine(canvasTimeStart, canvasTimeEnd, canvasWidth, height) { return ( <TodayLine canvasTimeStart={canvasTimeStart} canvasTimeEnd={canvasTimeEnd} canvasWidth={canvasWidth} height={height} /> ) } cursorLine(cursorTime, canvasTimeStart, canvasTimeEnd, canvasWidth, height) { return ( <CursorLine cursorTime={cursorTime} canvasTimeStart={canvasTimeStart} canvasTimeEnd={canvasTimeEnd} canvasWidth={canvasWidth} height={height} /> ) } verticalLines( canvasTimeStart, canvasTimeEnd, canvasWidth, minUnit, timeSteps, height ) { return ( <VerticalLines canvasTimeStart={canvasTimeStart} canvasTimeEnd={canvasTimeEnd} canvasWidth={canvasWidth} lineCount={_length(this.props.groups)} minUnit={minUnit} timeSteps={timeSteps} height={height} /> ) } handleRowClick = (e, rowIndex) => { // shouldnt this be handled by the user, as far as when to deselect an item? if (this.state.selectedItem) { this.selectItem(null) } if (this.props.onCanvasClick == null) return const time = this.getTimeFromRowClickEvent(e) const groupId = _get( this.props.groups[rowIndex], this.props.keys.groupIdKey ) this.props.onCanvasClick(groupId, time, e) } handleRowDoubleClick = (e, rowIndex) => { if (this.props.onCanvasDoubleClick == null) return const time = this.getTimeFromRowClickEvent(e) const groupId = _get( this.props.groups[rowIndex], this.props.keys.groupIdKey ) this.props.onCanvasDoubleClick(groupId, time, e) } horizontalLines(canvasWidth, groupHeights) { return ( <GroupRows canvasWidth={canvasWidth} lineCount={_length(this.props.groups)} groupHeights={groupHeights} clickTolerance={this.props.clickTolerance} onRowClick={this.handleRowClick} onRowDoubleClick={this.handleRowDoubleClick} /> ) } items( canvasTimeStart, zoom, canvasTimeEnd, canvasWidth, minUnit, dimensionItems, groupHeights, groupTops ) { return ( <Items canvasTimeStart={canvasTimeStart} canvasTimeEnd={canvasTimeEnd} canvasWidth={canvasWidth} lineCount={_length(this.props.groups)} dimensionItems={dimensionItems} minUnit={minUnit} groupHeights={groupHeights} groupTops={groupTops} items={this.props.items} groups={this.props.groups} keys={this.props.keys} selectedItem={this.state.selectedItem} dragSnap={this.props.dragSnap} minResizeWidth={this.props.minResizeWidth} canChangeGroup={this.props.canChangeGroup} canMove={this.props.canMove} canResize={this.props.canResize} useResizeHandle={this.props.useResizeHandle} canSelect={this.props.canSelect} moveResizeValidator={this.props.moveResizeValidator} topOffset={this.state.topOffset} itemSelect={this.selectItem} itemDrag={this.dragItem} itemDrop={this.dropItem} onItemDoubleClick={this.doubleClickItem} onItemContextMenu={this.contextMenuClickItem} itemResizing={this.resizingItem} itemResized={this.resizedItem} itemRenderer={this.props.itemRenderer} selected={this.props.selected} minimumWidthForItemContentVisibility={ this.props.minimumWidthForItemContentVisibility } /> ) } infoLabel() { let label = null if (this.state.dragTime) { label = `${moment(this.state.dragTime).format('LLL')}, ${ this.state.dragGroupTitle }` } else if (this.state.resizeTime) { label = moment(this.state.resizeTime).format('LLL') } return label ? <InfoLabel label={label} /> : '' } header( canvasTimeStart, zoom, canvasTimeEnd, canvasWidth, minUnit, timeSteps, headerLabelGroupHeight, headerLabelHeight ) { const { sidebarWidth, rightSidebarWidth } = this.props const leftSidebar = sidebarWidth != null && sidebarWidth > 0 && ( <div className="rct-sidebar-header" style={{ width: this.props.sidebarWidth }} > {this.props.sidebarContent} </div> ) const rightSidebar = rightSidebarWidth != null && rightSidebarWidth > 0 && ( <div className="rct-sidebar-header rct-sidebar-right" style={{ width: this.props.rightSidebarWidth }} > {this.props.rightSidebarContent} </div> ) return ( <Header canvasTimeStart={canvasTimeStart} hasRightSidebar={this.props.rightSidebarWidth > 0} canvasTimeEnd={canvasTimeEnd} canvasWidth={canvasWidth} minUnit={minUnit} timeSteps={timeSteps} headerLabelGroupHeight={headerLabelGroupHeight} headerLabelHeight={headerLabelHeight} width={this.state.width} zoom={zoom} visibleTimeStart={this.state.visibleTimeStart} visibleTimeEnd={this.state.visibleTimeEnd} stickyOffset={this.props.stickyOffset} stickyHeader={this.props.stickyHeader} showPeriod={this.showPeriod} headerLabelFormats={this.props.headerLabelFormats} subHeaderLabelFormats={this.props.subHeaderLabelFormats} registerScroll={this.registerScrollListener} leftSidebarHeader={leftSidebar} rightSidebarHeader={rightSidebar} headerRef={this.props.headerRef} /> ) } componentDidUpdate() { this.headerScrollListener(this.state.currentScrollLeft) } registerScrollListener = listener => { this.headerScrollListener = listener } sidebar(height, groupHeights) { const { sidebarWidth } = this.props return ( sidebarWidth != null && sidebarWidth > 0 && ( <Sidebar groups={this.props.groups} groupRenderer={this.props.groupRenderer} keys={this.props.keys} width={this.props.sidebarWidth} groupHeights={groupHeights} height={height} /> ) ) } rightSidebar(height, groupHeights) { const { rightSidebarWidth } = this.props return ( rightSidebarWidth != null && rightSidebarWidth > 0 && ( <Sidebar groups={this.props.groups} keys={this.props.keys} isRightSidebar width={this.props.rightSidebarWidth} groupHeights={groupHeights} height={height} /> ) ) } stackItems( items, groups, canvasTimeStart, visibleTimeStart, visibleTimeEnd, width ) { // if there are no groups return an empty array of dimensions if (groups.length === 0) { return { dimensionItems: [], height: 0, groupHeights: [], groupTops: [] } } const { keys, lineHeight, headerLabelGroupHeight, headerLabelHeight, stackItems, itemHeightRatio } = this.props const { draggingItem, dragTime, resizingItem, resizingEdge, resizeTime, newGroupOrder } = this.state const zoom = visibleTimeEnd - visibleTimeStart const canvasTimeEnd = canvasTimeStart + zoom * 3 const canvasWidth = width * 3 const headerHeight = headerLabelGroupHeight + headerLabelHeight const visibleItems = getVisibleItems( items, canvasTimeStart, canvasTimeEnd, keys ) const groupOrders = getGroupOrders(groups, keys) let dimensionItems = visibleItems.reduce((memo, item) => { const itemId = _get(item, keys.itemIdKey) const isDragging = itemId === draggingItem const isResizing = itemId === resizingItem let dimension = calculateDimensions({ itemTimeStart: _get(item, keys.itemTimeStartKey), itemTimeEnd: _get(item, keys.itemTimeEndKey), isDragging, isResizing, canvasTimeStart, canvasTimeEnd, canvasWidth, dragTime, resizingEdge, resizeTime }) if (dimension) { dimension.top = null dimension.order = isDragging ? newGroupOrder : groupOrders[_get(item, keys.itemGroupKey)] dimension.stack = !item.isOverlay dimension.height = lineHeight * itemHeightRatio dimension.isDragging = isDragging memo.push({ id: itemId, dimensions: dimension }) } return memo }, []) const stackingMethod = !stackItems ? nostack : (stackItems === 'force' ? forcestack : stack); const { height, groupHeights, groupTops } = stackingMethod( dimensionItems, groupOrders, lineHeight, headerHeight, this.props.minGroupHeight ) return { dimensionItems, height, groupHeights, groupTops } } handleScrollContextMenu = e => { const { canvasTimeStart, width, visibleTimeStart, visibleTimeEnd, groupTops, topOffset } = this.state const zoom = visibleTimeEnd - visibleTimeStart const canvasTimeEnd = canvasTimeStart + zoom * 3 const canvasWidth = width * 3 const { pageX, pageY } = e const ratio = (canvasTimeEnd - canvasTimeStart) / canvasWidth const boundingRect = this.scrollComponent.getBoundingClientRect() let timePosition = visibleTimeStart + ratio * (pageX - boundingRect.left) if (this.props.dragSnap) { timePosition = Math.round(timePosition / this.props.dragSnap) * this.props.dragSnap } let groupIndex = 0 for (var key of Object.keys(groupTops)) { var item = groupTops[key] if (pageY - topOffset > item) { groupIndex = parseInt(key, 10) } else { break } } if (this.props.onCanvasContextMenu) { e.preventDefault() this.props.onCanvasContextMenu( this.props.groups[groupIndex], timePosition, e ) } } childrenWithProps( canvasTimeStart, canvasTimeEnd, canvasWidth, dimensionItems, groupHeights, groupTops, height, headerHeight, visibleTimeStart, visibleTimeEnd, minUnit, timeSteps ) { if (!this.props.children) { return null } // convert to an array and remove the nulls const childArray = Array.isArray(this.props.children) ? this.props.children.filter(c => c) : [this.props.children] const childProps = { canvasTimeStart, canvasTimeEnd, canvasWidth, visibleTimeStart: visibleTimeStart, visibleTimeEnd: visibleTimeEnd, dimensionItems, items: this.props.items, groups: this.props.groups, keys: this.props.keys, // TODO: combine these two groupHeights: groupHeights, groupTops: groupTops, selected: this.state.selectedItem && !this.props.selected ? [this.state.selectedItem] : this.props.selected || [], height: height, headerHeight: headerHeight, minUnit: minUnit, timeSteps: timeSteps } return React.Children.map(childArray, child => React.cloneElement(child, childProps) ) } render() { const { items, groups, headerLabelGroupHeight, headerLabelHeight, sidebarWidth, rightSidebarWidth, timeSteps, showCursorLine, traditionalZoom } = this.props const { draggingItem, resizingItem, width, visibleTimeStart, visibleTimeEnd, canvasTimeStart, mouseOverCanvas, cursorTime } = this.state let { dimensionItems, height, groupHeights, groupTops } = this.state const zoom = visibleTimeEnd - visibleTimeStart const canvasTimeEnd = canvasTimeStart + zoom * 3 const canvasWidth = width * 3 const minUnit = getMinUnit(zoom, width, timeSteps) const headerHeight = headerLabelGroupHeight + headerLabelHeight const isInteractingWithItem = !!draggingItem || !!resizingItem if (isInteractingWithItem) { const stackResults = this.stackItems( items, groups, canvasTimeStart, visibleTimeStart, visibleTimeEnd, width ) dimensionItems = stackResults.dimensionItems height = stackResults.height groupHeights = stackResults.groupHeights groupTops = stackResults.groupTops } const outerComponentStyle = { height: `${height}px` } const canvasComponentStyle = { width: `${canvasWidth}px`, height: `${height}px` } return ( <div style={this.props.style} ref={el => (this.container = el)} className="react-calendar-timeline" > {this.header( canvasTimeStart, zoom, canvasTimeEnd, canvasWidth, minUnit, timeSteps, headerLabelGroupHeight, headerLabelHeight )} <div style={outerComponentStyle} className="rct-outer"> {sidebarWidth > 0 ? this.sidebar(height, groupHeights) : null} <ScrollElement scrollRef={el => (this.scrollComponent = el)} width={width} height={height} onZoom={this.changeZoom} onWheelZoom={this.handleWheelZoom} traditionalZoom={traditionalZoom} onScroll={this.onScroll} isInteractingWithItem={isInteractingWithItem} onMouseEnter={this.handleScrollMouseEnter} onMouseLeave={this.handleScrollMouseLeave} onMouseMove={this.handleScrollMouseMove} onContextMenu={this.handleScrollContextMenu} > <div ref={el => (this.canvasComponent = el)} className="rct-canvas" style={canvasComponentStyle} > {this.items( canvasTimeStart, zoom, canvasTimeEnd, canvasWidth, minUnit, dimensionItems, groupHeights, groupTops )} {this.verticalLines( canvasTimeStart, canvasTimeEnd, canvasWidth, minUnit, timeSteps, height, headerHeight )} {this.horizontalLines(canvasWidth, groupHeights)} {this.todayLine( canvasTimeStart, canvasTimeEnd, canvasWidth, height )} {mouseOverCanvas && showCursorLine ? this.cursorLine( cursorTime, canvasTimeStart, canvasTimeEnd, canvasWidth, height ) : null} {this.infoLabel()} {this.childrenWithProps( canvasTimeStart, canvasTimeEnd, canvasWidth, dimensionItems, groupHeights, groupTops, height, headerHeight, visibleTimeStart, visibleTimeEnd, minUnit, timeSteps )} </div> </ScrollElement> {rightSidebarWidth > 0 ? this.rightSidebar(height, groupHeights) : null} </div> </div> ) } }