Christian Heilmann

Building an advent calendar for Mozilla in PHP/JS/CSS (part 2)

Tuesday, November 29th, 2011 at 2:32 pm
Note: the server is defunct at the moment but you can get the source code of the calendar on GitHub: code and zip

In Yesterday’s tutorial I went up to making the advent calendar use Ajax to load the different content, now for extra effect I made the entries move to where you clicked the calendar. There is not much more to it and you can see the whole thing in action here.

Step 4: Moving overlays

Now, first of all, instead of showing the entry above the calendar we now cover it, which means we should give the end users a way to get rid of them. this means adding a close link to the entry. As the entry is assembled in the API, this needs to happen there:

<?php
  $data = '';
  $day = 0;
  $day = +$_GET['day'];
  if (+date('m') < 12 || $day > +date('d')) {
    $day = 0;
  }
  if ($day > 24) { $day = 24; }
 
  if ($day) {
    $data .= '<h1><a href="#">Title '.$day.'</a></h1>'.
             '<p>Description</p>'.
             '<p>See it in action <a href="#">here</a></p>';
  }
 
  $data .= '<p><a href="index.php" id="close">close</a></p>';
 
  if (isset($_GET['ajax'])) { echo $data; }
?>

The link also points back to the page, so if there is no JavaScript available, the page just reloads.

Now, in order to hide and show the entry, I positioned it absolutely and gave it a higher z-index:

article {
  z-index: 10;
  position: absolute;
  top: -400px;
  width: 700px;
  background: #d3caff;
}

This, of course needs to be un-done when there is a day selected. In the case of a no JavaScript version, this needs to be done in the PHP itself. To override the original, I defined a class:

article.show {
  top: 200px;
}

And in the PHP I write out this class when a valid day was sent in:

<article <?php if($day) {echo ' class="show"';} ?>>
   <?php if($day) {echo $data;} ?>
</article>

In order to move the entry to the right height, you need to capture the mouse position when the user clicks one of the links. For this, I modified the event delegation handler to read the y position of the mouse and send it through to the ajax loader:

list.addEventListener('click', function(ev) {
  var t = ev.target;
  if (t.tagName ==='A') {
    var y = ev.clientY + document.body.scrollTop +
            document.documentElement.scrollTop;
    load(+t.innerHTML, y);
  }
  ev.preventDefault();
}, false);

The loading function then does not only change the content of the article element but it also adds the class of “show” to it and modifies its top position to the y parameter it received.

function load(day, y) {
  var httpstats = /200|304/;
  var request = new XMLHttpRequest();
  request.onreadystatechange = function() {
    if (request.readyState == 4) {
      if (request.status && httpstats.test(request.status)) {
        output.innerHTML = request.responseText;
        output.className = 'show';
        output.style.top = y + 'px';
      } else {
        document.location = 'index.php?day=' + day;
      }
    }
  };
  request.open('get', 'simpleapi-closing.php?ajax=1&day='+day, true);
  request.setRequestHeader('If-Modified-Since',
                           'Wed, 05 Apr 2006 00:00:00 GMT');
  request.send(null);  
}

To make this smooth, all we need to do is add a CSS transition to the element – isn’t it wonderful to work with browsers that don’t suck?

article {
  z-index: 10;
  position: absolute;
  top: -400px;
  width: 700px;
  background: #d3caff;
  -moz-transition: top 1s;
  -webkit-transition: top 1s;
  -ms-transition: top 1s;
  -o-transition: top 1s;
  transition: top 1s;
}

To hide the element again we need to re-set the position and remove the “show” class. This happens in two cases: when the user clicks the close link (we gave it an ID of “close”) and when the user hits the ESC key on the keyboard. For the former we use event delegation on the article element (as its content keeps getting re-written), for the latter a keyboard handler on the document:

output.addEventListener('click',function(ev){
  var t = ev.target;
  if(t.tagName === 'A' && t.id === 'close') {
    output.style.top = '-400px';
    output.className = '';
    ev.preventDefault();
  }
},false);
 
document.addEventListener( 'keydown', function(key) {
   if ( key.keyCode === 27 ) {
     if( output.className === 'show' ) {
       output.style.top = '-400px';
       output.className = '';
     }
   }
 }, false );

That’s it! Smooth entries moving up and down when you click different links.

Step 5: Adding the real content

The final change to make this work was to find a way for people to send me links to show. For this, all I needed to do was create a Google spreadsheet and send it around.

You can publish your spreadsheets on the web using File > Publish to the web > Start Publishing. I chose CSV as the format and then changed the API accordingly.

I load the document in using cURL and then split it into an associative array using the csv_to_array() method by M. Bunge found on the PHP docs for str_getcsv().

I then check if there is an entry for the day and if there isn’t I just give back a “not yet” string. Otherwise I get the data from the associative array and assemble the real string.

<?php
$now = '';
$day = 0;
$data = '';
 
if (isset($_GET['day'])) {
  $day = +($_GET['day']);
  if ($day > 24) { $day = 24; }
  if (+date('m') < 12 || $day > +date('d')) {
    $day = 0;
  }
 
  $url = 'https://docs.google.com/spreadsheet/pub?'.
         'key=0AhphLklK1Ve4dEp5X2tBNHFPM0hQSHpZQnBjYl9NLUE&output=csv';
  $ch = curl_init(); 
  curl_setopt($ch, CURLOPT_URL, $url); 
  curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 
  curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
  curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
  $output = curl_exec($ch); 
  curl_close($ch);
 
  $csvdata = csv_to_array($output);
  $now = $csvdata[($day-1)];
  if ($now) {
    $data .= '<h1><a href="'.$now[0].'">'.$now[1].'</a></h1>'.
             '<p>'.$now[2].'</p>';
    if ($now[3] !== '') {
      $data .= '<p>You can also <a href="'.$now[3].
               '">see it in action here</a>.</p>';
    }
  } else {
    $data .= '<h1><a href="http://developer.mozilla.com">Not yet!</a></h1>'.
             '<p>You have to wait, like all the others.</p>';
  }
}
 
$data .= '<a id="close" href="index.php">x</a>';
 
if (isset($_GET['ajax'])) { echo $data; }
 
function csv_to_array($input, $delimiter=',') {
  $header = null;
  $data = array();
  $csvData = str_getcsv($input, "\n");
  foreach($csvData as $csvLine) {
    if (is_null($header)) {
      $header = explode($delimiter, $csvLine); 
    } else {
      $items = explode($delimiter, $csvLine);
      for($n = 0, $m = count($header); $n < $m; $n++){
        $prepareData[$n] = $items[$n];
      }
      $data[] = $prepareData;
    }
  }
  return $data;
}
?>

And that is that – a simple advent calendar in PHP progressively enhanced with JavaScript and CSS.

See the final functional demo in action at:
http://isithackday.com/calendar-tutorial/realcontent.php

Final tweaks

In the final version I will of course add some caching instead of hitting Google Docs live and I added a lot more CSS to make it look the way I wanted to. But this should get you going.

The Final Build folder has a cleaned up version, with external CSS and JavaScript and a nice (function(){}()); around the JS to stop those global variables.

You can also get the whole project on GitHub

Share on Mastodon (needs instance)

Share on Twitter

My other work: