Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

limit item dragging between groups (improve moveResizeValidator via dragGroupDelta) #787

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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
Expand Down Expand Up @@ -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_

Expand Down
212 changes: 212 additions & 0 deletions demo/app/demo-limit-group-movement/index.js
Original file line number Diff line number Diff line change
@@ -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 (
<Timeline
groups={groups}
items={items}
keys={keys}
sidebarWidth={150}
sidebarContent={<div>Above The Left</div>}
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}
>
<TimelineMarkers>
<TodayMarker />
<CustomMarker
date={
moment()
.startOf('day')
.valueOf() +
1000 * 60 * 60 * 2
}
/>
<CustomMarker
date={moment()
.add(3, 'day')
.valueOf()}
>
{({ styles }) => {
const newStyles = { ...styles, backgroundColor: 'blue' }
return <div style={newStyles} />
}}
</CustomMarker>
<CursorMarker />
</TimelineMarkers>
</Timeline>
)
}
}
3 changes: 2 additions & 1 deletion demo/app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
65 changes: 40 additions & 25 deletions src/lib/items/Item.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 ||
Expand Down Expand Up @@ -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;
}

Expand All @@ -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) {
Expand Down Expand Up @@ -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
Expand All @@ -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 },
Expand All @@ -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(
Expand All @@ -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
)
}

Expand Down Expand Up @@ -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
Expand All @@ -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: {
Expand Down