Search This Blog

Thursday, February 26, 2009

How to Easily Combine, Minify, and Cache JavaScript and CSS with 1&1 (And Other Web Hosts)


An Opening Word

In How to Compress Perl/CGI-Scripts... and How to Compress PHP and Other Text-Based Files..., I write about achieving better standards according to Yahoo’s Best Practices for Speeding up Websites guide.

If your server supports PHP5, now there is an incredibly easy way to minify both JavaScript and CSS files on the fly, PLUS a way to combine a number of different files into a single HTTP request. (See Step 4 of my blog entry on compressing PHP scripts to convert your site to PHP5, particularly if you are using a 1&1 shared hosting package.)

Thanks to Ryan Grove and Steve Clay, Minify! is now here (download link). As the source page on GoogleCode suggests, it combines, minifies, and caches JavaScript and CSS files on demand to speed up page loads.

Setup and Basic Use

Just download the zip file and drop the min folder in your root directory.

Then, to include single resources on your page, just set them up like the following with ?f= (f short for "file"), followed by the URL of the resource:
// css
<link rel="stylesheet" href="/min/?f=/path_to_file/file.css" type="text/css" />
// js
<script type="text/javascript" src="/min/?f=/path_to_file/file.js"></script>
You can also use URL-rewriting if your host supports it (see Note below):
// js -- notice the "?" before the "f" has been dropped
<script type="text/javascript" src="/min/f=/path_to_file/file.js"></script>
Minify comes with its own URL-builder for easily creating and checking links which you can access at http://name_of_your_site_here.com/min/index.php once you have it installed on your server.

The builder interface itself will teach you how to serve groups of files as a single, minified resource by adding a few lines to /min/groupsConfig.php, but we'll go ahead and look at it here anyway:
// In /min/groupsConfig.php, I set up the unique identifier "js"
// to specify a group of 5 hypothetical JS files
return array(
    'js' => array(
        '//js/file1.js',
        '//js/file2.js',
        '//js/file3.js',
        '//js/file4.js',
        '//js/file5.js'
    )
);
You can then include this group of files in your HTML tag by specifying g (for "group"), followed by your unique js identifier.
<script type="text/javascript" src="/min/g=js"></script>
Or you can take full advantage of comma-delimited arrays and set up as many sets of files paired with unique identifiers as you want (of course including any CSS files using the link tag, as shown in the first example on this page):
// "js" and "css" file groups set in /min/groupsConfig.php
return array(
    'js' => array(
        '//js/file1.js',
        '//js/file2.js',
        '//js/file3.js',
        '//js/file4.js',
        '//js/file5.js'
    ),
    'css' => array(
        '//css/file1.css',
        '//css/file2.css',
        '//css/file2.css'
    )
);

Setting a Long Cache Life for Your Pages

Minify's default cache life is 30 minutes for CSS and JS files. This works well for development servers, but for production servers with stable code, a longer cache time is recommended. Your can easily implement a year-long cache life in the HTML simply by appending a number (yes, any number) as a query string variable.
// css
<link rel="stylesheet" href="/min/g=css&1" type="text/css" />
// js
<script type="text/javascript" src="/min/g=js&1"></script>
However, you must update this number if you make changes to these files and it is important that users have the latest versions. For this reason, I recommend starting off with the number 1 and incrementing it as your changes dictate, though as long as the numbers are always different, it makes no difference how you do it.

Since this approach requires manual editing, this may not be optimal for you. For the full spectrum of caching possibilities, see the developer doc HttpCaching; for advanced options of specifying a different cache system (as well as other advanced uses) see the CookBook doc.

For more general use ideas, including how to specify a b for "base directory" when a group of single files all share the same root directory path, see the developer README file beginning with the heading "Minifying a Single File"; for advanced usage beyond the scope of the examples here, see the developer comments at CustomSource.

