Wednesday, October 14th, 2009

View Source Tutorial: Sticky Notes With HTML5 and CSS3

Category: Tutorial, View Source

>View Source is a new series where we crack open cool web sites and applications and detail how they were made, step by step.

Today we will take a look at the Webkit Sticky Notes demo that was created when Webkit first landed it’s HTML 5 SQL storage support:

stickynotes_screenshot

In this demo you can create new sticky notes that persist themselves into the local SQL storage and can be accessed while offline. When a sticky note is closed it ‘swooshes’ offscreen with a nice animated effect.

Technologies used in this View Source tutorial:

Note that the demo will only currently work in Webkit 4+ based browsers, Safari only; the technologies behind it though are beginning to arrive in Firefox and Chrome however.

Let’s break this demo down step by step.

Is there a Doctype in the house?

First we have the HTML5 doctype at the top of the page:

  1. < !doctype html>

This causes an HTML5 supporting browser to switch into HTML5 parsing mode.

Tune In, Turn On, Go Offline

Since we want this application to work offline, we have to provide all of the resources necessary to ‘draw’ this application when away from the network; i.e. we need to tell the browser where to find all of it’s HTML, CSS, JavaScript, images, etc. We can’t depend on the browser’s cache since some resources might not be in there. Instead, we have to use the HTML5 Application Cache API.

To use the HTML5 Application Cache, we first provide a file named StickyNotes.manifest that lists all the files we will need while offline:

  1. CACHE MANIFEST
  2. #version=1
  3. deleteButton.png
  4. deleteButtonPressed.png

By default, the first line of this file must have the magic words CACHE MANIFEST. Following this you can provide comments starting with the # character. Afterward we provide all of the files to bring offline. You do not need to provide the originating HTML page since this is included by default, though it is recommended that you do provide it in your files. In our example almost everything is self-contained within the page so all we have to grab are a few image files necessary to draw the small closing X button on each sticky note. The file names must be relative or absolute file names or URLs, but must be on the same domain as the originating page.

Note that your manifest file can have any file name or file extension that you want, but it must be served with the MIME type text/cache-manifest to get recognized.

Finally we have to point to our manifest file using the manifest attribute on the html tag:

  1. <html manifest="StickyNotes.manifest">

