react-chronos
Version:
⛓ React chronology component providing dual chronological timelines.
231 lines (199 loc) • 7.36 kB
JavaScript
import React, { PropTypes } from 'react';
import classnames from 'classnames/dedupe';
import { omit, keys } from 'lodash';
import * as utils from './utils';
/**
* The Chronology component is the main and base component.
*/
class Chronology extends React.Component {
/**
* Construct a new chronlogy.
*/
constructor(props) {
super(props);
}
/**
* Apply styling to timeline.
*/
styleTimeline() {
this.timeline.style.position = 'absolute';
if (this.props.type === 'horizontal') {
const timelineHeight = this.timeline.offsetHeight;
const containerHeight = this.container.offsetHeight;
const timelineTop = ((containerHeight - timelineHeight) / containerHeight) / 2;
this.timeline.style.top = `${timelineTop * 100}%`;
this.timeline.style.left = 0;
this.timeline.style.height = `${timelineHeight}px`;
this.timeline.style.marginTop = `-${timelineHeight / 2}px`;
} else if (this.props.type === 'vertical') {
const timelineWidth = this.timeline.offsetWidth;
const containerWidth = this.container.offsetWidth;
const timelineLeft = ((containerWidth - timelineWidth) / containerWidth) / 2;
this.timeline.style.left = `${timelineLeft * 100}%`;
this.timeline.style.top = 0;
this.timeline.style.width = `${timelineWidth}px`;
this.timeline.style.marginTop = `-${timelineWidth / 2}px`;
}
}
/**
* Apply styling to events and its markers (if any).
*/
styleEvents() {
const events = Array.from(this.container.querySelectorAll(this.props.eventSelector));
const markers = Array.from(this.container.querySelectorAll(this.props.markerSelector));
const containerWidth = this.container.offsetWidth;
const containerHeight = this.container.offsetHeight;
const { type, eventClassNames, markerClassNames } = this.props;
// Positionate events side by side.
let sides = { a: 0, b: 0 };
let lastMarkerPos = 0;
events.forEach((event, i) => {
event.style.position = 'absolute';
if (type === 'horizontal') {
// Select in which side to put the event.
const top = sides.a <= sides.b;
if (top) {
event.style.top = 0;
event.style.left = `${sides.a}px`;
sides.a += utils.outerWidth(event);
event.className = classnames(event.className, eventClassNames.top);
} else {
event.style.bottom = 0;
event.style.left = `${sides.b}px`;
sides.b += utils.outerWidth(event);
event.className = classnames(event.className, eventClassNames.bottom);
}
// Position the marker.
const marker = markers[i];
if (marker) {
const className = top ? markerClassNames.top : markerClassNames.bottom;
marker.className = classnames(marker.className, className);
marker.style.position = 'absolute';
const markerTop = ((containerHeight - marker.offsetHeight) / containerHeight) / 2;
marker.style.top = `${markerTop * 100}%`;
let nextMarkerLeft = parseInt(event.style.left, 10);
const markerOuterWidth = utils.outerWidth(marker);
const willOverlap = i > 0 && (nextMarkerLeft <= (lastMarkerPos + markerOuterWidth));
if (willOverlap) {
nextMarkerLeft = lastMarkerPos + markerOuterWidth;
}
marker.style.left = `${nextMarkerLeft}px`;
lastMarkerPos = nextMarkerLeft;
}
// TODO Handle special case where the marker could be place outside the event.
// Adjust the width of the container and the timeline.
// TODO The container should only be resized if the width wasn't provided.
// this.container.style.width = Math.max(sides.a, sides.b) + 'px';
this.timeline.style.width = Math.max(sides.a, sides.b) + 'px';
} else if (type === 'vertical') {
// Select in which side to put the event.
const left = sides.a <= sides.b;
if (left) {
event.style.left = 0;
event.style.top = `${sides.a}px`;
sides.a += utils.outerHeight(event);
event.className = classnames(event.className, eventClassNames.left);
} else {
event.style.right = 0;
event.style.top = `${sides.b}px`;
sides.b += utils.outerHeight(event);
event.className = classnames(event.className, eventClassNames.right);
}
// Position the marker.
const marker = markers[i];
if (marker) {
const className = left ? markerClassNames.left : markerClassNames.right;
marker.className = classnames(marker.className, className);
marker.style.position = 'absolute';
const markerLeft = ((containerWidth - marker.offsetWidth) / containerWidth) / 2;
marker.style.left = `${markerLeft * 100}%`;
let nextMarkerTop = parseInt(event.style.top, 10);
const markerOuterHeight = utils.outerHeight(marker);
const willOverlap = i > 0 && (nextMarkerTop <= (lastMarkerPos + markerOuterHeight));
if (willOverlap) {
nextMarkerTop = lastMarkerPos + markerOuterHeight;
}
marker.style.top = `${nextMarkerTop}px`;
lastMarkerPos = nextMarkerTop;
}
// TODO Handle special case where the marker could be place outside the event.
// Adjust the height of the container and the timeline.
// TODO The container should only be resized if the height wasn't provided.
// this.container.style.height = Math.max(sides.a, sides.b) + 'px';
this.timeline.style.height = Math.max(sides.a, sides.b) + 'px';
}
});
}
/**
* Redraw the entire chronology.
*/
redraw() {
this.container.style.position = 'relative';
this.styleTimeline();
this.styleEvents();
}
/**
* Redraw upon mounting.
*/
componentDidMount() {
this.redraw();
}
/**
* Redraw upon update.
*/
componentDidUpdate() {
this.redraw();
}
/**
* Render the chronology.
*/
render() {
const { timelineStyle } = this.props;
const otherProps = omit(this.props, keys(Chronology.propTypes));
return (
<div ref={(el) => this.container = el} {...otherProps}>
<div style={timelineStyle} ref={(el) => this.timeline = el}></div>
{this.props.children}
</div>
);
}
}
Chronology.propTypes = {
type: PropTypes.oneOf(['horizontal', 'vertical']),
eventSelector: PropTypes.string,
eventClassNames: PropTypes.shape({
left: PropTypes.string,
right: PropTypes.string,
top: PropTypes.string,
bottom: PropTypes.string,
}),
markerSelector: PropTypes.string,
markerClassNames: PropTypes.shape({
left: PropTypes.string,
right: PropTypes.string,
top: PropTypes.string,
bottom: PropTypes.string,
}),
timelineStyle: PropTypes.object,
};
Chronology.defaultProps = {
type: 'vertical',
eventSelector: '.event',
eventClassNames: {
left: 'event-left',
right: 'event-right',
top: 'event-top',
bottom: 'event-bottom',
},
markerSelector: '.marker',
markerClassNames: {
left: 'marker-left',
right: 'marker-right',
top: 'marker-top',
bottom: 'marker-bottom',
},
timelineStyle: {
backgroundColor: '#888',
},
};
export default Chronology;