JSFiddle - React, Tailwind, and code Playground
by Alexandru Mihai
HTML
<div class="timetable"></div>
CSS
.timetable{box-sizing:border-box}.timetable::after{clear:both;content:"";display:table}.timetable *,.timetable *:before,.timetable *:after{box-sizing:inherit}.timetable ul,.timetable li{margin:0;padding:0;list-style-type:none}.timetable aside,.timetable section{float:left}.timetable aside{width:25%;padding:0!important;margin-top:46px;border-right:5px solid transparent}.timetable aside li{padding:0 15px;background-color:#EFEFEF;line-height:46px}.timetable aside li:not(:last-of-type){border-bottom:1px solid #fff}.timetable aside .row-heading{display:inline-block;max-width:100%;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;word-wrap:normal}.timetable ul{list-style-type:none;margin:0}.timetable aside li,.timetable time li{height:46px}.timetable section{width:75%;padding:0!important;overflow-x:scroll;-webkit-overflow-scrolling:touch}.timetable section time{white-space:nowrap;display:inline-block}.timetable section header{height:46px;transform-style:preserve-3d;font-size:0}.timetable section header::after{clear:both;content:"";display:table}.timetable section header li{display:inline-block;font-size:1rem;overflow:visible;width:0;line-height:46px;display:inline-block;position:relative}.timetable section header li:not(:last-of-type){width:96px}.timetable section header li .time-label{display:block;position:absolute;left:0}.timetable section header li:not(:first-of-type) .time-label{transform:translateX(-50%)}.timetable section header li:last-of-type .time-label{transform:translateX(-100%)}@media only screen and (max-width:960px){.timetable aside{width:40%}.timetable section{width:60%}}.timetable ul.room-timeline{border-left:none;position:relative;overflow:hidden}.timetable ul.room-timeline li{position:relative;background-color:#F4F4F4}.timetable ul.room-timeline li:nth-of-type(odd){background-color:#FDFDFD}.timetable ul.room-timeline li:first-of-type{border-top:1px solid #E5E5E5}.timetable ul.room-timeline li:last-of-type{border-bottom:1px solid...
JavaScript
/*jshint -W079*/
'use strict';
var Timetable = function() {
this.scope = {
hourStart: 9,
hourEnd: 17
};
this.locations = [];
this.events = [];
};
Timetable.Renderer = function(tt) {
if (!(tt instanceof Timetable)) {
throw new Error('Initialize renderer using a Timetable');
}
this.timetable = tt;
};
(function() {
function isValidHourRange(start, end) {
return isValidHour(start) && isValidHour(end);
}
function isValidHour(number) {
return isInt(number) && isInHourRange(number);
}
function isInt(number) {
return number === parseInt(number, 10);
}
function isInHourRange(number) {
return number >= 0 && number < 24;
}
function locationExistsIn(loc, locs) {
return locs.indexOf(loc) !== -1;
}
function isValidTimeRange(start, end) {
var correctTypes = start instanceof Date && end instanceof Date;
var correctOrder = start < end;
return correctTypes && correctOrder;
}
function getDurationHours(startHour, endHour) {
return endHour >= startHour ? endHour - startHour : 24 + endHour - startHour;
}
Timetable.prototype = {
setScope: function(start, end) {
if (isValidHourRange(start, end)) {
this.scope.hourStart = start;
this.scope.hourEnd = end;
} else {
throw new RangeError('Timetable scope should consist of (start, end) in whole hours from 0 to 23');
}
return this;
},
addLocations: function(newLocations) {
function hasProperFormat() {
return newLocations instanceof Array;
}
var existingLocations = this.locations;
if (hasProperFormat()) {
newLocations.forEach(function(loc) {
if (!locationExistsIn(loc, existingLocations)) {
existingLocations.push(loc);
} else {
throw new Error('Location already exists');
}
});
} else {
throw new Error('Tried to add locations in wrong format');
}
return this;
},
addEvent: function(name, location, start, end, options) {
if (!locationExistsIn(location, this.locations)) {
throw new...