When the browser first downloads this page, it will see and grab the manifest file. It will then proceed to download all of the files given in the manifest file. Afterward, in the background, the browser will periodically re-fetch the manifest file to see if it has changed. If it has changed in any way (such as adding or removing a file or changing a comment, such as the #version=1 comment above), it will then do an HTTP HEAD request on each file in the manifest file to see if those have changed. If they have changed then a file is reloaded into the offline manifest.

Swoosh!

Let’s break down the UI and the nice swoosh effect that happens when you close a sticky note.

First off, each sticky note is just a DIV with the following styling:

  1. .note {
  2.     background-color: rgb(255, 240, 70);
  3.     height: 250px;
  4.     padding: 10px;
  5.     position: absolute;
  6.     width: 200px;
  7.     -webkit-box-shadow: 0px 5px 10px rgba(0, 0, 0, 0.5);
  8. }

Basically, we make the background yellow; give it a height and width; give it some internall padding so the text looks nice in the middle; and then position the note absolutely. We then add a black box shadow that is 50% transparent.

The little close button is just two PNG images:

deleteButton.png deleteButtonPressed.png

The first PNG image we only show when the mouse hovers over the corner of the sticky note using a CSS .note:hover pseudo-class. The second PNG image is shown when the user clicks (or ‘activates’ in CSS pseudo-parlance) the image:

  1. .closebutton:active {
  2.     background-image: url(deleteButtonPressed.png);
  3. }

The cool thing about both the :hover and :active pseudo-classes is it lets you do things like this elegantly without having to use any JavaScript.

The main close button itself is added to the page as a background-image and positioned absolutely within the overall note DIV:

  1. .closebutton {
  2.     display: none;
  3.     background-image: url(deleteButton.png);
  4.     height: 30px;
  5.     position: absolute;
  6.     left: -15px;
  7.     top: -15px;
  8.     width: 30px;
  9. }

When you click the close button you get a nice swoosh effect. This is actually done in JavaScript as follows:

javascript
< view plain text >
  1. this.note.style.webkitTransition = '-webkit-transform ' + duration + 's ease-in, opacity ' + duration + 's ease-in';
  2. this.note.offsetTop; // Force style recalc
  3. this.note.style.webkitTransformOrigin = "0 0";
  4. this.note.style.webkitTransform = 'skew(30deg, 0deg) scale(0)';
  5. this.note.style.opacity = '0';

Let’s break this down. Let’s first take a look at the full CSS Transition property after it would be expanded, with the duration set to 2 seconds. Let’s expand the rest of the CSS after the JavaScript has been applied to make this more readable:

  1. -webkit-transition: -webkit-transform 2s ease-in, opacity 2s ease-in;
  2. -webkit-transform-origin: 0 0;
  3. -webkit-transform: skew(30deg, 0deg) scale(0);
  4. opacity: 0;

We want two things to happen here; first, we want a transformation to happen on our DIV that will skew the top along the X axis causing a ‘shearing’ look while also scaling the DIV to zero so it disappears; at the same time, we want the opacity to go from 1 to 0 so it fades over time.

We define the two transformations using the -webkit-transform property: skew(30deg, 0deg) skews 30 degrees along the x-axis and none along the y-axis, and scale(0) scales things to zero. Transformations have to work according to an origin, which we set with -webkit-transform-origin to the upper-left corner: 0 0. We also define the final state of the opacity to be 0.

The next step is to define when all of this happens. We do this in one go by setting -webkit-transition. We use the magic property name -webkit-transform to refer to the transforms we defined earlier (i.e. the skewing and scaling) and say that it will happen over 2 seconds, easing in gradually (ease-in). We then declare that the opacity property will also happen over 2 seconds and also have a nice easing in effect.

The one thing to notice in the JavaScript code is the this.note.offsetTop property; ostensibly this is there to force a fix for a Webkit bug around style recalculations. It is a bit unclear whether this is now fixed in the latest versions of Webkit.

yiddish_phrase

(That’s ‘JavaScript’ translated into Yiddish; I couldn’t come up with a good catchy head line for this section).

The JavaScript in this sample is well put together and uses several modern JavaScript features, so let’s take a quick look at that.

First, each note is encapsulated into a Note class that keeps track of its DOM elements on screen; handles all mouse actions; and stores its internal state, such as its text, x and y coordinates, etc.

This isn’t the place to teach OOP JavaScript programming, but each Note class is a standard function that extends the prototype and internally binds the DOM elements to itself to handle the mouse:

javascript
< view plain text >
  1. // constructor
  2. function Note() {
  3.    var self = this;
  4.  
  5.     var note = document.createElement('div');
  6.     note.className = 'note';
  7.     note.addEventListener('mousedown', function(e) { return self.onMouseDown(e) }, false);
  8.     note.addEventListener('click', function() { return self.onNoteClick() }, false);
  9.     this.note = note;
  10.  
  11.     // etc
  12. }

The interesting thing about this JavaScript (or yiddish_phrase in Yiddish) is that it uses the new JavaScript getter and setter syntax to control getting and setting all internal attributes; this make even more sense when you treat each Note as a kind of Data Access Object that you will store into the local SQL storage, which we will detail in the next section. Here are some of the attributes defined on the Notes class:

javascript
< view plain text >
  1. Note.prototype = {
  2.     get id()
  3.     {
  4.         if (!("_id" in this))
  5.             this._id = 0;
  6.         return this._id;
  7.     },
  8.  
  9.     set id(x)
  10.     {
  11.         this._id = x;
  12.     },
  13.  
  14.     get text()
  15.     {
  16.         return this.editField.innerHTML;
  17.     },
  18.  
  19.     set text(x)
  20.     {
  21.         this.editField.innerHTML = x;
  22.     },
  23.  
  24.     // etc.

When dealing with a particular Note, this now means you can simply say console.log(note.text) for example to grab its text, without having to be exposed to the particular ways in which these values are fetched or stored.

SQL Storage

Finally, it’s time to detail how we save and load our notes.

When working with the HTML5 Database, the first step is to open a database by name; note the error handling code in our sample below as well ensuring that we actually have either permission or even the ability to do storage:

javascript
< view plain text >
  1. try {
  2.     if (window.openDatabase) {
  3.         db = openDatabase("NoteTest", "1.0", "HTML5 Database API example", 200000);
  4.         if (!db)
  5.             alert("Failed to open the database on disk.  This is probably because the"
  6.                   + "version was bad or there is not enough space left in this "
  7.                   + "domain's quota");
  8.     } else
  9.         alert("Couldn't open the database.  Please try with a WebKit nightly with this feature enabled");
  10. } catch(err) { }

The magic is in the openDatabase method. This method takes a unique name for the database ("NoteTest"); a version number so you can later handle upgrading a database if necessary ("1.0"); a human friendly name that can be shown to users if they are exploring what data is saved locally by web sites ("HTML5 Database API example"); and finally the size in bytes that you estimate a web application will need in total (200000). This last value is important; when you first open a database, the user will be prompted on whether to give your site permission with the total you request here. You should estimate the total size you want so that the user isn’t pestered whenever the quota is hit. In our example we ask for 200K.

Before jumping into directly using the database, it’s important to note some features of the HTML5 Database API. First, it is highly asynchronous, which means most of its methods require a JavaScript callback that get the results. This is an unfortunate necessity so that a web browser’s UI does not ‘hang’ due to JavaScript doing heavy file I/O, which might happen if JavaScript is reading from a particularly large database table for example. Second, every read or write to the database must be surrounded by a transaction, which is another necessity due to the unforgiving environment of the web: a user might leave the page in the middle of a database read or write, close the browser, etc. Both of these aspects make working with the Database API a bit trickier than would otherwise be true.

With this in mind, let’s look at code in our sample that reads all of our post-it notes into memory on page load, line by line.

First, we have to grab a transaction, as mentioned above; this method takes a JavaScript callback that will receive the transaction on which we can execute actual SQL:

javascript
< view plain text >
  1. db.transaction(function(tx) {

The tx result is our transaction object that we can actually start calling SQL on.

Next, we then grab all of our notes from a table named WebKitStickyNotes that was created earlier in the code. This table has several columns: a unique ID, the note text, a timestamp, and the left, top, and z-index on where to place this on the page:

javascript
< view plain text >
  1. db.transaction(function(tx) {
  2.    tx.executeSql("SELECT id, note, timestamp, left, top, zindex FROM WebKitStickyNotes",
  3.                   [],
  4.                   function(tx, result) {
  5.                     // handle result
  6.                   },
  7.                   function(tx, error) {
  8.                     // handle error
  9.                   });

In the code snippet above, you’ll see that we execute our SQL on the transaction we were handed back earlier by calling executeSql. We then provide the SQL to execute, which is a simple SELECT statement grabbing all of our notes and columns. The second argument is an empty array and is only used if we are inserting or updating values, which we will see later. The final two arguments is a JavaScript callback that will receive the results, and a JavaScript callback if an error occurs, respectively.

Let’s look at working with the results returned by this SQL call, focusing on just the successful results callback:

javascript
< view plain text >
  1. for (var i = 0; i < result.rows.length; ++i) {
  2.   var row = result.rows.item(i);
  3.  
  4.   var note = new Note();
  5.   note.id = row['id'];
  6.   note.text = row['note'];
  7.   note.timestamp = row['timestamp'];
  8.   note.left = row['left'];
  9.   note.top = row['top'];
  10.   note.zIndex = row['zindex'];
  11.  
  12.   // code snipped
  13. }

We basically loop over each row of the result, getting the number of rows by calling ‘results.rows.length’. We grab an individual row item by calling ‘result.rows.item(i)’. Once we have a SQL row result, we can treat it as a normal JavaScript object, where each column just becomes a JavaScript member property that we can fetch. For example, we can grab the timestamp property by calling row['timestamp'] or row.timestamp.

We now see where our JavaScript getters and setters come in handy; we basically just copy over the value of a row into a new Note object, and set each one like a normal property, such as note.text = row['note']. Under the covers our JavaScript object takes this value and updates the DOM and UI based on the new value.

Inserting a new Note into our table works in a similar way. Here is a snippet of code from the saveAsNew function:

javascript
< view plain text >
  1. db.transaction(function(tx) {
  2.   tx.executeSql("INSERT INTO WebKitStickyNotes "
  3.                + "(id, note, timestamp, left, top, zindex) "
  4.                + "VALUES (?, ?, ?, ?, ?, ?)",
  5.                [note.id, note.text, note.timestamp, note.left,
  6.                 note.top, note.zIndex]);
  7.         });

Basically, we get a transaction object again. Once we have that asynchronously we execute a standard SQL INSERT statement. We provide all of the new values using question marks in the SQL, and then provide the values to fill in as a separate JavaScript array. We again see our JavaScript getters making the code cleaner and more encapsulated to easily grab the values for inserting into the database, even if doing so might involve actually reading them from the DOM.

Creating our initial table and updating existing rows is roughly the same and can be seen yourself by looking at the sticky notes demo.

Conclusion

At this point you've seen a nice example that uses CSS Transitions to create a fancy UI; modern JavaScript coding including using the new getter and setter syntax; working with the HTML5 Application Cache to create offline web apps; and using the HTML5 Database to store your data. Have fun!

Related Content:

Posted by Brad Neuberg at 7:30 am
7 Comments

+++--
3.1 rating from 65 votes

7 Comments »

Comments feed TrackBack URI

This is a nice article and series, with lots of useful illustrations of these techniques. Can’t wait for more.

Here’s another take on the stickies idea: http://websticki.es/

Comment by smith — October 14, 2009

Great article ! great webkit !
Thanks.

Comment by MrPaipai — October 14, 2009

I love these informative posts, please keep ‘em coming :)

Comment by someguynameddylan — October 14, 2009

A lot of work went into that article. Thanks Brad!

Comment by AnM8tR — October 14, 2009

what is the Yiddish coming in here?

Comment by adardesign — October 14, 2009

Word of warning: If you’re using FF 3.5 and you click in the demo area, you’ll get some truly funky behavior. After encountering an error message with a spinner that wouldn’t stop until I tried the escape key, I now see a “ghosted” image of the two sticky notes in the middle of my browser. It doesn’t go away when I switch tabs or navigate to other sites, it’s like it’s permanently etched into the browser window. So now I have to restart my browser to be able to use it again effectively.

I’d highly recommend implementing some old-fashioned browser detection here, or at least moving the warning up to somewhere before the demo box itself. Maybe at one point the demo degraded gracefully, but it sure doesn’t do so in FF 3.5.

Comment by brianharper — October 14, 2009

Small usability issue, trying to select text within the notes is incredibly painful, as instead of selecting the text, the note moves.

Comment by FofR — October 19, 2009

Leave a comment

You must be logged in to post a comment.