Go to homepage

Projects /

Schedule Template

A simple template that lets you display events on a timeline, as well as organize them in groups (week days, conference rooms etc…)

Schedule Template
Check our new component library →

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, ..).

Project duplicated

Project created

Globals imported

There was an error while trying to export your project. Please try again or contact us.