UNPKG

@babylimon/react-calendar-timeline

Version:
575 lines (507 loc) 18.7 kB
import { Component } from 'react' import PropTypes from 'prop-types' import interact from 'interactjs' import moment from 'moment' import { _get, deepObjectCompare } from '../utility/generic' import { composeEvents } from '../utility/events' import { defaultItemRenderer } from './defaultItemRenderer' import { coordinateToTimeRatio } from '../utility/calendar' import { getSumScroll, getSumOffset } from '../utility/dom-helpers' import { overridableStyles, selectedStyle, selectedAndCanMove, selectedAndCanResizeLeft, selectedAndCanResizeLeftAndDragLeft, selectedAndCanResizeRight, selectedAndCanResizeRightAndDragRight, leftResizeStyle, rightResizeStyle } from './styles' export default class Item extends Component { // removed prop type check for SPEED! // they are coming from a trusted component anyway // (this complicates performance debugging otherwise) static propTypes = { canvasTimeStart: PropTypes.number.isRequired, canvasTimeEnd: PropTypes.number.isRequired, canvasWidth: PropTypes.number.isRequired, visibleTimeEnd: PropTypes.number.isRequired, visibleTimeStart: PropTypes.number.isRequired, timelineWidth: PropTypes.number.isRequired, order: PropTypes.object, dragSnap: PropTypes.number, minResizeWidth: PropTypes.number, selected: PropTypes.bool, canChangeGroup: PropTypes.bool.isRequired, canMove: PropTypes.bool.isRequired, canResizeLeft: PropTypes.bool.isRequired, canResizeRight: PropTypes.bool.isRequired, keys: PropTypes.object.isRequired, item: PropTypes.object.isRequired, onSelect: PropTypes.func, onDrag: PropTypes.func, onDrop: PropTypes.func, onResizing: PropTypes.func, onResized: PropTypes.func, onContextMenu: PropTypes.func, itemRenderer: PropTypes.func, itemProps: PropTypes.object, canSelect: PropTypes.bool, dimensions: PropTypes.object, useResizeHandle: PropTypes.bool, moveResizeValidator: PropTypes.func, onItemDoubleClick: PropTypes.func, scrollRef: PropTypes.object, dragging: PropTypes.bool.isRequired, resizing: PropTypes.bool.isRequired, dragOffset: PropTypes.number.isRequired, resizeEdge: PropTypes.oneOf(['left', 'right']), resizeStart: PropTypes.number, onDragStart: PropTypes.func.isRequired, onDragEnd : PropTypes.func.isRequired, onResizeStart: PropTypes.func.isRequired, } static defaultProps = { selected: false, itemRenderer: defaultItemRenderer } constructor(props) { super(props) this.cacheDataFromProps(props) } interactMounted = false; shouldComponentUpdate(nextProps, nextState) { var shouldUpdate = nextProps.dragging !== this.props.dragging || nextProps.resizing !== this.props.resizing || nextProps.keys !== this.props.keys || !deepObjectCompare(nextProps.itemProps, this.props.itemProps) || nextProps.selected !== this.props.selected || nextProps.item !== this.props.item || nextProps.canvasTimeStart !== this.props.canvasTimeStart || nextProps.canvasTimeEnd !== this.props.canvasTimeEnd || nextProps.canvasWidth !== this.props.canvasWidth || (nextProps.order ? nextProps.order.index : undefined) !== (this.props.order ? this.props.order.index : undefined) || nextProps.dragSnap !== this.props.dragSnap || nextProps.minResizeWidth !== this.props.minResizeWidth || nextProps.canChangeGroup !== this.props.canChangeGroup || nextProps.canSelect !== this.props.canSelect || nextProps.canMove !== this.props.canMove || nextProps.canResizeLeft !== this.props.canResizeLeft || nextProps.canResizeRight !== this.props.canResizeRight || nextProps.dimensions !== this.props.dimensions return shouldUpdate } cacheDataFromProps(props) { this.itemId = _get(props.item, props.keys.itemIdKey) this.itemTitle = _get(props.item, props.keys.itemTitleKey) this.itemDivTitle = props.keys.itemDivTitleKey ? _get(props.item, props.keys.itemDivTitleKey) : this.itemTitle this.itemTimeStart = _get(props.item, props.keys.itemTimeStartKey) this.itemTimeEnd = _get(props.item, props.keys.itemTimeEndKey) this.itemGroupId = _get(props.item, props.keys.itemGroupKey) } getTimeRatio() { const { canvasTimeStart, canvasTimeEnd, canvasWidth } = this.props return coordinateToTimeRatio(canvasTimeStart, canvasTimeEnd, canvasWidth) } dragTimeSnap(dragTime, considerOffset) { const { dragSnap } = this.props if (dragSnap) { const offset = considerOffset ? moment().utcOffset() * 60 * 1000 : 0 return Math.round(dragTime / dragSnap) * dragSnap - offset % dragSnap } else { return dragTime } } resizeTimeSnap(dragTime) { const { dragSnap } = this.props if (dragSnap) { const endTime = this.itemTimeEnd % dragSnap return Math.round((dragTime - endTime) / dragSnap) * dragSnap + endTime } else { return dragTime } } dragTime(e) { const startTime = moment(this.itemTimeStart) if (this.props.dragging) { return this.dragTimeSnap(this.timeFor(e) + this.props.dragOffset, true) } else { return startTime } } timeFor(e) { const ratio = coordinateToTimeRatio(this.props.canvasTimeStart, this.props.canvasTimeEnd, this.props.canvasWidth) const offset = getSumOffset(this.props.scrollRef).offsetLeft const scrolls = getSumScroll(this.props.scrollRef) return (e.pageX - offset + scrolls.scrollLeft) * ratio + this.props.canvasTimeStart; } resizeTimeDelta(e, resizeEdge) { const length = this.itemTimeEnd - this.itemTimeStart const timeDelta = this.dragTimeSnap( (e.pageX - this.props.resizeStart) * this.getTimeRatio() ) if ( length + (resizeEdge === 'left' ? -timeDelta : timeDelta) < (this.props.dragSnap || 1000) ) { if (resizeEdge === 'left') { return length - (this.props.dragSnap || 1000) } else { return (this.props.dragSnap || 1000) - length } } else { return timeDelta } } mountInteract() { const leftResize = this.props.useResizeHandle ? ".rct-item-handler-resize-left" : true const rightResize = this.props.useResizeHandle ? ".rct-item-handler-resize-right" : true interact(this.item) .resizable({ edges: { left: this.canResizeLeft() && leftResize, right: this.canResizeRight() && rightResize, top: false, bottom: false }, enabled: this.props.selected && (this.canResizeLeft() || this.canResizeRight()) }) .draggable({ enabled: this.props.selected && this.canMove() }) .styleCursor(false) .on('dragstart', e => { e.stopPropagation() if (this.props.selected) { const clickTime = this.timeFor(e) this.props.onDragStart(true, this.itemTimeStart - clickTime, this.itemId) } else { return false } }) .on('dragmove', e => { e.stopPropagation() if (this.props.dragging) { if (e.dropzone) { const newGroupId = e.dropzone.target.dataset.groupid let dragTime = this.dragTime(e) if(dragTime !== _get(this.props.item, this.props.keys.itemTimeStartKey)){ if (this.props.moveResizeValidator) { dragTime = this.props.moveResizeValidator('move', this.props.item, dragTime) } if (this.props.onDrag) { this.props.onDrag(this.itemId, dragTime, newGroupId) } } } } }) .on('dragend', e => { e.stopPropagation() if (this.props.dragging) { if(e.dropzone){ const newGroupId = e.dropzone.target.dataset.groupid if (this.props.onDrop) { let dragTime = this.dragTime(e) if (this.props.moveResizeValidator) { dragTime = this.props.moveResizeValidator('move', this.props.item, dragTime) } this.props.onDrop(this.itemId, dragTime, newGroupId) } } this.props.onDragEnd() } }) .on('resizestart', e => { e.stopPropagation() if (this.props.selected) { this.props.onResizeStart(true, this.itemId); } else { return false } }) .on('resizemove', e => { e.stopPropagation() if (this.props.resizing) { let resizeEdge = this.props.resizeEdge if (!resizeEdge) { resizeEdge = e.deltaRect.left !== 0 ? 'left' : 'right' } let resizeTime = this.resizeTimeSnap(this.timeFor(e)) const isResizeTimeChangedRight = resizeEdge === 'right' && resizeTime !== _get(this.props.item, this.props.keys.itemTimeEndKey) const isResizeTimeChangedLeft = resizeEdge === 'left' && resizeTime !== _get(this.props.item, this.props.keys.itemTimeStartKey) if(isResizeTimeChangedRight || isResizeTimeChangedLeft){ if (this.props.moveResizeValidator) { resizeTime = this.props.moveResizeValidator('resize', this.props.item, resizeTime, resizeEdge) } if (this.props.onResizing) { this.props.onResizing(this.itemId, resizeTime, resizeEdge) } } } }) .on('resizeend', e => { if (this.props.resizing) { const { resizeEdge } = this.props let resizeTime = this.resizeTimeSnap(this.timeFor(e)) if (this.props.moveResizeValidator) { resizeTime = this.props.moveResizeValidator('resize', this.props.item, resizeTime, resizeEdge) } if (this.props.onResized) { this.props.onResized(this.itemId, resizeTime, resizeEdge, this.resizeTimeDelta(e, resizeEdge)) } } }) .on('tap', e => { e.stopPropagation() this.actualClick(e, e.pointerType === 'mouse' ? 'click' : 'touch') }) this.interactMounted=true; } canResizeLeft(props = this.props) { if (!props.canResizeLeft) { return false } let width = parseInt(props.dimensions.width, 10) return width >= props.minResizeWidth } canResizeRight(props = this.props) { if (!props.canResizeRight) { return false } let width = parseInt(props.dimensions.width, 10) return width >= props.minResizeWidth } canMove(props = this.props) { return !!props.canMove } componentDidUpdate(prevProps) { this.cacheDataFromProps(this.props) let { interactMounted } = this const couldDrag = prevProps.selected && this.canMove(prevProps) const couldResizeLeft = prevProps.selected && this.canResizeLeft(prevProps) const couldResizeRight = prevProps.selected && this.canResizeRight(prevProps) const willBeAbleToDrag = this.props.selected && this.canMove(this.props) const willBeAbleToResizeLeft = this.props.selected && this.canResizeLeft(this.props) const willBeAbleToResizeRight = this.props.selected && this.canResizeRight(this.props) if(!!this.item){ if (this.props.selected && !interactMounted) { this.mountInteract() interactMounted = true } if ( interactMounted && (couldResizeLeft !== willBeAbleToResizeLeft || couldResizeRight !== willBeAbleToResizeRight) ) { const leftResize = this.props.useResizeHandle ? this.dragLeft : true const rightResize = this.props.useResizeHandle ? this.dragRight : true interact(this.item).resizable({ enabled: willBeAbleToResizeLeft || willBeAbleToResizeRight, edges: { top: false, bottom: false, left: willBeAbleToResizeLeft && leftResize, right: willBeAbleToResizeRight && rightResize } }) } if (interactMounted && couldDrag !== willBeAbleToDrag) { interact(this.item).draggable({ enabled: willBeAbleToDrag }) } } else{ interactMounted= false; } this.interactMounted = interactMounted } componentDidMount(){ if(!!this.item){ if (this.props.selected && !this.interactMounted) { this.mountInteract() this.interactMounted =true; } } } onMouseDown = e => { e.stopPropagation() if (!this.interactMounted) { e.preventDefault() this.startedClicking = true } } onMouseUp = e => { e.stopPropagation() if (!this.interactMounted && this.startedClicking) { this.startedClicking = false this.actualClick(e, 'click') } } onTouchStart = e => { e.stopPropagation() if (!this.interactMounted) { e.preventDefault() this.startedTouching = true } } onTouchEnd = e => { e.stopPropagation() if (!this.interactMounted && this.startedTouching) { this.startedTouching = false this.actualClick(e, 'touch') } } handleDoubleClick = e => { e.stopPropagation() if (this.props.onItemDoubleClick) { this.props.onItemDoubleClick(this.itemId, e) } } handleContextMenu = e => { e.stopPropagation() if (this.props.onContextMenu) { e.preventDefault() e.stopPropagation() this.props.onContextMenu(this.itemId, e) } } actualClick(e, clickType) { e.stopPropagation() if (this.props.canSelect && this.props.onSelect) { this.props.onSelect(this.itemId, clickType, e) } } handleOnClick = e => { e.stopPropagation() } getItemRef = el => (this.item = el) getDragLeftRef = el => (this.dragLeft = el) getDragRightRef = el => (this.dragRight = el) getItemProps = (props = {}) => { //TODO: maybe shouldnt include all of these classes const classNames = 'rct-item' + (this.props.item.className ? ` ${this.props.item.className}` : '') return { key: this.itemId, ref: this.getItemRef, title: this.itemDivTitle, className: classNames + ` ${props.className ? props.className : ''}`, onMouseDown: composeEvents(this.onMouseDown, props.onMouseDown), onMouseUp: composeEvents(this.onMouseUp, props.onMouseUp), onTouchStart: composeEvents(this.onTouchStart, props.onTouchStart), onTouchEnd: composeEvents(this.onTouchEnd, props.onTouchEnd), onDoubleClick: composeEvents(this.handleDoubleClick, props.onDoubleClick), onContextMenu: composeEvents(this.handleContextMenu, props.onContextMenu), onClick: composeEvents(this.handleOnClick, props.onClick), style: Object.assign({}, this.getItemStyle(props)) } } getResizeProps = (props = {}) => { let leftName = "rct-item-handler rct-item-handler-left rct-item-handler-resize-left" if (props.leftClassName) { leftName += ` ${props.leftClassName}` } let rightName = "rct-item-handler rct-item-handler-right rct-item-handler-resize-right" if (props.rightClassName) { rightName += ` ${props.rightClassName}` } return { left: { ref: this.getDragLeftRef, className: leftName, style: Object.assign({}, leftResizeStyle, props.leftStyle) }, right: { ref: this.getDragRightRef, className: rightName, style: Object.assign({}, rightResizeStyle, props.rightStyle) } } } getItemStyle(props) { const dimensions = this.props.dimensions const baseStyles = { position: 'absolute', boxSizing: 'border-box', left: `${dimensions.left}px`, top: `${dimensions.top}px`, width: `${dimensions.width}px`, height: `${dimensions.height}px`, lineHeight: `${dimensions.height}px` } const finalStyle = Object.assign( {}, overridableStyles, this.props.selected ? selectedStyle : {}, this.props.selected & this.canMove(this.props) ? selectedAndCanMove : {}, this.props.selected & this.canResizeLeft(this.props) ? selectedAndCanResizeLeft : {}, this.props.selected & this.canResizeLeft(this.props) & this.props.dragging ? selectedAndCanResizeLeftAndDragLeft : {}, this.props.selected & this.canResizeRight(this.props) ? selectedAndCanResizeRight : {}, this.props.selected & this.canResizeRight(this.props) & this.props.dragging ? selectedAndCanResizeRightAndDragRight : {}, props.style, baseStyles ) return finalStyle } render() { if (typeof this.props.order === 'undefined' || this.props.order === null) { return null } const timelineContext = { canvasTimeStart: this.props.canvasTimeStart, canvasTimeEnd: this.props.canvasTimeEnd, visibleTimeEnd: this.props.visibleTimeEnd, visibleTimeStart: this.props.visibleTimeStart, timelineWidth: this.props.timelineWidth, } const itemContext = { dimensions: this.props.dimensions, useResizeHandle: this.props.useResizeHandle, title: this.itemTitle, canMove: this.canMove(this.props), canResizeLeft: this.canResizeLeft(this.props), canResizeRight: this.canResizeRight(this.props), selected: this.props.selected, dragging: this.props.dragging, dragOffset: this.props.dragOffset, dragTime: this.itemTimeStart, newGroupId: this.itemGroupId, resizing: this.props.resizing, resizeEdge: this.props.resizeEdge, resizeStart: this.props.resizeStart, resizeTime: this.props.resizeEdge !== undefined ? this.props.resizeEdge === 'right'? this.itemTimeEnd : this.itemTimeStart : null , width: this.props.dimensions.width } return this.props.itemRenderer({ item: this.props.item, timelineContext, itemContext, getItemProps: this.getItemProps, getResizeProps: this.getResizeProps }) } }