Skip to content
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
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres (more or less) to [Semantic Versioning](http://semver.org/).

## Unreleased

* Added onCollision event between items.
* Added item collision tests
* Added item collision demo

## 0.28.0

Expand Down
97 changes: 97 additions & 0 deletions __tests__/utils/calendar/item-collision.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import { isCollision } from "../../../src/lib/utility/generic";


describe('item collision', ()=>{

const items = [
{
id: '1',
group: '1',
start_time: 100,
end_time: 120,
canMove: false,
canResize: false,
className: ''
},
{
id: '2',
group: '1',
start_time: 110,
end_time: 120,
canMove: false,
canResize: false,
className: ''
},
{
id: '3',
group: '1',
start_time: 200,
end_time: 250,
canMove: false,
canResize: false,
className: ''
},
{
id: '4',
group: '1',
start_time: 210,
end_time: 240,
canMove: false,
canResize: false,
className: ''
},
{
id: '5',
group: '1',
start_time: 210,
end_time: 240,
canMove: false,
canResize: false,
className: ''
},
{
id: '6',
group: '1',
start_time: 115,
end_time: 130,
canMove: false,
canResize: false,
className: ''
},
];

describe('From sides', () => {

it('From left', () => {
isCollision(items, 1, (currentItem, item) => {
expect(currentItem.id).toBe(items[1].id);
expect(item.id).toBe(items[0].id);
});

isCollision(items, 2, () => {
throw new Error('Don\'t call me');
});
});

it('From right', () => {
isCollision(items, 0, (currentItem, item) => {
expect(currentItem.id).toBe(items[1].id);
expect(item.id).toBe(items[0].id);
});

isCollision(items, 2, () => {
throw new Error('Don\'t call me');
});
});
});

describe('From nested', () => {
it('From inside', () => {
isCollision(items, 3, (currentItem, item) => {
expect(currentItem.id).toBe(items[3].id);
expect(item.id).toBe(items[2].id);
});
});
});

});
212 changes: 212 additions & 0 deletions demo/app/demo-colision/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,
TodayMarker,
CustomMarker,
CursorMarker,
} 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())
}

handleItemCollision = (currentItem, item) => {
console.log('Item ' + currentItem.id + 'collided with ' + item.id)
}

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)
}
}

handleZoom = (timelineContext, unit) => {
console.log('Zoomed', timelineContext, unit)
}


moveResizeValidator = (action, item, time) => {
if (time < new Date().getTime()) {
var newTime =
Math.ceil(new Date().getTime() / (15 * 60 * 1000)) * (15 * 60 * 1000)
return newTime
}

return time
}

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}
onZoom={this.handleZoom}
onCollision={this.handleItemCollision}
moveResizeValidator={this.moveResizeValidator}
buffer={3}
>
<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>
)
}
}
1 change: 1 addition & 0 deletions demo/app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ const demos = {
customInfoLabel: require('./demo-custom-info-label').default,
controledSelect: require('./demo-controlled-select').default,
controlledScrolling: require('./demo-controlled-scrolling').default,
collision: require('./demo-colision/').default,
}

// A simple component that shows the pathname of the current location
Expand Down
14 changes: 13 additions & 1 deletion src/lib/Timeline.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
getCanvasWidth,
stackTimelineItems
} from './utility/calendar'
import { _get, _length } from './utility/generic'
import { _get, _length, isCollision } from './utility/generic'
import {
defaultKeys,
defaultTimeSteps,
Expand Down Expand Up @@ -69,6 +69,7 @@ export default class ReactCalendarTimeline extends Component {
onItemContextMenu: PropTypes.func,
onCanvasDoubleClick: PropTypes.func,
onCanvasContextMenu: PropTypes.func,
onCollision: PropTypes.func,
onZoom: PropTypes.func,
onItemDrag: PropTypes.func,

Expand Down Expand Up @@ -159,6 +160,7 @@ export default class ReactCalendarTimeline extends Component {
onCanvasClick: null,
onItemDoubleClick: null,
onItemContextMenu: null,
onCollision: null,
onZoom: null,

verticalLineClassNamesForTime: null,
Expand Down Expand Up @@ -656,6 +658,12 @@ export default class ReactCalendarTimeline extends Component {
this.setState({ draggingItem: null, dragTime: null, dragGroupTitle: null })
if (this.props.onItemMove) {
this.props.onItemMove(item, dragTime, newGroupOrder)

this.items()
}

if(this.props.onCollision) {
isCollision(this.props.items, item, this.props.onCollision);
}
}

Expand All @@ -679,6 +687,10 @@ export default class ReactCalendarTimeline extends Component {
if (this.props.onItemResize && timeDelta !== 0) {
this.props.onItemResize(item, resizeTime, edge)
}

if(this.props.onCollision) {
isCollision(this.props.items, item, this.props.onCollision);
}
}

updatingItem = ({ eventType, itemId, time, edge, newGroupOrder }) => {
Expand Down
14 changes: 14 additions & 0 deletions src/lib/utility/generic.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,18 @@ export function keyBy(value, key) {
return obj
}

export function isCollision(items, pickedItem, callback) {
const currentItem = items[pickedItem];
const itemsGroup = items.filter(item => item.group === currentItem.group && item.id !== currentItem.id);

itemsGroup.forEach(item => {
if((currentItem.start <= item.start && currentItem.end >= item.end) ||
(currentItem.start >= item.start && currentItem.end <= item.end) ||
(currentItem.start <= item.start && currentItem.end >= item.start) ||
(currentItem.start <= item.end && currentItem.end >= item.end)) {
return callback(currentItem, item);
}
});
}

export function noop() {}