Submit your widget

jQuery Desktop

Created 13 years ago   Views 22829   downloads 4528    Author sonspring
jQuery Desktop
View DemoDownload
239
Share |

CSS

 

The rest of the eye-candy is handled via CSS, which involved extensive use of z-index to get things positioned just so. It probably goes without saying, but this demo obviously will bomb in IE6, but works just fine in IE7 and IE8.

While I don’t want to bore you with all the details, especially when this code is freely available to peruse, I did want to call out a few specifics. First off, I attempted to write modular styles, a technique that has been dubbed object oriented CSS by Yahoo web developer extrodinaire Nicole Sullivan.

When I first saw her presentation on OO concepts in CSS, I turned to my coworker and said: “That’s what we’ve been doing. Now there’s a name for it.” This was the approach I used when building the 960 Grid System. While there are no grids in this demo, here’s an example of what I’m talking about…

 

.abs {
    position: absolute;
    top: auto;
    left: auto;
    right: auto;
    bottom: auto;
}

#bar_top,
#bar_bottom {
    left: 0;
    right: 0;
}

#bar_top {
    top: 0;
}

#bar_bottom {
    bottom: 0;
}

 

 

Example HTML

<div class="abs" id="bar_top">...</div>

 

 

Because it is often a pain to type out repetitious style declarations, and easy to forget to define something that you are not targeting specifically, using an OO approach helps to avoid extra typing and mitigates forgetfulness.

In the case of this demo, I make sure to always define auto if not setting a specific value for absolutely positioned elements. In my experience, IE doesn’t do too well if those are left to chance. This ensures it renders correctly.

Progressive Enhancement

For those browsers that support it, namely Firefox and Safari, you will notice there a slight visual enhancements, such as rounded corners. Also, when hovering over a window controls – like minimize, resize, close – you will notice a slight glow via box-shadow. Here’s a comparison, zoomed 200%…

 

Layers

There are essentially four planes at work, to sell the desktop effect. As shown in the diagram below, the lowermost layer is the wallpaper, above which sit the top bar, desktop layer, and the bottom bar. The upper layers consists of application icons and windows, which are held within the desktop.

This layering is achieved mostly by the order in which elements appear in the markup, with z-index offering specific guidance when sibling elements need to overlap each other. Astute readers may view-source and notice the absence of the aforementioned wallpaper layer. This oddity will be explained shortly.

HTML5

It should be noted that this demo would work just as well served in HTML 4.01 or XHTML 1.0, because I didn’t make use of any new elements like <header>, <footer>, <article> etc. In my opinion, these new tags are very specific to newspaper sites, and offer little use for building web applications.

That’s okay though, because good ol’ <div> tags still work just fine, and in all browsers. Setting those nitpicks aside (har har), nowadays I prefer to use HTML5 on all my projects, because it allows for more terseness, and increased productivity, by getting rid of needless code requirements. For instance…

XHTML 1.0
<script type="text/javascript">
/* <![CDATA[ */
// Code here.
/* ]]> */
</script>


HTML 4.01
<script type="text/javascript">
// Code here.
</script>


HTML5
<script>
// Code here.
</script>

 

Javascript

Think about it. When is the last time you wrote code inside a <script> tag to be interpreted in the browser, but it wasn’t JavaScript? Same goes for CSS includes as well. No more typing the obvious: type="text/css".

But, I digress. I think we can all agree that HTML5 is better than any of its predecessors, especially because it can be served as either HTML or XHTML. So, let’s get into the crux of this demo: JavaScript. More specifically, the use of the awesome jQuery library and its trustworthy counterpart jQuery UI.

Though jQuery UI offers a variety of rich interface components, I opted to do my own visual look and feel, using only the draggable and resizable functionality. By using jQuery and jQuery UI, the amount of code I actually had to write myself was reduced considerably. Let’s delve into what makes this demo tick.

First off, we define an empty object to be used as a pseudo namespace – This technique is commonly referred to as the Module Pattern in JavaScript.

var JQD = (function($) {
    return {
        // Code here.
    }
})(jQuery);

 

 

By passing jQuery into the self-executing closure function, we can safely use the dollar sign ($) without worrying about potential collisions from other code that might make its way into the “desktop” environment. Plus, it keeps our code from conflicting with others’ since everything lives within the JQD object.

While there is certainly room for argument over whether this is the ultimate JavaScript programming pattern, it is one way to keep variable scope tidy. For more, I’d highly recommend this book – Pro JavaScript Design Patterns.

The first function, JQD.init_clock begins by creating a new Date object. From there, we can then derive all we need to know about the current month, day, year, and time. This function calls itself once every 60 seconds, to ensure that the clock stays current. We could be more precise by invoking it once per second, but that would have a greater impact on page performance.

 

// Update every 60 seconds.
setTimeout(JQD.init_clock, 60000);

 

 

The next two functions, JQD.clear_active and JQD.window_flat are simply utilities that have been abstracted out to their own methods, for the sake of reuse. One clears elements’ active state and closes open menus. The other lowers the z-index of all windows, so one can be given prominence.

