diff --git a/CHANGELOG.md b/CHANGELOG.md index 03a23704b..e7723ec23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/__tests__/utils/calendar/item-collision.js b/__tests__/utils/calendar/item-collision.js new file mode 100644 index 000000000..c9da52473 --- /dev/null +++ b/__tests__/utils/calendar/item-collision.js @@ -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); + }); + }); + }); + +}); diff --git a/demo/app/demo-colision/index.js b/demo/app/demo-colision/index.js new file mode 100644 index 000000000..23337d353 --- /dev/null +++ b/demo/app/demo-colision/index.js @@ -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 ( + 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} + onZoom={this.handleZoom} + onCollision={this.handleItemCollision} + moveResizeValidator={this.moveResizeValidator} + buffer={3} + > + + + + + {({ styles }) => { + const newStyles = { ...styles, backgroundColor: 'blue' } + return
+ }} + + + + + ) + } +} diff --git a/demo/app/index.js b/demo/app/index.js index 30c7cc437..7b8b0e12e 100644 --- a/demo/app/index.js +++ b/demo/app/index.js @@ -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 diff --git a/src/lib/Timeline.js b/src/lib/Timeline.js index c77ba2f35..00fc6502d 100644 --- a/src/lib/Timeline.js +++ b/src/lib/Timeline.js @@ -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, @@ -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, @@ -159,6 +160,7 @@ export default class ReactCalendarTimeline extends Component { onCanvasClick: null, onItemDoubleClick: null, onItemContextMenu: null, + onCollision: null, onZoom: null, verticalLineClassNamesForTime: null, @@ -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); } } @@ -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 }) => { diff --git a/src/lib/utility/generic.js b/src/lib/utility/generic.js index 3e4ae1527..89fd66f87 100644 --- a/src/lib/utility/generic.js +++ b/src/lib/utility/generic.js @@ -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() {}