Dumb Tinkering, Revisited

Saw this link come up online today from the formidable Chris Coyier, where he said he was doing some dumb tinkering to get the results he shows on his demo page. Well, that got me inspired. And when I get inspired, I write. Sometimes it’s stories. Sometimes it’s code. I suppose it depends on what’s inspiring me.

So, I got industrious and turned Chris’ work into a jQuery Plugin, which I have named letterScroll.

So, the basic idea was that you have a series of letters, each contained in their own separate DOM element, like the used in Chris’ example. Well, the first thing I saw when I looked at the code was that just as proof-of-concept, Chris had limited it to the same letters that were in the text.

I wanted to be able to do this on any text, not just what was in an array in the code. I want to be able to point this code at some container and say, ‘HEY! jQuery! Make this do cool stuff when people mouse over it!’ Yes. I do talk to my code. Like flowers, it blooms when you show it affection. (Sidekick: Or yer just a bloomin’ idiot! *cue laugh track*)

Realizing that I wanted to be able to point this code at any DOM element I specify, and realizing that I wanted to extend its capabilities immediately made me realize I needed to pluginize this code. If you are not familiar with jQuery pluginization, I highly recommend visiting http://www.benalman.com and learning about it.

To start off with, I needed the ability to get any arbitrary bit of text within a container. So, how do I do that? Well, jQuery has the .text() method, which returns the plaintext within any element. Okay, technically according to the API, it returns ‘a string containing the combined text of all matched elements’, which is correct and still what we want.

So, when I did this originally from Chris’ example, I used:

var text = $("#page-wrap").text();

And what I got back was rather interesting:

\n
\n
J
a
v
a
S
c
r
i
p
t
\n
\n

Not what I wanted, by a long shot. It’s one string, with a bunch of newlines. Okay, so what’s next? I need to get rid of the newlines before and after the text. Enter .trim(), it saves the day. So, now my code moves one step further and becomes:

var text = $.trim($(“#page-wrap”).text());

And now I have a very vertical list that spells out the text in the interior of my DOM element. Well, I know that the letters are separated by newlines. .trim() wouldn’t get rid of those because they are inside the string I wanted trimmed, not around it. So, now I have to revert back to good old vanilla JavaScript to get the job done. Because not only do I want to get rid of the newlines, but I want to break my string up into an array. So, I will use .split() for this.

So, now I have the following code:

var text = $.trim($("#page-wrap").text()),
    letters = text.split('\n');

That gives me an array of just the letters from my DOM element container. So, now I have an array named the same as what the rest of Chris’ code references, and it works just fine. However, I don’t have a guarantee that my HTML code will be nicely formatted with newlines (\n) between everything, so I have to normalize my input. To do that, I add the following code:

var text = $.trim($("#page-wrap").text()),
    orig_letters = text.replace(/\s+/g,''),
    letters = orig_letters.split('');

And now I just get an array of the letters, with all types of whitespace removed. I still want this pluginized, so now I’m going to take it a bit farther. Let’s set up the basic framework of our plugin with the following code:

(function($) {
    var defaults = {
    },
    options = {},
    letters = [];
    $.fn.letterScroll = function(user_options) {
        options = $.extend(defaults, user_options);
    };
})(jQuery);

That gives me the basic framework I need to start building a plugin. If you’re not familiar with any of these constructs, again go to the Ben Alman link referenced above. You’ll learn a lot. Trust me.

I’ve set up an object for storing my default plugin options, named…oddly enough, default. I’ve set up an empty object named options which I will use $.extend() on later and I have set up the letters array as global to my plugin. There is a very good reason for this, which I will show you in just a minute.

My next step is deciding what I want my default options to be. I want configurable code. Looking at Chris’ code, I decided to specify any value that is explicitly stated as a default option.

I found four things that jumped out at me immediately. The first is #page-wrap, his DOM element. Well, since I’m using this as a plugin, I’ll have access to that in $(this), so I will use it appropriately. Second is ‘span’, which is the container element that holds each block of text and onto which we will be attaching a .hover() event. So, I will set a default for the container. Third is the time between each iteration of the letters. Chris has it set to 40 in his setTimeout. What if I want the letters to go slower or faster? Well, then I need that to be configurable too. The last thing I thought about making configurable was the index at which the letters start cycling. Chris passes ’0′ to his changeLetter function, so that it starts at the first letter and moves through the list. What if I wanted to start at the third letter?

As a result, here is my new defaults object, with values and comments:

var defaults = {
'timeout'       : 40,       //Milliseconds between letter rotations
'container'     : 'span',   //Type of element the text is in
'offset'        : 0         //Offset of letters to use for scrolling
}

As you can see, our plugin is starting to take shape. So, let’s take it another step forward. I see that Chris defined a changeLetter function. Well, I want to make that as a separate method that my plugin calls, so I am going to separate it out into its own function. Without walking through each mental step of what was changed and why, I will just give you this code outright:

$.changeLetter = function(el, i) {
    el.text(letters[i]);
    if (i < letters.length) {
        setTimeout($.changeLetter, options.timeout, el, i+1);
    } else {
        el.text(el.data('orig'));
    }
};

Now you see why I wanted to make the letters array global to my plugin. This way, my changeLetter method can access it without issue. Because our plugin is within a closure, the letters array is invisible outside of it. You can also see how I have incorporated the options object into the code, to control the timeout between each letter.

So now I just need my plugin function itself. This function is going to accomplish a few things. The first is to sanitize the text inside my DOM element. The next is to use Chris’ really inventive method of going through and adding the original text of each element into a data attribute on that element. Last, but certainly not least, is adding an action that triggers the changeLetter function when you move the mouse over the element. I also tidied up using the regexp to filter out the whitespace.

Without further adieu:

$.fn.letterScroll = function(user_options) {
    options = $.extend(defaults, user_options);
var content = $(this),
orig_letters = $.trim(content.text()),
regex = /\s+/g;
orig_letters = orig_letters.replace(regex, ”);
letters = orig_letters.split(”);
content.find(options.container).each(function(i) {
        $(this).data('orig', $(this).text());
    })
    .hover(function() {
        $.changeLetter($(this), options.offset);
    });
};

So, I accept an options object from my user, and extend defaults with it, putting the result in options. Then, I grab my $(this) and store it in a semantically-correct variable. I process the plaintext as discussed above. Next, I use .find() to return a list of the container element that I’ve specified and process setting the data attribute on each one of them.

Last is the magic, where the .hover() function triggers my changeLetter, and this is where I have utilized my configurable offset. I put this all together into one nice plugin file, and viola…you can check out the Demo here.

Looking this over, I wanted to make sure my inputs were being checked so that people couldn’t just pass anything through, so I added the following check just to be a bit more robust:

options = (typeof user_options === "object") ? $.extend(defaults, user_options) : defaults;
Download sourceminified, or everything (includes demo file).

Comment if you have anything you’d like to add.