That brings us to the JQD.window_resize function, which is responsible for maximizing and restoring the dimensions / position of the windows. Look over the code below, then read on for a step by step explanation…

//
// Resize modal window.
//
window_resize: function(el) {
    // Nearest parent window.
    var win = $(el).closest('div.window');

    // Is it maximized already?
    if (win.hasClass('window_full')) {
        // Restore window position.
        win.removeClass('window_full').css({
            'top': win.attr('data-t'),
            'left': win.attr('data-l'),
            'right': win.attr('data-r'),
            'bottom': win.attr('data-b'),
            'width': win.attr('data-w'),
            'height': win.attr('data-h')
        });
    }
    else {
        win.attr({
            // Save window position.
            'data-t': win.css('top'),
            'data-l': win.css('left'),
            'data-r': win.css('right'),
            'data-b': win.css('bottom'),
            'data-w': win.css('width'),
            'data-h': win.css('height')
        }).addClass('window_full').css({
            // Maximize dimensions.
            'top': '0',
            'left': '0',
            'right': '0',
            'bottom': '0',
            'width': '100%',
            'height': '100%'
        });
    }

    // Bring window to front.
    JQD.window_flat();
    win.addClass('window_stack');
},

 

First, we assign a variable to the nearest parent window. jQuery’s closest() method keeps looking up the DOM tree until a parent window is found. We then check for the existence of the class name window_full and if it exists, we get the window’s former dimensions from its respective HTML5 data-* attributes.

If the window_full class was not present, then we know the window not maximized, so we store the current dimensions in data-* attributes, and set its top, left, right, bottom values to zero – and width, height to 100%. Lastly, we push down all windows, then bring the active window to the top.

The last main function, JQD.init_desktop instantiates the clock, and does the heavy-lifting of wiring up all the JS event listeners. I will break each of these out into snippets, so that we can evaluate them in digestible chunks.

 

The first involves listening to events on the document itself. The mousedown event is the first half of a click event – triggered when the mouse button is pushed, but has not yet been released – which is when a click is registered. When mousedown occurs anywhere in the document, all active elements are cleared, and menus are hidden – mimicking desktop functionality.

We also cancel right-click by binding a listener to the contextmenu event. This would be the first step in creating a context menu when the desktop is clicked, but I didn’t go this far because Opera does not allow for right-click cancellation. Instead, it always shows the browser default right-click menu.

// Cancel mousedown, right-click.
$(document).mousedown(function(ev) {
    if (!$(ev.target).closest('a').length) {
        JQD.clear_active();
        return false;
    }
}).bind('contextmenu', function() {
    return false;
});

 

The next event listener is bound to all links, awaiting a user’s click and then determining if the link is external or internal to the page. Because our desktop demo is a web page, and therefore essentially stateless, it is important not to break the metaphor by having links that cause the page to refresh.

That would obviously spoil the illusion. It is for this reason that apps like Gmail and Google Calendar pop external links into a new browser window/tab, and why we’re doing the same via a dynamically added target="_blank".