Note: If you want to use rewritten urls and you are using a Linux shared hosting package with 1&1 (and maybe even if you're not), you will probably need to make a small adjustment to the included .htaccess file in the min directory. The line in the native code you're after reads as follows:
RewriteRule ^([a-z]=.*)  index.php?$1  [L,NE]
but you will probably need to change it:
RewriteRule ([a-z]=.*) /min/index.php?$1 [L,NE]
Also to point out the obvious, make sure if you put the min directory a level deeper (see below), you specify the sub-directory: for example, /myscripts/min/index.php.

Note on Use Inside A Sub-Directory

By default, Minify is intended to have the min directory parked at the root level. It works just fine a directory level deeper, however, though this sometimes causes problems with the URL-building interface. The developer docs recommend a solution that uses PHP; see AlternateFileLayouts.

Update: I also ran into a small issue when minifying HTML: see the gray box at the end of How to Minify HTML using PHP and Minify (including final HTML output of PHP files) for the work-around.

If you prefer to hatch a solution to the builder interface in JavaScript, you'll need to modify the code slightly. Use your IDE or text editor to open the _index.js file found in the builder directory.

The first block of code currently reads as follows; our interest here all deal with _minRoot:
var MUB = {
    _uid : 0
    ,_minRoot : '/min/?'
    ,checkRewrite : function () {
        var testUri = location.pathname.replace(/\/[^\/]*$/, '/rewriteTest.js').substr(1);
        function fail() {
            $('#minRewriteFailed')[0].className = 'topNote';
        };
        $.ajax({
            url : '../f=' + testUri + '&' + (new Date()).getTime()
            ,success : function (data) {
                if (data === '1') {
                    MUB._minRoot = '/min/';
                    $('span.minRoot').html('/min/');
                } else
                    fail();                
            }
            ,error : fail
        });
    }
Replace this with the following code, which is not particularly pretty, but is "smart" and will read the location of the directory when the script is called, no matter how many levels deep. The three changes are marked below by the comment // here.
var MUB = {
    _uid : 0
    ,_minRoot : location.pathname.replace('/builder/', '') + '?' // here
    ,checkRewrite : function () {
        var testUri = location.pathname.replace(/\/[^\/]*$/, '/rewriteTest.js').substr(1);
        function fail() {
            $('#minRewriteFailed')[0].className = 'topNote';
        };
        $.ajax({
            url : '../f=' + testUri + '&' + (new Date()).getTime()
            ,success : function (data) {
                if (data === '1') {
                    MUB._minRoot = MUB._minRoot.replace('?', ''); // here
                    $('span.minRoot').html(MUB._minRoot); // here
                } else
                    fail();                
            }
            ,error : fail
        });
    }

Copy to Clipboard Functionality with JavaScript and Flash 10: Creating a Loop to Instantitate Multiple Copy Buttons


As a security precaution, JavaScript is not allowed to read from or write to the system clipboard. Up until Flash 10 came out with a new security feature that required copying to the clipboard to be instantiated directly from a user clicking on a Flash control, it was a simple thing to download a tiny Flash file, include it invisibly on a web page, and send it information intended for the clipboard via JavaScript. When Flash 10 came out, however, that functionality was broken.

Not any more. There is a project on GoogleCode developed by Joseph Huckaby known as zeroclipboard that is also backwards compatible with Flash 9:
The Zero Clipboard library provides an easy way to copy text to the clipboard using an invisible Adobe Flash movie, and a JavaScript interface. The "Zero" signifies that the library is invisible and the user interface is left entirely up to you.

This library is fully compatible with Flash Player 10, which requires that the clipboard copy operation be initiated by a user click event inside the Flash movie. This is achieved by automatically floating the invisible movie on top of a DOM element of your choice. Standard mouse events are even propagated out to your DOM element, so you can still have rollover and mouse down effects.
To date, adding the functionality to multiple controls requires you to call multiple instances, unless the size of the control remains constant. Joe put together a demo version of the latter using JQuery on this page.

I also put together a non-JQuery technique using straight JavaScript below. The code is heavily commented to explain what I am doing in each step and where everything goes.

Step 1: Find the "glue" function and space it down one space to open a blank line. Paste the following "addon" function (including ending comma!) directly above the "glue" function in the space you opened--it recreates only that portion of the "glue" function that we still need to get hover effects to work properly.
addon: function (elem) {
    this.domElement = ZeroClipboard.$(elem);
},
Step 2: Replace the original ZeroClipboard.Client() function with the following, which remembers the unique ID of each copy button (that's what the "divId" is about):
// modified ZeroClipboard.Client() function
Client: function (elem, divId) {
    // constructor for new simple upload client
    this.handlers = {};

    // unique ID
    this.id = ZeroClipboard.nextId++;
    this.movieId = 'ZeroClipboardMovie_' + this.id;

    // register client with singleton to receive flash events
    ZeroClipboard.register(this.id, this);

    // create movie
    if (elem) this.glue(elem);

    // if passed, register the unique div id
    if (divId) this.divId = divId;
}
Step 3: The following init() function is meant to replace the existing one. It loops through all objects of a given type looking for a particular class. Your HTML page will need to assign both a common class and a unique ID to these elements in order for it to work.

Example:
<div id="unique1" class="common">Copy!</div>
<div id="unique2" class="common">Copy!</div>
and so on.

Make sure you change the SOME_CLASS_NAME variable to whatever common class name you are using. As with the original script, you will also need to determine what text to copy and work out how that will be performed.
function init() {
    // set the SOME_CLASS_NAME variable to whatever common class you are
    // using. Our example above uses the class "common"
    var SOME_CLASS_NAME = 'common';
    // Gather together all the elements we're interested in 'divs' variable
    // (could be any element including '*' to grab all elements), and
    // initialize 'node' variable.
    var divs = document.getElementsByTagName('div'),
        node = '';

    // This kind of unorthodox looping has been shown to be faster with
    // benchmarking tests when looping over DOM elements
    // (http://blogs.sun.com/greimer/entry/best_way_to_code_a). Note that
    // 'node' is the unique instance of the container in each loop.
    for (; node = divs[i++];) {

        // If the elements we've collected together have the class
        // 'SOME_CLASS_NAME' set, we want them. We need to
        // ensure that they have a unique id specified as well.
        if (node.className && /SOME_CLASS_NAME/.test(node.className)) {

            // We read the id of the element automatically (the reason a unique ID
            // needs to be set in the first place).
            var buttonId = node.id;
            // We send the newly read ID for registration in the modified
            // ZeroClipboard.Client function shown above.
            clip = new ZeroClipboard.Client('', buttonId);
            // We take advantage of new 'addon' function above.
            clip.addon(buttonId);
            clip.setHandCursor(true);
            clip.addEventListener('mouseOver', function (client) {

                // 'client' is the unique instance of our movie container, so we need it
                // to prefix the setText function. (Our example below uses the instantiated
                // id of the copy button div to grab its value--$(client.divId).value
                // --though it is not likely that this will be the copy element we want.
                // A little ingenuity is probably in order here for the setText text.) 
                client.setText($(client.divId).value);

                // Here we pass the copy button id on mouseover.
                ZeroClipboard.$(buttonId).addClass('hover');
            });
            clip.addEventListener('mouseOut', function (client) {
                // We again pass the copy button id on mouseout.
                ZeroClipboard.$(buttonId).removeClass('hover');
            });

            // We read/set button_width and button_height.
            var button_width = $(buttonId).offsetWidth,
                button_height = $(buttonId).offsetHeight;

            // We create a floating DIV...
            var div = document.createElement('div'),
                zIndex = 99;
            // Check the z-index...
            if ($(buttonId).style && $(buttonId).style.zIndex) {
                zIndex = parseInt($(buttonId).style.zIndex) + 1;
            }
            div.style.position = 'absolute';
            div.style.left = 0;
            div.style.top = 0;
            div.style.width = button_width + 'px';
            div.style.height = button_height + 'px';
            div.style.zIndex = zIndex;
            // We need the button id so that we can append its movie instance.
            $(buttonId).appendChild(div);
            // All finished! :)
            div.innerHTML = clip.getHTML(button_width, button_height);
        }
    }
}

Opening/Unblocking .CHM Help Files on Windows XP SP2 or Vista


In one of my coding projects, I created a .chm help file, and loaded it to the server. However, it didn't survive the round trip: when I attempted to open a downloaded copy in Windows Vista, I saw the following message (click to view full size):


I did not for certain what was wrong, though I suspected it was some kind of security issue on Vista. I was correct.

.chm files that have been downloaded from the Internet are considered a security risk on both Windows XP and Vista. Bill Evjen blogs about the solution here; the solution, as he reports, is simple. Just right-click the file, select Properties, and then click Unblock (click to view full size):

Saturday, February 14, 2009

Getting the HTML of a Range Created in JavaScript


Working on a mainly JavaScript-driven rich-text editor for the discussion forum on Mr. Renaissance, I wanted to include a feature that automatically inserted block quotations. Two days after the editor's debut, I suddenly noticed that this feature was losing its formatting: all line breaks and markup were gone, in its place one large, crunched-together paragraph that even ran sentences together in places.

A bit of Googling revealed that using range to retrieve the user's selection returns plain text (at least in non-IE browsers), not its formatted HTML:
The contents of userSelection

The userSelection variable is now either a Mozilla Selection or a Microsoft Text Range object. As such it grants access to all methods and properties defined on such objects.

However, the Mozilla Selection object that userSelection refers to in W3C-compliant browsers also contains the text the user has selected (as text, not as HTML).

From Introduction to Range, emphasis mine.
Frustrating, to say the least. I won't go into a list of all the things I tried, but I finally found the following (slightly modified) snippet of code at Snipplr, courtesy of David King of the entirely open-source OOPStudios in Newcastle, UK:
var getSelectionHTML = function() {
    var s;
    if (window.getSelection) {
        // W3C Ranges
        s = window.getSelection();
        // Get the range:
        if (s.getRangeAt) var r = s.getRangeAt(0);
        else {
            var r = document.createRange();
            r.setStart(s.anchorNode, s.anchorOffset);
            r.setEnd(s.focusNode, s.focusOffset);
        }
        // And the HTML:
        var clonedSelection = r.cloneContents(),
            div = document.createElement('div');
        div.appendChild(clonedSelection);
        return div.innerHTML;
    } else if (document.selection) {
        // Explorer selection, return the HTML
        s = document.selection.createRange();
        return s.htmlText;
    } else {
        return '';
    }
};
I am pleased to report that David has created a wonderful little function that returns the HTML of the user's selection rather than just plain, unformatted text.

Note: If you want to simulate the action of Cut (rather than Paste), change r.cloneContents() to r.extractContents(). Normally, there would be no reason to do so, but it is a possibility.

Friday, February 13, 2009

"invalid range in character class": JavaScript Error Solved


"invalid range in character class"

It was maddening. Firebug showed this error, but gave no additional information: not even a script or function name, much less a line number. Googling didn't help very much: it suggested that it was an error involving regular expressions. Thousands and thousands of lines of code and at least half a dozen external JavaScript files later, I'm pleased to say that I discovered what the message means and now want to pass along the knowledge in hopes of saving someone else endless hours.

The short answer is that the word range refers to a "range of values," say the range from 1 to 100 typically written as 1-100. Notice that my shorthand for range involves a dash or minus sign between the 1 and 100. Therein lies the problem. If you want a regular expression to test for the presence of a dash/minus sign as a dash/minus sign, it needs to be escaped with a backslash, or else some browsers (Firefox 2 in this case) think that you are attempting to express a range instead and freak out accordingly.

In particular, I had one tiny function that was used on only one form dealing with phone numbers. Valid data included a blank space, any number, a dash, or a right or left parenthesis to accommodate a number such as (555) 555-5555. It wasn't crucial that users type in their phone number in any certain way, I just try to get in the practice of filtering my data both client and server side.

In a test copy of the production environment, I first eliminated my scripts one by one until the offending error disappeared. Then I brought that script back, eliminating each function body until the error disappeared. It turned out to be this tiny function, short for "is integer" though obviously adding the space, dash, and parentheses:
var isInt = function(s) {
    s.value = s.value.replace(/[^\d-()\s]+/g, '');
};
Notice that the - character is not escaped. Now here's the fix:
var isInt = function(s) {
    s.value = s.value.replace(/[^\d\-()\s]+/g, '');
};
I have now added the escape \-, and thousands of lines of code are now working again. Hopefully this post will save someone else hours of frustration.

Update: As Earl suggests in his comment below, you can also move the dash to the end of your regular expression square brackets without using an escape: /[^\d()\s-]+/g. 5n1p3r also brings up the point below that if you're using the RegExp object and an escape, you need two backslashes, the first to escape your escape.