diff --git a/README.md b/README.md index daa482dbb..5cb05d841 100644 --- a/README.md +++ b/README.md @@ -316,7 +316,7 @@ Called when the canvas is clicked by the right button of the mouse. Note: If thi Called when the timeline is zoomed, either via mouse/pinch zoom or clicking header to change timeline units -## moveResizeValidator(action, itemId, time, resizeEdge) +## moveResizeValidator(action, itemId, time, resizeEdge, dragOrderIndex?, dragGroupDelta?) This function is called when an item is being moved or resized. It's up to this function to return a new version of `change`, when the proposed move would violate business logic. @@ -326,8 +326,14 @@ The argument `resizeEdge` is when resizing one of `left` or `right`. The argument `time` describes the proposed new time for either the start time of the item (for move) or the start or end time (for resize). +The argument `dragOrderIndex` is group index item belonged to before dragging. [optional, exists only in dragging/move] + +The argument `dragGroupDelta` is group index difference we should move item to on/after dragging. [optional, exists only in dragging/move] + The function must return a new unix timestamp in milliseconds... or just `time` if the proposed new time doesn't interfere with business logic. +Optionally in 'move' action you can return an object with `dragTime` timestamp in ms and `dragGroupDelta` final group index shift (integer number) + For example, to prevent moving of items into the past, but to keep them at 15min intervals, use this code: ```js @@ -435,8 +441,8 @@ Rather than applying props on the element yourself and to avoid your props being * onTouchEnd: event handler * onDoubleClick: event handler * onContextMenu: event handler - * style: inline object - + * style: inline object + \*\* _the given styles will only override the styles that are not a requirement for positioning the item. Other styles like `color`, `radius` and others_ diff --git a/demo/app/demo-limit-group-movement/index.js b/demo/app/demo-limit-group-movement/index.js new file mode 100644 index 000000000..168266d4a --- /dev/null +++ b/demo/app/demo-limit-group-movement/index.js @@ -0,0 +1,212 @@ +/* eslint-disable no-console */ +import React, { Component } from 'react' +import moment from 'moment' + +import Timeline, { + TimelineMarkers, + TimelineHeaders, + TodayMarker, + CustomMarker, + CursorMarker, + CustomHeader, + SidebarHeader, + DateHeader +} from 'react-calendar-timeline' + +import generateFakeData from '../generate-fake-data' + +var minTime = moment() + .add(-6, 'months') + .valueOf() +var maxTime = moment() + .add(6, 'months') + .valueOf() + +var keys = { + groupIdKey: 'id', + groupTitleKey: 'title', + groupRightTitleKey: 'rightTitle', + itemIdKey: 'id', + itemTitleKey: 'title', + itemDivTitleKey: 'title', + itemGroupKey: 'group', + itemTimeStartKey: 'start', + itemTimeEndKey: 'end' +} + +export default class App extends Component { + constructor(props) { + super(props) + + const { groups, items } = generateFakeData() + const defaultTimeStart = moment() + .startOf('day') + .toDate() + const defaultTimeEnd = moment() + .startOf('day') + .add(1, 'day') + .toDate() + + this.state = { + groups, + items, + defaultTimeStart, + defaultTimeEnd + } + } + + handleCanvasClick = (groupId, time) => { + console.log('Canvas clicked', groupId, moment(time).format()) + } + + handleCanvasDoubleClick = (groupId, time) => { + console.log('Canvas double clicked', groupId, moment(time).format()) + } + + handleCanvasContextMenu = (group, time) => { + console.log('Canvas context menu', group, moment(time).format()) + } + + handleItemClick = (itemId, _, time) => { + console.log('Clicked: ' + itemId, moment(time).format()) + } + + handleItemSelect = (itemId, _, time) => { + console.log('Selected: ' + itemId, moment(time).format()) + } + + handleItemDoubleClick = (itemId, _, time) => { + console.log('Double Click: ' + itemId, moment(time).format()) + } + + handleItemContextMenu = (itemId, _, time) => { + console.log('Context Menu: ' + itemId, moment(time).format()) + } + + handleItemMove = (itemId, dragTime, newGroupOrder) => { + const { items, groups } = this.state + + const group = groups[newGroupOrder] + + this.setState({ + items: items.map( + item => + item.id === itemId + ? Object.assign({}, item, { + start: dragTime, + end: dragTime + (item.end - item.start), + group: group.id + }) + : item + ) + }) + + console.log('Moved', itemId, dragTime, newGroupOrder) + } + + handleItemResize = (itemId, time, edge) => { + const { items } = this.state + + this.setState({ + items: items.map( + item => + item.id === itemId + ? Object.assign({}, item, { + start: edge === 'left' ? time : item.start, + end: edge === 'left' ? item.end : time + }) + : item + ) + }) + + console.log('Resized', itemId, time, edge) + } + + // this limits the timeline to -6 months ... +6 months + handleTimeChange = (visibleTimeStart, visibleTimeEnd, updateScrollCanvas) => { + if (visibleTimeStart < minTime && visibleTimeEnd > maxTime) { + updateScrollCanvas(minTime, maxTime) + } else if (visibleTimeStart < minTime) { + updateScrollCanvas(minTime, minTime + (visibleTimeEnd - visibleTimeStart)) + } else if (visibleTimeEnd > maxTime) { + updateScrollCanvas(maxTime - (visibleTimeEnd - visibleTimeStart), maxTime) + } else { + updateScrollCanvas(visibleTimeStart, visibleTimeEnd) + } + } + + moveResizeValidator = (action, item, time, resizeEdge, index, delta) => { + let newTime = time; + if (time < new Date().getTime()) { + newTime = + Math.ceil(new Date().getTime() / (15 * 60 * 1000)) * (15 * 60 * 1000) + } + + // move items only in checker style + // (odd rows to odd rows only and even rows to even rows) + + if ((index + delta) % 2 !== index % 2) { + const moveDelta = (index + delta >= this.state.groups.length - 1) ? -1 : 1 + return {dragTime: newTime, dragGroupDelta: delta + moveDelta}; + } + + return newTime + } + + render() { + const { groups, items, defaultTimeStart, defaultTimeEnd } = this.state + + return ( + Above The Left} + canMove + canResize="right" + canSelect + itemsSorted + itemTouchSendsClick={false} + stackItems + itemHeightRatio={0.75} + defaultTimeStart={defaultTimeStart} + defaultTimeEnd={defaultTimeEnd} + onCanvasClick={this.handleCanvasClick} + onCanvasDoubleClick={this.handleCanvasDoubleClick} + onCanvasContextMenu={this.handleCanvasContextMenu} + onItemClick={this.handleItemClick} + onItemSelect={this.handleItemSelect} + onItemContextMenu={this.handleItemContextMenu} + onItemMove={this.handleItemMove} + onItemResize={this.handleItemResize} + onItemDoubleClick={this.handleItemDoubleClick} + onTimeChange={this.handleTimeChange} + moveResizeValidator={this.moveResizeValidator} + > + + + + + {({ styles }) => { + const newStyles = { ...styles, backgroundColor: 'blue' } + return
+ }} + + + + + ) + } +} diff --git a/demo/app/index.js b/demo/app/index.js index 5dcf3b1b3..5cb6b0804 100644 --- a/demo/app/index.js +++ b/demo/app/index.js @@ -16,7 +16,8 @@ const demos = { customItems: require('./demo-custom-items').default, customHeaders: require('./demo-headers').default, customInfoLabel: require('./demo-custom-info-label').default, - controledSelect: require('./demo-controlled-select').default + controledSelect: require('./demo-controlled-select').default, + limitGroupMovement: require('./demo-limit-group-movement').default } // A simple component that shows the pathname of the current location diff --git a/src/lib/items/Item.js b/src/lib/items/Item.js index 2099a1a1a..0750c3625 100644 --- a/src/lib/items/Item.js +++ b/src/lib/items/Item.js @@ -104,7 +104,7 @@ export default class Item extends Component { nextProps.canvasTimeStart !== this.props.canvasTimeStart || nextProps.canvasTimeEnd !== this.props.canvasTimeEnd || nextProps.canvasWidth !== this.props.canvasWidth || - (nextProps.order ? nextProps.order.index : undefined) !== + (nextProps.order ? nextProps.order.index : undefined) !== (this.props.order ? this.props.order.index : undefined) || nextProps.dragSnap !== this.props.dragSnap || nextProps.minResizeWidth !== this.props.minResizeWidth || @@ -167,7 +167,7 @@ export default class Item extends Component { const offset = getSumOffset(this.props.scrollRef).offsetLeft const scrolls = getSumScroll(this.props.scrollRef) - + return (e.pageX - offset + scrolls.scrollLeft) * ratio + this.props.canvasTimeStart; } @@ -181,7 +181,7 @@ export default class Item extends Component { const offset = getSumOffset(this.props.scrollRef).offsetTop const scrolls = getSumScroll(this.props.scrollRef) - + for (var key of Object.keys(groupTops)) { var groupTop = groupTops[key] if (e.pageY - offset + scrolls.scrollTop > groupTop) { @@ -221,6 +221,37 @@ export default class Item extends Component { } } + validateDragState(e) { + const newState = { + dragTime: this.dragTime(e), + dragGroupDelta: this.dragGroupDelta(e), + } + + if (this.props.moveResizeValidator) { + const validationResult = this.props.moveResizeValidator( + 'move', + this.props.item, + newState.dragTime, + null, + this.props.order.index, + newState.dragGroupDelta + ) + + if (typeof validationResult === 'number') { + // for backward compatibility + newState.dragTime = validationResult + } else if (typeof validationResult === 'object') { + // for change in dragGroupDelta (we need to block groupDelta change) + const { dragTime, dragGroupDelta } = validationResult + if (typeof dragTime === 'number') newState.dragTime = dragTime + if (typeof dragGroupDelta === 'number') + newState.dragGroupDelta = dragGroupDelta + } + } + + return newState + } + mountInteract() { const leftResize = this.props.useResizeHandle ? ".rct-item-handler-resize-left" : true const rightResize = this.props.useResizeHandle ? ".rct-item-handler-resize-right" : true @@ -245,7 +276,7 @@ export default class Item extends Component { const clickTime = this.timeFor(e); this.setState({ dragging: true, - dragStart: { + dragStart: { x: e.pageX, y: e.pageY, offset: this.itemTimeStart - clickTime }, @@ -259,15 +290,7 @@ export default class Item extends Component { }) .on('dragmove', e => { if (this.state.dragging) { - let dragTime = this.dragTime(e) - let dragGroupDelta = this.dragGroupDelta(e) - if (this.props.moveResizeValidator) { - dragTime = this.props.moveResizeValidator( - 'move', - this.props.item, - dragTime - ) - } + const { dragTime, dragGroupDelta } = this.validateDragState(e); if (this.props.onDrag) { this.props.onDrag( @@ -286,20 +309,12 @@ export default class Item extends Component { .on('dragend', e => { if (this.state.dragging) { if (this.props.onDrop) { - let dragTime = this.dragTime(e) - - if (this.props.moveResizeValidator) { - dragTime = this.props.moveResizeValidator( - 'move', - this.props.item, - dragTime - ) - } + const { dragTime, dragGroupDelta } = this.validateDragState(e); this.props.onDrop( this.itemId, dragTime, - this.props.order.index + this.dragGroupDelta(e) + this.props.order.index + dragGroupDelta ) } @@ -425,7 +440,7 @@ export default class Item extends Component { const willBeAbleToResizeRight = this.props.selected && this.canResizeRight(this.props) - if(!!this.item){ + if(this.item) { if (this.props.selected && !interactMounted) { this.mountInteract() interactMounted = true @@ -437,7 +452,7 @@ export default class Item extends Component { ) { const leftResize = this.props.useResizeHandle ? this.dragLeft : true const rightResize = this.props.useResizeHandle ? this.dragRight : true - + interact(this.item).resizable({ enabled: willBeAbleToResizeLeft || willBeAbleToResizeRight, edges: {