Schedule Template
Schedule Template
We’ve come across this web component many times: when we check the schedule of a conference, or the timetable of the classes of our gym. From a web designer perspective, it is handy to have a simple, responsive template to use if you ever need to create a schedule table. So we built one!
Creating the structure
The HTML structure is composed of three different elements: a div.timeline
for the events timeline(09:00, 09:30, ..), a div.events
wrapping the events list and a div.event-modal
for the modal window used to provide more details about the selected event.
<div class="cd-schedule">
<div class="timeline">
<ul>
<li><span>09:00</span></li>
<li><span>09:30</span></li>
<!-- additional elements here -->
</ul>
</div>
<div class="events">
<ul>
<li class="events-group">
<div class="top-info"><span>Monday</span></div>
<ul>
<li class="single-event" data-start="09:30" data-end="10:30" data-content="event-abs-circuit" data-event="event-1">
<a href="#0">
<em class="event-name">Abs Circuit</em>
</a>
</li>
<!-- other events here -->
</ul>
</li>
<li class="events-group">
<div class="top-info"><span>Tuesday</span></div>
<ul>
<!-- events here -->
</ul>
</li>
<!-- additional li.events-group here -->
</ul>
</div>
<div class="event-modal">
<header class="header">
<div class="content">
<span class="event-date"></span>
<h3 class="event-name"></h3>
</div>
<div class="header-bg"></div>
</header>
<div class="body">
<div class="event-info"></div>
<div class="body-bg"></div>
</div>
<a href="#0" class="close">Close</a>
</div>
</div>
Adding style
On small devices (window width less than 800px), all the events inside an .events-group
are lined up horizontally: we set a display: flex
to the .events-group > ul
element and an overflow-x: scroll
to make the events scrollable.
.cd-schedule .events .events-group > ul {
position: relative;
padding: 0 5%;
/* force its children to stay on one line */
display: flex;
overflow-x: scroll;
-webkit-overflow-scrolling: touch;
}
.cd-schedule .events .single-event {
/* force them to stay on one line */
flex-shrink: 0;
float: left;
height: 150px;
width: 70%;
max-width: 300px;
}
As for the .event-modal
, it is has a fixed position and it is moved to the right outside the viewport. When the user selects an event, the .modal-is-open
class is used to translate the .event-modal
back into the viewport.
.cd-schedule .event-modal {
position: fixed;
z-index: 3;
top: 0;
right: 0;
height: 100%;
width: 100%;
visibility: hidden;
transform: translateX(100%);
transition: transform .4s, visibility .4s;
}
.cd-schedule.modal-is-open .event-modal {
/* .modal-is-open class is added as soon as an event is selected */
transform: translateX(0);
visibility: visible;
}
On bigger devices, all the events are in absolute position and placed inside a timetable: the top position and the height of each event are evaluated using the data-start
and data-end
attributes of the event itself and set using JavaScript (more in the Events handling section).
@media only screen and (min-width: 800px) {
.cd-schedule .events {
float: left;
width: 100%;
}
.cd-schedule .events .events-group {
width: 20%;
float: left;
}
.cd-schedule .events .single-event {
position: absolute;
z-index: 3;
/* top position and height will be set using js */
width: calc(100% + 2px);
left: -1px;
}
}
As for the .event-modal
, the opening/closing animation is created using jQuery combined with CSS Transitions and Transformations (more in the Events handling section).
Events handling
To implement this event schedule, we created a SchedulePlan
object and used the scheduleReset()
and initEvents()
functions to init the schedule and attach event handlers to the proper elements.
function SchedulePlan( element ) {
this.element = element;
this.timeline = this.element.find('.timeline');
//...
this.eventsWrapper = this.element.find('.events');
this.eventsGroup = this.eventsWrapper.find('.events-group');
this.singleEvents = this.eventsGroup.find('.single-event');
//..
this.scheduleReset();
this.initEvents();
}
On big devices, the scheduleReset()
method takes care of placing the events inside the timetable and set their height. To evaluate the height, for example, we calculate the duration of the event (data-end minus data-start), divide it by the ‘eventUnit’ (in our case it’s 30 minutes) and then multiply it by the height of ‘timeline unit’ (in our case, 50px).
var self = this;
this.singleEvents.each(function(){
//place each event in the grid -> need to set top position and height
var start = getScheduleTimestamp($(this).attr('data-start')), //getScheduleTimestamp converts hh:mm to timestamp
duration = getScheduleTimestamp($(this).attr('data-end')) - start;
var eventTop = self.eventUnitHeight*(start - self.timelineStart)/self.timelineUnitDuration,
eventHeight = self.eventUnitHeight*duration/self.timelineUnitDuration;
$(this).css({
top: (eventTop -1) +'px',
height: (eventHeight+1)+'px'
});
});
When the user selects an event, the jQuery load()
function is used to load the content of the event just selected (its data-content
is used to determine the file content to be loaded).
In addition to that, on big devices, the .event-modal
is animated to show the event content. First, the .event-modal
is placed on top of the selected event and its height and width are changed to be equal to the ones of the selected event; then the .header-bg
and .body-bg
elements are scaled up to create the morphing animation; at the end of this animation, the modal content is revealed.
SchedulePlan.prototype.openModal = function(event) {
var self = this;
var mq = self.mq();
this.animating = true;
//update event name and time
this.modalHeader.find('.event-name').text(event.find('.event-name').text());
this.modalHeader.find('.event-date').text(event.find('.event-date').text());
this.modal.attr('data-event', event.parent().attr('data-event'));
//update event content
this.modalBody.find('.event-info').load(event.parent().attr('data-content')+'.html .event-info > *', function(data){
//once the event content has been loaded
self.element.addClass('content-loaded');
});
this.element.addClass('modal-is-open');
if( mq == 'mobile' ) {
self.modal.one(transitionEnd, function(){
self.modal.off(transitionEnd);
self.animating = false;
});
} else {
//change modal height/width and translate it
self.modal.css({
top: eventTop+'px', //this is the selected event top position
left: eventLeft+'px', //this is the selected event left position
height: modalHeight+'px', //this is the modal final height
width: modalWidth+'px', //this is the modal final width
});
transformElement(self.modal, 'translateY('+modalTranslateY+'px) translateX('+modalTranslateX+'px)');
//set modalHeader width
self.modalHeader.css({
width: eventWidth+'px', //this is the selected event width
});
//set modalBody left margin
self.modalBody.css({
marginLeft: eventWidth+'px',
});
//change modalBodyBg height/width and scale it
self.modalBodyBg.css({
height: eventHeight+'px',
width: '1px',
});
transformElement(self.modalBodyBg, 'scaleY('+HeaderBgScaleY+') scaleX('+BodyBgScaleX+')');
//change modal modalHeaderBg height/width and scale it
self.modalHeaderBg.css({
height: eventHeight+'px',
width: eventWidth+'px',
});
transformElement(self.modalHeaderBg, 'scaleY('+HeaderBgScaleY+')');
self.modalHeaderBg.one(transitionEnd, function(){
//wait for the end of the modalHeaderBg transformation and show the modal content
self.modalHeaderBg.off(transitionEnd);
self.animating = false;
self.element.addClass('animation-completed');
});
}
};
One note: we implemented a simple load()
function to upload the new html content, but you may wanna replace it with a $.ajax
call in order to handle errors, beforeSend request etc. according to your project.