// Relative or remote links?
$('a').click(function() {
    var url = $(this).attr('href');
    this.blur();

    if (url.match(/^#/)) {
        return false;
    }
    else if (url.match('://')) {
        $(this).attr('target', '_blank');
        return true;
    }
});

 

Next up, more link handling. In this scenario, we’re looking for a mousedown event triggered on the top menu bar, ala Mac OS X. This opens the corresponding menu, and if the user moves his/her mouse over another menu trigger, any other open menus are closed, and the new one is opened.

// Make top menus active.
$('a.menu_trigger').mousedown(function() {
    JQD.clear_active();
    $(this).addClass('active').next('ul.menu').show();
}).mouseenter(function() {
    // Transfer focus, if already open.
    if ($('ul.menu').is(':visible')) {
        JQD.clear_active();
        $(this).addClass('active').next('ul.menu').show();
    }
});

 

What desktop would be complete without icons, the hallmark of a true GUI application? The next scenario binds event listeners for mousedown and double-clicks, on links with class="icon". It also makes the icons moveable via the jQuery UI draggable() function, restricted to the #desktop.

Also take note a DOM scripting technique I like to refer to as “link chaining.” Basically, each icon is just a link pointing to its respective taskbar button, ala Windows OS. In turn, each of those is a link pointing to a corresponding modal window. When an icon is double-clicked, its taskbar button slides into place, and the button’s link target window is shown and brought to the forefront.

// Desktop icons.
$('a.icon').mousedown(function() {
    // Highlight the icon.
    JQD.clear_active();
    $(this).addClass('active');
}).dblclick(function() {
    // Get the link's target.
    var x = $($(this).attr('href'));
    var y = $(x.find('a').attr('href'));

    // Show the taskbar button.
    if (x.is(':hidden')) {
        x.remove().appendTo('#dock').end().show('fast');
    }

    // Bring window to front.
    JQD.window_flat();
    y.addClass('window_stack').show();
}).draggable({
    revert: true,
    containment: 'parent'
});

 

Also like in Windows/Linux, each taskbar button causes its respective window to be shown or minimized, depending on the state of the window. In this case, we are using the live() method because the taskbar buttons themselves are spliced and reintroduced into the DOM when their desktop icons are clicked, via remove() and appendTo().

This ensures the most recently opened window has its taskbar button placed furthest to the right, but it breaks any bound event listeners. What the live() method actually does is listen for clicks on the parent element, in this case the #dock and then asks: “Wait, was that actually a click on the dock’s child link?” If so, it takes the appropriate action on the link’s target window.

// Taskbar buttons.
$('#dock a').live('click', function() {
    // Get the link's target.
    var x = $($(this).attr('href'));

    // Hide, if visible.
    if (x.is(':visible')) {
        x.hide();
    }
    else {
        // Bring window to front.
        JQD.window_flat();
        x.show().addClass('window_stack');
    }

    // Stop the live() click.
    this.blur();
    return false;
});

 

The following example reveals a window on mousedown, if it happens to be obscured by other open windows. Each window is draggable via its title bar, and resizable via its bottom-right corner. Also, a minimum width and height are defined, and windows are confined to their parent element – #desktop.

We then traverse a bit further, via the find() method, attaching a double-click event handler to the window’s title bar, which will maximize / resize the window. Then, we go another level deep via find() again, this time to the 16×16 icon in the title bar, wiring up a double-click handler to close the window if triggered.

// Make windows movable.
$('div.window').mousedown(function() {
    // Bring window to front.
    JQD.window_flat();
    $(this).addClass('window_stack');
}).draggable({
    // Confine to desktop.
    // Movable via top bar only.
    containment: 'parent',
    handle: 'div.window_top'
}).resizable({
    containment: 'parent',
    minWidth: 400,
    minHeight: 200

// Double-click top bar to resize, ala Windows OS.
}).find('div.window_top').dblclick(function() {
    JQD.window_resize(this);

// Double click top bar icon to close, ala Windows OS.
}).find('img').dblclick(function() {
    // Traverse to the close button, and hide its taskbar button.
    $($(this).closest('div.window_top').find('a.window_close').attr('href')).hide('fast');

    // Close the window itself.
    $(this).closest('div.window').hide();

    // Stop propagation to window's top bar.
    return false;
});

 

In order for windows to be minimized, resized, or closed, we assign event handlers to the respective buttons. Also, one for mousedown that affects all three buttons. Regardless of what action is taken, we want to clear all active elements, and then return false to keep the mousedown from propagating to the parent title bar. Else, the window would be draggable by its action buttons, a weird behavior indeed.

// Get action buttons for each window.
$('a.window_min, a.window_resize, a.window_close').mousedown(function() {
    JQD.clear_active();
    // Stop propagation to window's top bar.
    return false;
});

// Minimize the window.
$('a.window_min').click(function() {
    $(this).closest('div.window').hide();
});

// Maximize or restore the window.
$('a.window_resize').click(function() {
    JQD.window_resize(this);
});

// Close the window.
$('a.window_close').click(function() {
    $(this).closest('div.window').hide();
    $($(this).attr('href')).hide('fast');
});

 

For those familiar with Windows/Linux, “show desktop” is a recognizable button, used to minimize all windows simultaneously, allowing for an unobstructed view of the desktop. The same is true with our demo. If one or more windows are visible, we assume the user wants to see the desktop, and all windows are minimized. Otherwise, if all windows are minimized, then all are restored.

// Show desktop button, ala Windows OS.
$('#show_desktop').click(function() {
    // If any windows are visible, hide all.
    if ($('div.window:visible').length) {
        $('div.window').hide();
    }
    else {
        // Otherwise, reveal hidden windows that are open.
        $('#dock li:visible a').each(function() {
            $($(this).attr('href')).show();
        });
    }
});

 

When the page loads, the table within each window is given alternating zebra stripes, ala Mac OS X. Also, when a window is open and the user clicks on a row of data about a particular file, we highlight the entire row. This behavior is fairly typical across all operating systems, so it was worth implementing.

$('table.data').each(function() {
    // Add zebra striping, ala Mac OS X.
    $(this).find('tr:even td').addClass('zebra');
}).find('tr').live('click', function() {
    // Highlight row, ala Mac OS X.
    $(this).closest('tr').addClass('active');
});

 

Lastly, we load the desktop wallpaper. The reason for this is simple: To avoid blocking the loading of other assets, such as images or JavaScript. You see, since the #wallpaper is the bottommost of all our layered planes, it needs to come first in the markup, immediately after the opening <body> tag.

In order to do this, we simply use jQuery’s prepend() method, and pass in the markup for our wallpaper image. This allows us to splice it in as the first child element of the document.body, placing it absolutely lowest in the stack.

// Add wallpaper last, to prevent blocking.
$('body').prepend('<img id="wallpaper" class="abs" src="assets/images/misc/wallpaper.jpg" />');