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!
👋 A new version of this component is available. Download now →.
Creating the structure
The HTML structure is composed of three different elements: a div.cd-schedule__timeline
for the events timeline(09:00, 09:30, ..), a div.cd-schedule__events
wrapping the events list and a div.cd-schedule-modal
for the modal window used to provide more details about the selected event.
<div class="cd-schedule cd-schedule--loading margin-top-lg margin-bottom-lg js-cd-schedule">
<div class="cd-schedule__timeline">
<ul>
<li><span>09:00</span></li>
<li><span>09:30</span></li>
<!-- additional elements here -->
</ul>
</div> <!-- .cd-schedule__timeline -->
<div class="cd-schedule__events">
<ul>
<li class="cd-schedule__group">
<div class="cd-schedule__top-info"><span>Monday</span></div>
<ul>
<li class="cd-schedule__event">
<a data-start="09:30" data-end="10:30" data-content="event-abs-circuit" data-event="event-1" href="#0">
<em class="cd-schedule__name">Abs Circuit</em>
</a>
</li>
<!-- other events here -->
</ul>
</li>
<li class="cd-schedule__group">
<div class="cd-schedule__top-info"><span>Tuesday</span></div>
<ul>
<!-- events here -->
</ul>
</li>
<!-- additional li.cd-schedule__group here -->
</ul>
</div>
<div class="cd-schedule-modal">
<header class="cd-schedule-modal__header">
<div class="cd-schedule-modal__content">
<span class="cd-schedule-modal__date"></span>
<h3 class="cd-schedule-modal__name"></h3>
</div>
<div class="cd-schedule-modal__header-bg"></div>
</header>
<div class="cd-schedule-modal__body">
<div class="cd-schedule-modal__event-info"></div>
<div class="cd-schedule-modal__body-bg"></div>
</div>
<a href="#0" class="cd-schedule-modal__close text-replace">Close</a>
</div>
</div> <!-- .cd-schedule -->
⚠️ Note: if you want to change the timeline hours (e.g., add events after the 18:00), make sure to:
- Create a new list item (with your new time) in the
.cd-schedule__timeline
; - Update the
--schedule-rows-number
css variable inside the style.scss file (for example, if you add a new slot - 18:30 - change the--schedule-rows-number
value from 19 to 20).
Adding style
On small devices (window width smaller than 800px) and if JavaScript is disabed, all the events inside a .cd-schedule__group
are lined up horizontally: we set a display: flex
to the .cd-schedule__group > ul
element and an overflow-x: scroll
to make the events scrollable.
.cd-schedule__group > ul {
position: relative;
padding: 0 var(--component-padding);
display: flex;
overflow-x: scroll;
-webkit-overflow-scrolling: touch;
}
.cd-schedule__event {
flex-shrink: 0; // force them to stay on one line
float: left; // flex fallback
height: 150px;
width: 70%;
max-width: 300px;
}
As for the .cd-schedule-modal
, it is has a fixed position and it is moved to the right outside the viewport. When the user selects an event, the .cd-schedule-modal--open
class is used to translate the .cd-schedule-modal
back into the viewport.
.cd-schedule-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--open { // this 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).
.js {
@include breakpoint(md) {
.cd-schedule__events {
width: 100%;
> ul {
display: flex;
flex-wrap: nowrap;
}
}
.cd-schedule__group {
flex-basis: 0;
flex-grow: 1;
}
.cd-schedule__event {
position: absolute;
z-index: 3;
width: calc(100% + 2px); // top position and height will be set using js
left: -1px;
}
}
}
As for the .cd-schedule-modal
, the opening/closing animation is created using JavaScript combined with CSS Transitions and Transformations (more in the Events handling section).
Events handling
To implement this event schedule, we created a ScheduleTemplate
object and used the scheduleReset()
and initEvents()
functions to init the schedule and attach event handlers to the proper elements.
function ScheduleTemplate( element ) {
this.element = element;
this.timelineItems = this.element.getElementsByClassName('cd-schedule__timeline')[0].getElementsByTagName('li');
//..
this.singleEvents = this.element.getElementsByClassName('cd-schedule__event');
//..
this.initSchedule();
};
ScheduleTemplate.prototype.initSchedule = function() {
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).
ScheduleTemplate.prototype.placeEvents = function() {
// on big devices - place events in the template according to their time/day
var self = this,
slotHeight = this.topInfoElement.offsetHeight;
for(var i = 0; i < this.singleEvents.length; i++) {
var anchor = this.singleEvents[i].getElementsByTagName('a')[0];
var start = getScheduleTimestamp(anchor.getAttribute('data-start')),
duration = getScheduleTimestamp(anchor.getAttribute('data-end')) - start;
var eventTop = slotHeight*(start - self.timelineStart)/self.timelineUnitDuration,
eventHeight = slotHeight*duration/self.timelineUnitDuration;
this.singleEvents[i].setAttribute('style', 'top: '+(eventTop-1)+'px; height: '+(eventHeight +1)+'px');
}
};
When the user selects an event, Ajax 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 .cd-schedule-modal
is animated to show the event content.
First, the .cd-schedule-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 .cd-schedule-modal__header-bg
and .cd-schedule-modal__body-bg
elements are scaled up to create the morphing animation; at the end of this animation, the modal content is revealed.
ScheduleTemplate.prototype.openModal = function(target) {
var self = this;
var mq = self.mq();
this.animating = true;
//update event name and time
this.modalEventName.textContent = target.getElementsByTagName('em')[0].textContent;
this.modalDate.textContent = target.getAttribute('data-start')+' - '+target.getAttribute('data-end');
this.modal.setAttribute('data-event', target.getAttribute('data-event'));
//update event content
this.loadEventContent(target.getAttribute('data-content'));
Util.addClass(this.modal, 'cd-schedule-modal--open');
if( mq == 'mobile' ) {
self.modal.addEventListener('transitionend', function cb(){
self.animating = false;
self.modal.removeEventListener('transitionend', cb);
});
} else {
var eventPosition = target.getBoundingClientRect(),
eventTop = eventPosition.top,
eventLeft = eventPosition.left,
eventHeight = target.offsetHeight,
eventWidth = target.offsetWidth;
var windowWidth = window.innerWidth,
windowHeight = window.innerHeight;
var modalWidth = ( windowWidth*.8 > self.modalMaxWidth ) ? self.modalMaxWidth : windowWidth*.8,
modalHeight = ( windowHeight*.8 > self.modalMaxHeight ) ? self.modalMaxHeight : windowHeight*.8;
var modalTranslateX = parseInt((windowWidth - modalWidth)/2 - eventLeft),
modalTranslateY = parseInt((windowHeight - modalHeight)/2 - eventTop);
var HeaderBgScaleY = modalHeight/eventHeight,
BodyBgScaleX = (modalWidth - eventWidth);
//change modal height/width and translate it
self.modal.setAttribute('style', 'top:'+eventTop+'px;left:'+eventLeft+'px;height:'+modalHeight+'px;width:'+modalWidth+'px;transform: translateY('+modalTranslateY+'px) translateX('+modalTranslateX+'px)');
//set modalHeader width
self.modalHeader.setAttribute('style', 'width:'+eventWidth+'px');
//set modalBody left margin
self.modalBody.setAttribute('style', 'margin-left:'+eventWidth+'px');
//change modalBodyBg height/width ans scale it
self.modalBodyBg.setAttribute('style', 'height:'+eventHeight+'px; width: 1px; transform: scaleY('+HeaderBgScaleY+') scaleX('+BodyBgScaleX+')');
//change modal modalHeaderBg height/width and scale it
self.modalHeaderBg.setAttribute('style', 'height: '+eventHeight+'px; width: '+eventWidth+'px; transform: scaleY('+HeaderBgScaleY+')');
self.modalHeaderBg.addEventListener('transitionend', function cb(){
//wait for the end of the modalHeaderBg transformation and show the modal content
self.animating = false;
Util.addClass(self.modal, 'cd-schedule-modal--animation-completed');
self.modalHeaderBg.removeEventListener('transitionend', cb);
});
}
};
Note: we implemented a simple Ajax request to upload the new html content, but you may wanna replace it with a complete request (error handling, loader element, ..).