Chapter 11. Customizing the Client

Letting users change things on the client gives them the impression that they have some control over what they are using. Browser makers already allow users to modify certain aspects of the client. In many desktop applications, the user can also modify colors, fonts, and the basic positioning of objects. You can also incorporate these features into web applications.

One of the major themes of Web 2.0 is interaction between the user and the web application. In essence, this means enabling the user to modify the application in ways that affect how they interact with each other. This could also mean changing the way the application works or interacts with all users of the application. This has far-reaching implications, and you must be careful to ensure that any changes one user makes in an application will not adversely affect all other users.

Changes that affect only the local user of the web application are much safer to make, and quite frankly, are also much easier to implement. For now, we will consider changes for the local user. We will save our discussion of global application changes for later chapters.

Browser Customizations

When thinking about customizations we want to implement for the user, it’s best to first determine what the browser provides automatically. The first thing that comes to mind, especially in browsers such as Firefox, is the ability to change the browser’s theme. In addition, most browsers also enable users to change such aspects as the page’s font size, style, and character encoding. Figure 11-1 shows these user choices in a typical Firefox browser.

The View menu of a Firefox browser allows for user changes

Figure 11-1. The View menu of a Firefox browser allows for user changes

Typically, you’d find these choices under the browser’s View menu. In Internet Explorer, the user will find in the View menu choices for font size (Text Size) and character encoding. Natively, Internet Explorer does not enable users to change the style of a page. For Firefox, the View menu allows the user to change the font size (Text Size), page style, and character encoding. These choices are also available in Opera.

Stylesheets

As I said, Internet Explorer does not natively allow the user to change the style of someone else’s page. In most other browsers, you can change the page style by selecting View → Page Style in the File menu, as shown in Figure 11-2. Opera users can access stylesheets by selecting View → Style (when Opera has built-in styles besides those provided by the developer of the application or page being viewed).

The Page Style drop down found on the Firefox browser

Figure 11-2. The Page Style drop down found on the Firefox browser

Typically users can only turn stylesheets off (View → Page Style → No Style) or select one of the alternate stylesheets that the application provides. A developer can do this by using the alternate value within the rel attribute of the <link> element:

<link type="text/css" rel="alternate stylesheet" media="screen"
    href="path_to_alternate_style_sheet.css" />

The browser will also enable the user to select a user-defined stylesheet, a stylesheet that sits on the user’s machine and sets styles on the page in a user-defined way. A user-defined stylesheet will usually have the user’s sizes, colors, and fonts set the way he wants to view them. Example 11-1 shows what you might find in a typical user-defined stylesheet.

Tip

Having alternate stylesheets from which the user can choose can make a page more accessible. The alternate stylesheets should allow the user to do such things as change a colorful site to monochrome, strip all color and images from the page, or alter the page in a way that makes it easier to view. Figure 11-3 shows what it might look like when the developer has created alternate stylesheets.

The drop down with alternate stylesheets shown

Figure 11-3. The drop down with alternate stylesheets shown

Example 11-1. A typical user-defined stylesheet

/* Example 11-1.  A typical user-defined stylesheet. */

body {
    background-color: #fff;
    color: #000;
    font-family: Verdana;
    font-size: 1em;
    margin: 0;
    padding: 2px;
}

/*
 * This second body rule is to control the default size in pixels; everything will be
 * relative to this.
 */
body {
    font-size: 14px;
}

br {
    margin-bottom: 20px;
}

h1 {
    font-size: 2em;
    font-weight: bold;
}
h2 {
    font-size: 1.75em;
    font-weight: bold;
}

h3 {
    font-size: 1.5em;
    font-style: italic;
}

h4 {
    font-size: 1.25em;
    font-style: italic;
}

p {
    padding: 3px 8px;
    text-indent: 3em;
}

pre {
    background-color: transparent;
    color: #0c0;
    font-family: monospace;
}

Font Sizes

All browsers enable users to change the font size on the page they are currently viewing, typically by selecting View → Text Size in the File menu, as shown in Figure 11-4. Browsers such as Internet Explorer enable users to choose from five font sizes (Smallest, Smaller, Medium, Larger, Largest), whereas most other browsers allow users to just increment or decrement the font size through steps (percentages).

The Text Size drop down found in the Firefox browser

Figure 11-4. The Text Size drop down found in the Firefox browser

Character Encoding

All browsers also enable users to choose the character encoding on the page, typically by selecting View → Character Encoding from the File menu, as shown in Figure 11-5.

The Character Encoding drop down found in the Firefox browser

Figure 11-5. The Character Encoding drop down found in the Firefox browser

Character encoding is very important, especially in Eastern European and Asian countries, where the characters used are not the standard Western Roman ones. Some browsers even allow users to change the direction in which the characters are written to the page. This is also an important characteristic that a browser should utilize when rendering Asian character sets.

Character encoding is a user choice that, unfortunately, is not easy for developers to replicate short of creating different pages in different character encodings depending on user request. As this is not practical, it is best to allow this functionality to remain in the hands of the browser makers.

Stylesheet Switching

Stylesheet switching is a concept that has been around for almost as long as stylesheets. It is another way in which developers make their applications feel more Windows-like by giving users control over the style of the page.

Creating the Stylesheets

When you’re creating the ability to switch styles directly in an Ajax application, first you must craft the stylesheet in a way that makes it easy to switch. Unless you plan to do a lot of rework, you will have to break the CSS rules into multiple files. This method of style switching will also be easier if you set up the structure to support the different stylesheets that will be created. Figure 11-6 shows a simple structure for CSS files.

A possible structure for CSS files

Figure 11-6. A possible structure for CSS files

Obviously, this isn’t the only way to set up the structure. However, I’ve found that this structure makes it easy to quickly find the rules I’m looking for. This screen directory contains a separate file for the structure, font, and color (or theme) that will be used for the page. Inside this directory you can have two more directories: font-sizes and themes. The font-sizes directory will hold all the alternative CSS files for controlling the sizes the font can have, and the themes directory will hold any alternative color schemes the page might have.

You can find a slightly simpler setup in the print directory. This is because the developer will normally not want the end user to have as much control over how the page will be printed as she does how the pages are viewed on-screen. Therefore, there are no choices for font sizes or themes within the current directory. There are merely the three files for structure, font, and color, as there are in the screen directory.

First we’ll look at the three main files in the screen and print directories so that you can have a better understanding of what each file will contain. Once these files are set up, we can turn our attention to the alternate stylesheet files:

screen.css

The screen.css file contains all the screen CSS style rules for the following property types: boxes and layout, lists, and text. The screen CSS file holds the rules for the structure of the page and is not interested in anything that has to do with fonts or colors.

fonts.css

The fonts.css file contains all the screen CSS style rules for the font properties. This file is used to control the default font family, sizes, weights, and so forth.

colors.css

The colors.css file contains all the screen CSS style rules for the color and background property types. In this file, you set all of the page’s color attributes and background settings.

The following shows an example of what the structure.css file would contain:

body {
    line-height: 1.5em;
    margin: 0;
    padding: 0;
}

a:hover {
    text-decoration: none;
}

tr td {
    border-style: solid;
    border-width: 1px;
    padding: 2px 4px;
}

a > img {
    border-style: none;
}

p.required {
    text-align: center;
}

.f_right {
    float: right;
}

#bodyFooter {
    clear: both;
    padding: 3px;
    text-transform: uppercase;
}

Here is an example of what the fonts.css file would contain:

body {
    font-family: Arial, Helvetica, sans-serif;
    font-size: 1em;
}

a:hover {
    font-style: italic;
}

tr td {
    font-size: .9em;
}

p.required {
    font-weight: bold;
}

#bodyFooter {
    font-size: .75em;
}

Finally, here is an example of what the colors.css file would contain:

body {
    background: #fff url('../..//images/bodyBackground.png') no-repeat fixed 0 0;
    color: #000;
}

a:hover {
    background-color: transparent;
    color: #0c0;
}

tr td {
    background-color: transparent;
    border-color: #000;
    color: #000;
}

a > img {
    vertical-align: middle;
}

p.required {
    background-color: transparent;
    color: #f00;
}

#bodyFooter {
    background-color: #009;
    color: #fff;
}

There is no difference between the content of the files in the print and screen directories. What is different is the unit of measure that is used. Screen files are more likely to use px and em units, for example, whereas print files are more likely to use pt, cm, or in. The difference is in the two types of units: relative and absolute.

Tip

The relative units that CSS supports are pixels (px), x-height (ex), relative size (em), and percentage (%). The absolute units that CSS supports are centimeters (cm), inches (in), millimeters (mm), points (pt), and picas (pc). The difference is that absolute units are assumed to be the same distance across all browsers, screen resolutions, and printer faces (one inch should be one inch wherever it is used). Relative units, on the other hand, may vary because of differences in browser rendering, screen resolutions, and printer faces.

Alternate Stylesheets

Now that the default files are defined, it is time to consider what alternatives the user may need. Of course, alternate stylesheets can be used for theme switching that has nothing to do with giving the user greater accessibility. In this case, the developer merely wants to give the user different options regarding how the page looks or flows. No matter what the intention, or what the alternate stylesheets are going to do to the page, the basics on how to switch the CSS files with JavaScript will be the same.

I touched on the basic structure of an alternate stylesheet link at the beginning of the chapter. Now we will take a closer look at what we need to make main and alternate stylesheet links. Consider the following:

<link type="text/css" rel="stylesheet" media="screen" title="medium"
    href="screen/font-sizes/medium.css" />
<link type="text/css" rel="alternate stylesheet" media="screen" title="smaller"
    href="screen/font-sizes/smaller.css" />
<link type="text/css" rel="alternate stylesheet" media="screen" title="larger"
    href="screen/font-sizes/larger.css" />
<link type="text/css" rel="alternate stylesheet" media="screen" title="monochrome"
    href="screen/themes/mono

In this example, there is one main stylesheet link and three alternative ones. You must place the alternate keyword in the row attribute of the <link> element. This not only tells the browser that the link is supposed to be the alternative so as not to break browser functionality, but it also allows for easier parsing to determine the alternative ones in JavaScript. Each link also contains a title attribute which, strictly speaking, is not necessary for our JavaScript code, but is another way to make sure we are grabbing the appropriate stylesheets. More important, the title attribute is necessary for the browser to recognize that the link is an alternative link.

Tip

You can provide three different types of stylesheets for the browser: persistent, preferred, and alternate. Persistent stylesheets use the keyword stylesheet in the rel attribute but have no title attribute set; preferred stylesheets use the keyword stylesheet in the rel attribute and have a title attribute set; and alternate stylesheets, as we just discussed, have the alternate keyword in the rel attribute and do have a title attribute set. Paul Sowden wrote a great article, “Alternative Style: Working with Alternate Style Sheets,” for A List Apart in 2001 that explains this better (http://alistapart.com/stories/alternate/).

The Switching Object

We built some alternate stylesheets, and now we need to enable the user to switch between the different choices without having to rely on the functionality provided by the browser. So, if the user is going to rely on our application, we must provide the means to do the necessary switching.

It does not matter whether we provide the choices in a drop down or in a list, as long as there is an intuitive means to apply the switching functionality. The following example provides the user with a list of choices of alternate stylesheets:

<div id="styleChoicesContainer">
    <ul id="styleChoicesList">
        <li>style choices: </li>
        <li>
            <a href="setStyle.php?s=default"
                    onclick="return StyleSwitcher.setActive('default'),">
                default
            </a>
        </li><li>
            <a href="setStyle.php?s=alternate1"
                    onclick="return StyleSwitcher.setActive('alternate1'),">
                alternate 1
            </a>
        </li><li>
            <a href="setStyle.php?s=alternate2"
                    onclick="return StyleSwitcher.setActive('alternate2'),">
                alternate 2
            </a>
        </li><li>
            <a href="setStyle.php?s=alternate3"
                    onclick="return StyleSwitcher.setActive('alternate3'),">
                alternate 3
            </a>
        </li><li>
        <a href="setStyle.php?s=alternate4"
                    onclick="return StyleSwitcher.setActive('alternate4'),">
                alternate 4
            </a>
        </li>
    </ul>
</div>

It would be easy to style this list for horizontal display with a little CSS. But this does nothing until we write functionality behind the list. Example 11-2 shows the JavaScript required for us to make the list functional.

Example 11-2. A simple style-switching object

/* Example 11-2.  A simple style-switching object. */

/**
 * This object, StyleSwitcher, contains all of the functionality to get and set an
 * alternative style chosen by the user from a list provided by the application.  It
 * contains the following methods:
 *    - setActive(p_title)
 *    - getActive( )
 */
var StyleSwitcher = {
    /**
     * This method, setActive, takes the passed /p_title/ variable and sets the
     * appropriate alternate stylesheet as the current enabled one.
     *
     * @param {String} p_title The title that is to be set as active.
     * @member StyleSwitcher
     * @return Returns false so that the click event will be ignored.
     * @type Boolean
     */
    setActive: function(p_title) {
        /* Get a list of the <link> elements in the document */
        var links = document.getElementsByTagName('link'),

        /*
         * Loop through the list, setting the appropriate <link> elements to
         * disabled, and set the <link> element with the title attribute equal to
         * /p_title/ to active.
         */
        for (var i = links.length; i > 0;) {
            /* Get the current <link> element */
            var iLink = links[i--];

            /* Is this element an appropriate stylesheet to mark? */
            if (iLink.getAttribute('rel').indexOf('style') != -1 &&
                    iLink.getAttribute('title')) {
                iLink.disabled = true;
                /* Is this element the one we are looking for? */
                if (iLink.getAttribute('title') == p_title)
                    iLink.disable = false;
            }
        }
        /* Set the cookie to the passed /p_title/ variable, and do not let it expire
         * for one year.
         */
        Cookie.set('appStyle', p_title, 365);
        return (false);
    },
    /**
     * This method, getActive, returns the current active stylesheet node (<link>
     * element) in the document, provided that there is one to return.
     *
     * @member StyleSwitcher
     * @return Returns the active stylesheet node, if one exists.
     * @type Node
     */
    getActive: function( ) {
        /* Get a list of the <link> elements in the document */
        var links = document.getElementsByTagName('link'),

        /*
         * Loop through the list until the active stylesheet is located, then
         * return it.
         */
        for (var i = links.length; i > 0;) {
            /* Get the current link element */
            var iLink = links[i--];

            /* Is this the currently active <link> element? */
            if (iLink.getAttribute('rel').indexOf('style') != -1 &&
                    iLink.getAttribute('title') && !iLink.disabled)
                return (iLink.getAttribute('title'));
        }
        return (null);
    }
};

Now when the user clicks on one of the choices on our list, the active stylesheet will change to the corresponding <link> element located in our document’s <head> element. Figure 11-7 and Figure 11-8 demonstrate what dynamic changing of stylesheets might look like.

The original page before switching styles

Figure 11-7. The original page before switching styles

The same page once the styles have switched

Figure 11-8. The same page once the styles have switched

Remembering the User’s Selection

Our style switcher is fine, but it only changes the style for the user for the currently active session. The next time the user visits the page, the page will default to the stylesheet the developer chose, and not what the user had selected. We need a way to save the user’s choice between browser sessions. And of course, the use of cookies will do the trick. Example 11-3 shows an easy cookie object that you can implement in a page.

Example 11-3. cookie.js: A simple cookie object

/**
 * @fileoverview Example 11-3.  cookie.js: A simple cookie object.
 *
 * This file, cookie.js, contains a simple cookie object that can be used to get and
 * set a cookie as well as erase cookies and check to see if a given browser even
 * supports cookies.
 */

/**
 * This object, Cookie, is simply a mechanism to allow for easier access of the
 * document.cookie object for the page.  It contains the following methods:
 *    - set(p_name, p_value, p_expires)
 *    - get(p_name)
 *    - erase(p_name)
 *    - accept( )
 */
var Cookie = {
    /**
     * This method, set, creates a cookie with the name equal to /p_name/ with a
     * value of /p_value/ that expires at the specified /p_expires/ should it
     * exist, returning whether the cookie was created or not.
     *
     * @member Cookie
     * @param {String} p_name The name for the cookie to be set.
     * @param {String} p_value The value for the cookie to be set.
     * @param {Float} p_expires The time before the cookie to be set expires.
     * @return Returns whether the cookie was set or not.
     * @type Boolean
     */
    set: function(p_name, p_value, p_expires) {
        /* The expires string for the cookie */
        var expires = '';

        /* Was an expires time sent to the method? */
        if (p_expires != undefined) {
            /* Get a base time for the expiration date */
            var expirationDate = new Date( );

            /* Set the expiration to one day times the passed /p_expires/ value */
            expirationDate.setTime(expirationDate.getTime( ) + (86400000 *
                parseFloat(p_expires)));
            /* Create the expires string for the cookie */
            expires = '; expires=' + expirationDate.toGMTString( );
        }
        return (document.cookie = escape(name) + '=' + escape(p_value || '') +
            expires);
    },
    /**
     * This method, get, returns the cookie with a name equal to the passed /p_name/
     * variable if one exists.
     *
     * @member Cookie
     * @param {String} p_name The name for the cookie to return.
     * @return Returns the cookie data if it exists or /null/ otherwise.
     * @type String
     */
    get: function(p_name) {
        /* Get the matching cookie */
        var cookie = document.cookie.match(new RegExp('(^|;)\s*' + escape(p_name)
            + '=([^;\s]*)'));

        return (cookie ? unescape(cookie[2]) : null);
    },
    /**
     * This method, erase, removes the cookie with the passed /p_name/ variable from
     * the document. It returns the erased cookie (i.e., null if erase succeeded,
     * the cookie otherwise).
     *
     * @member Cookie
     * @param {String} p_name The name for the cookie to erase.
     * @return Returns the cookie after it is erased.
     * @type Boolean | String
     */
    erase: function(p_name) {
        /* Get the cookie with the passed /p_name/ variable */
        var cookie = Cookie.get(p_name) || true;

        /*
         * Set the cookie with the passed /p_name/ variable to an empty string, and
         * make it expire
         */
        Cookie.set(p_name, '', -1);
        return (cookie);
    },
    /**
     * This method, accept, tests to see if the browser accepts cookies and returns
     * the results of this test.
     *
     * @member Cookie
     * @return Returns whether the browser accepts cookies or not.
     * @type Boolean
     */
    accept: function( ) {
        /* Can the test be accomplished using the browser's built-in members? */
        if (typeof navigator.cookieEnabled == 'boolean')
            return (navigator.cookieEnabled);
        /* Attempt to set and erase a cookie and return the results */
        Cookie.set('_test', '1'),
        return (Cookie.erase('_test') === '1'),
    }
};

We must incorporate this cookie object into the style-switching object from Example 11-2. It is not enough to just store the user’s choice in a cookie. We must also provide a way to choose the user’s choice from the cookie when the page first loads. Example 11-4 shows how we do this.

Example 11-4. Using cookies to store user choices incorporated in our original style switcher

/*
 * Example 11-4.  Using cookies to store user choices incorporated in our original
 * style switcher.
 */

/**
 * This object, StyleSwitcher, contains all of the functionality to get and set an
 * alternate style chosen by the user from a list provided by the application.  It
 * contains the following methods:
 *    - setActive(p_title)
 *    - getActive( )
 *    - getPreferred( )
 *    - loadStyle( )
 */
var StyleSwitcher = {
    /**
     * This method, setActive, takes the passed /p_title/ variable and sets
     * the appropriate alternate style sheet as the current enabled one.  It then
     * stores this choice into a cookie for future use.
     *
     * @member StyleSwitcher
     * @param {String} p_title The title that is to be set as active.
     * @return Returns false so that the click event will be ignored.
     * @type Boolean
     * @requires Cookie This method uses the Cookie object to store the
     *     user's selection.
     * @see Cookie#set
     */
    setActive: function(p_title) {
        /* Get a list of the <link> elements in the document */
        var links = document.getElementsByTagName('link'),

        /*
         * Loop through the list, setting the appropriate <link> elements to
         * disabled, and set the <link> element with the title attribute equal to
         * /p_title/ to active.
         */
        for (var i = links.length; i > 0;) {
            /* Get the current <link> element */
            var iLink = links[i--];

            /* Is this element an appropriate stylesheet to mark? */
            if (iLink.getAttribute('rel').indexOf('style') != -1 &&
                    iLink.getAttribute('title')) {
                iLink.disabled = true;
                /* Is this element the one we are looking for? */
                if (iLink.getAttribute('title') == p_title)
                    iLink.disable = false;
            }
        }

        /*
         * Set the cookie to the passed /p_title/ variable, and do not let it expire
         * for one year
         */
        Cookie.set('appStyle', p_title, 365);
        return (false);
    },
    /**
     * This method, getActive, returns the current active stylesheet node (<link>
     * element) for the page, provided that there is one to return.
     *
     * @member StyleSwitcher
     * @return Returns the active stylesheet node, if one exists.
     * @type Node
     */
    getActive: function( ) {
        /* Get a list of the <link> elements in the document */
        var links = document.getElementsByTagName('link'),

        /*
         * Loop through the list until the active stylesheet is located, then
         * return it
         */
        for (var i = links.length; i > 0;) {
            /* Get the current <link> element */
            var iLink = links[i--];

            /* Is this the currently active <link> element? */
            if (iLink.getAttribute('rel').indexOf('style') != -1 &&
                    iLink.getAttribute('title') && !iLink.disabled)
                return (iLink.getAttribute('title'));
        }
        return (null);
    },
    /**
     * This method, getPreferred, returns the preferred stylesheet title for the
     * page, provided that there is one to return.
     *
     * @member StyleSwitcher
     * @return Returns the preferred stylesheet title, if one exists.
     */
     * @type String
    getPreferred: function( ) {
        /* Get a list of the <link> elements in the document */
        var links = document.getElementsByTagName('link'),

        /*
         * Loop through the list until the preferred stylesheet is located, then
         * return it.
         */
        for (var i = links.length; i > 0;) {
            /* Get the current <link> element */
            var iLink = links[i--];

            /* Is this the preferred <link> element? */
            if (iLink.getAttribute('rel').indexOf('style') != -1 &&
                    iLink.getAttribute('rel').indexOf('alt') == -1 &&
                    iLink.getAttribute('title'))
                return (iLink.getAttribute('title'));
        }
        return (null);
    },
    /**
     * This method, loadStyle, loads the stylesheet for the application,
     * attempting to first get it from the cookie, and if not from there, then the
     * preferred stylesheet for the page is selected instead.
     *
     * @member StyleSwitcher
     * @requires Cookie This method uses the Cookie object to get the
     *     user's selection.
     * @see Cookie#get
     */
    loadStyle: function( ) {
        /* Get the cookie, and extract the appropriate title to set active */
        var cookie = Cookie.get('appStyle'),
        var title = ((cookie) ? cookie : this.getPreferred( ));

        /* Set the active stylesheet for the page */
        this.setActive(title);
    }
};

try {
    /* Load the style sheet for the page */
    Event.observe(window, 'load', StyleSwitcher.loadStyle, false);
} catch (ex) {}

Switching Different Customizations

The switching object in Example 11-4 works well if the user will have only one customization option to select, but what if the application requires more than one? Suppose the application were to enable the user to choose the font size and color theme for the display. In such a case, we would need to change the switching object. The simplest solution is to change the object to a class structure that can be prototyped so that multiple copies of the object can be created.

Example 11-5 shows our final style switcher object, which is able to have multiple instances for different customizations in the same application. The <link> elements for the stylesheets would look like this:

<link type="text/css" rel="stylesheet" media="screen" title="group1"
    href="css/group1.css" />
<link type="text/css" rel="alternate stylesheet" media="screen"
    title="group1_alt1" href="css/group1_alt1.css" />
<link type="text/css" rel="alternate stylesheet" media="screen"
    title="group1_alt2" href="css/group1_alt2.css" />
<link type="text/css" rel="alternate stylesheet" media="screen"
    title="group1_alt3" href="css/group1_alt3.css" />
<link type="text/css" rel="stylesheet" media="screen" title="group2"
    href="css/group2.css" />
<link type="text/css" rel="alternate stylesheet" media="screen"
    title="group2_alt1" href="css/group2_alt1.css" />
<link type="text/css" rel="alternate stylesheet" media="screen"
    title="group2_alt2" href="css/group2_alt2.css" />

It will become more apparent where this would be necessary throughout the rest of the chapter.

Example 11-5. styleSwitcher.js: A style-switching class based on our earlier object

/**
 * @fileoverview Example 11-5.  styleSwitcher.js: A style switching class based on
 * our earlier object.
 *
 * This file, styleSwitcher.js, contains the styleSwitcher object, which represents
 * one instance of a group of styles that the user can switch between.
 */

/**
 * This object, StyleSwitcher, contains all of the functionality to get and set an
 * alternative style chosen by the user from a list provided by the application.  It
 * contains the following methods:
 *    - initialize(p_cookieName)
 *    - setActive(p_title)
 *    - getActive( )
 *    - getPreferred( )
 *    - loadStyle( )
 * This class requires that the titles of the stylesheets be of the form
 * /group/_/value/ so that it is easier to determine the groups each belongs to.
 * The preferred stylesheet for a group should have a title of /group/ - and the
 * cookie name should also be /group/.
 */
var styleSwitcher = Class.create( );
styleSwitcher.prototype = {
    /**
     * This member, _cookieName, stores the name of the cookie for later use in
     * the object.
     * @private
     */
    _cookieName: null,
    /**
     * This method, initialize, is the constructor for the class.  The cookie name
     * for the individual objects should be the group name.
     *
     * @member styleSwitcher
     * @constructor
     * @param {String} p_cookieName The name of the /group/ and the cookie.
     */
    initialize: function(p_cookieName) {
        this._cookieName = p_cookieName;
        this.loadStyle( );
    },
    /**
     * This method, setActive, takes the passed /p_title/ variable and sets the
     * appropriate alternate stylesheet as the current enabled one.  It then stores
     * this choice into a cookie for future use.
     *
     * @member styleSwitcher
     * @param {String} p_title The title that is to be set as active.
     * @return Returns false so that the click event will be ignored.
     * @type Boolean
     * @requires Cookie This method uses the Cookie object to store the user's
     *     selection.
     * @see Cookie#set
     */
    setActive: function(p_title) {
        /* Get a list of the <link> elements in the document */
        var links = document.getElementsByTagName('link'),

        /*
         * Loop through the list, setting the appropriate <link> elements to
         * disabled, and set the <link> element with the title attribute equal to
         * /p_title/ to active.
         */
        for (var i = links.length; i > 0;) {
            /* Get the current <link> element */
            var iLink = links[i--];

            /* Is this element an appropriate stylesheet to mark? */
            if (iLink.getAttribute('rel').indexOf('style') != -1 &&
                    iLink.getAttribute('title')) {
                iLink.disabled = true;
                /* Is this element the one we are looking for? */
                if (iLink.getAttribute('title') == p_title)
                    iLink.disable = false;
            }
        }
        /*
         * Set the cookie to the passed /p_title/ variable, and do not let it
         * expire for one year
         */
        Cookie.set(this._cookieName, p_title, 365);
        return (false);
    },
    /**
     * This method, getActive, returns the current active stylesheet node (<link>
     * element) for the page, provided that there is one to return.
     *
     * @member styleSwitcher
     * @return Returns the active stylesheet node, if one exists.
     * @type Node
     */
    getActive: function( ) {
        /* Get a list of the <link> elements in the document */
        var links = document.getElementsByTagName('link'),

        /*
         * Loop through the list until the active stylesheet is located, then
         * return it.
         */
        for (var i = links.length; i > 0;) {
            /* Get the current <link> element */
            var iLink = links[i--];

            /* Is this the currently active <link> element? */
            if (iLink.getAttribute('rel').indexOf('style') != -1 &&
                    iLink.getAttribute('title') && !iLink.disabled)
                return (iLink.getAttribute('title'));
        }
        return (null);
    },
    /**
     * This method, getPreferred, returns the preferred stylesheet title for the
     * page, provided that there is one to return.
     *
     * @member styleSwitcher
     * @return Returns the preferred stylesheet title, if one exists.
     * @type String
     */
    getPreferred: function( ) {
        /* Get a list of the <link> elements in the document */
        var links = document.getElementsByTagName('link'),

        /*
         * Loop through the list until the preferred stylesheet is located, then
         * return it.
         */
        for (var i = links.length; i > 0;) {
            /* Get the current <link> element */
            var iLink = links[i--];
            var group = iLink.getAttribute('title'),

            if (group.indexOf('_') == -1 && group == this._cookieName)
                /* Is this the preferred <link> element? */
                if (iLink.getAttribute('rel').indexOf('style') != -1 &&
                        iLink.getAttribute('rel').indexOf('alt') == -1)
                    return (group);
        }
        return (null);
    },
    /**
     * This method, loadStyle, loads the stylesheet for the application, attempting
     * to first get it from the cookie, and if not from there, then the preferred
     * stylesheet for the page is selected instead.
     *
     * @member styleSwitcher
     * @requires Cookie This method uses the Cookie object to get the user's
     *     selection.
     * @see Cookie#get
     */
    loadStyle: function( ) {
        /* Get the cookie, and extract the appropriate title to set active */
        var cookie = Cookie.get(this._cookieName);
        var title = ((cookie) ? cookie : this.getPreferred( ));

        /* Set the active style sheet for the page */
        this.setActive(title);
    }
};

Then in the application, preferably right after the page loads, we would create instances of the objects:

var group1Switcher = new styleSwitcher('group1'),
var group2Switcher = new styleSwitcher('group2'),

We would utilize the objects like this:

<div id="group1Container">
    <ul id="group1List">
        <li>style choices: </li>
        <li>
            <a href="setStyle.php?s=group1"
                    onclick="return group1Switcher.setActive('group1'),">
                alternate 1
            </a>
        </li><li>
            <a href="setStyle.php?s=group1_alt1"
                    onclick="return group1Switcher.setActive('group1_alt1'),">
                alternate 2
            </a>
        </li><li>
            <a href="setStyle.php?s=group1_alt2"
                    onclick="return group1Switcher.setActive('group1_alt2'),">
                alternate 3
            </a>
        </li><li>
            <a href="setStyle.php?s=group1_alt3"
                    onclick="return group1Switcher.setActive('group1_alt3'),">
                alternate 4
            </a>
        </li>
    </ul>
</div>
<div id="group2Container">
    <ul id="group2List">
        <li>style choices: </li>
        <li>
            <a href="setStyle.php?s=group2"
                    onclick="return group2Switcher.setActive('group2'),">
                alternate 1
            </a>
        </li><li>
            <a href="setStyle.php?s=group2_alt1"
                    onclick="return group2Switcher.setActive('group2_alt1'),">
                alternate 2
            </a>
        </li><li>
            <a href="setStyle.php?s=group2_alt2"
                    onclick="return group2Switcher.setActive('group2_alt2'),">
                alternate 3
            </a>
        </li>
    </ul>
</div>

Easy Font-Size Switching

The easiest user customization a developer can offer is dynamically changing font sizes. Though this is offered through the browser, it is a nice feature to have built right into the application. The more functionality that is programmed directly into the application, the more it will feel like an application and less like a typical web site. This occurs because the user will spend less time moving the mouse outside the application when font-size switching is available right on the page. Also, keeping the user focused within the application makes the user feel self-sufficient with it. Further, this standardizes how to obtain this functionality when it is in the same place—in your application—despite different browsers or operating systems.

Using Relative Sizes

Because of all the bugs that browsers had back in the 4.0 era—specifically, Netscape Navigator 4 and Internet Explorer 4—it was hard for developers to get any consistency using relative font sizes. To address this issue, developers were advised to use pixels for all font sizing, to get some semblance of normalcy with fonts in a web page.

Times have changed, of course, and so have browsers’ ability to render fonts correctly using relative units of measure. Thinking of backward compatibility, we still need to be prepared for the odd Netscape Communicator 4.8 floating about, but with some simple CSS tricks, this is easy to handle. Other than that, relative font sizes are the way to go, for several reasons:

  • Internet Explorer resizing issues

  • Easy developer size changes

  • No guesswork on sizing

Internet Explorer on Windows will not do text resizing from its View → Text Size options unless the developer used only relative font sizes in the CSS rules for the page. If he used any absolute-value font sizes (cm, in, etc.), the application will not appropriately resize the fonts as requested.

If relative font sizing is used for font switching, the developer needs to change the size of the text in only one spot in the CSS to change the font size of the whole page. This will become more apparent in the next section, when we discuss the CSS files required for effective font-size switching.

Using a relative font size takes some of the guesswork out of designing font sizes in a page since all elements will be sized off a single base (whatever that may be). This almost goes hand in hand with making size changes easier for the developer. It makes me shiver to think about having to change every reference to a font size in a file when someone decides to change a size in the application. Talk about mindless work! The next section shows the files required to build the font resizing customization for the user in the application using relative fonts.

Tip

Using relative font sizes in your application instead of absolute font sizes satisfies the following Web Accessibility Initiative-Web Content Accessibility Guidelines (WAI-WCAG) 1.0 guideline:

  • Priority 2 checkpoint 3.4: Use relative rather than absolute units in markup language attribute values and style sheet property values.

The Font CSS

The first file to look at is the persistent file that will load the font information that all browsers will use for the page:

<link type="text/css" rel="stylesheet" media="screen"
    href="css/screen/all_fonts.css" />

The contents of this file look like this:

body {
    font-family: Verdana, Geneva, Arial, Helvetica, sans-serif;
    font-size: 12px;
    font-style: normal;
}

h1 {
    font-family: Arial, Helvetica, sans-serif;
    font-size: 24px;
}

h2 {
    font-family: Arial, Helvetica, sans-serif;
    font-size: 20px;
    font-style: italic;
}

input[type=text] {
    font-family: "Courier New", Courier, monospace;
    font-size: 11px;
}

.required {
    font-size: 13px;
    font-weight: bold;
}

#bodyHeader {
    font-size: 16px;
    font-weight: bolder;
}

#bodyFooter {
    font-family: Arial, Helvetica, sans-serif;
    font-size: 8px;
}

This file sets all the font information we want our page to have, not just the font-size. After this file, the following markup should be put in the page:

<style type="text/css">
    /*
     * The @import rule with quotes is used to hide the file from the
     *        following browsers:
     *    - Netscape 4.x
     *    - Windows Internet Explorer 3
     *    - Windows Internet Explorer 4 (except 4.72)
     *    - Mac Internet Explorer 4.01
     *    - Mac Internet Explorer 4.5
     *    - Konqueror 2.1.2
     *    - Windows Amaya 5.1
     */
     @import url("css/screen/fonts.css");
</style>

We used the @import rule with quotes here to load the fonts.css file into the browser. By using the rule as we did, we ensure that older browsers will ignore the contents of the file where we will set all the rules we set in our persistent file to relative units. The fonts.css file looks like this:

body {
    font-size: 1em;
}

h1 {
    font-size: 2em;
}

h2 {
    font-size: 1.75em;
}

input[type=text] {
    font-size: .9em;
}

.required {
    font-size: 1.1em;
}

#bodyHeader {
    font-size: 1.4em;
}

#bodyFooter {
    font-size: .75em;
}

This file uses the em unit, setting everything to a relative size based on a browser-determined standard. Following the fonts.css file in the browser markup should be any preferred and alternative files. These files will control the font-size switching through the fileSwitcher object from Example 11-5:

<link type="text/css" rel="stylesheet" media="screen" title="size"
    href="css/screen/font-sizes/medium.css" />
<link type="text/css" rel="alternate stylesheet" media="screen"
    title="size_smallest" href="css/screen/font-sizes/smallest.css" />
<link type="text/css" rel="alternate stylesheet" media="screen"
    title="size_smaller" href="css/screen/font-sizes/smaller.css" />
<link type="text/css" rel="alternate stylesheet" media="screen"
    title="size_larger" href="css/screen/font-sizes/larger.css" />
<link type="text/css" rel="alternate stylesheet" media="screen"
    title="size_largest" href="css/screen/font-sizes/largest.css" />

All of these files are almost completely identical, and they look like this:

body {
    font-size: 12px;
}

The preferred stylesheet—medium.css, in this case—sets the font-size of the <body> element to 12 pixels, which is what the first stylesheet also set this element to. Therefore, older browsers essentially ignore it, whereas browsers that used relative font sizing now get a base set to work from instead of relying on the browser for one. The five files for our styleSwitcher object set the <body> element to the following font sizes:

  • smallest.css, 8 pixels

  • smaller.css, 10 pixels

  • medium.css, 12 pixels

  • larger.css, 14 pixels

  • largest.css, 16 pixels

The means by which the developer presents these choices to the user is arbitrary to the styleSwitcher object. The choices could be in a list, or they could be radio buttons; it really does not matter as long as the code behind them is the same for the specific mouse event being listened for.

A Font-Size Slider Bar

A list of choices or a set of radio buttons is fine for displaying choices to the user. If a developer wants to spruce this up a bit to provide more Web 2.0-like functionality, however, a slider bar is a good alternative to implement. Fortunately for us, script.aculo.us already has an object that can handle this functionality: the Control.Slider object. The basic syntax of this object is:

new Control.Slider('slider_handle's_id', 'slider_track's_id', [options]);

Table 11-1 shows all the options available to the Control.Slider object that can be passed with the options parameter.

Table 11-1. Available options for the Control.Slider script.aculo.us object

Option

Description

Default

alignX

This option will move the starting point on the x-axis for the handle in relation to the track.

0

alignY

This option will move the starting point on the y-axis for the handle in relation to the track.

0

axis

This option sets the direction that the slider will move in. This value is either 'horizontal' or 'vertical'.

'horizontal'

disabled

This option will lock the slider so that it cannot be moved.

None

handleDisabled

This option is the id of the image that represents a handle when it is disabled.

None

handleImage

This option is the id of the image that represents the handle when it is not disabled.

None

increment

This option defines the relationship of value to pixels. A value of 1 means each movement of one pixel equates to a value of 1.

1

maximum

This option sets the maximum value that the slider will move.

(The tracked length in pixels adjusted by the increment)

minimum

This option sets the minimum value that the slider will move.

0

range

This option allows for the use of range for the minimum and maximum values. Use the $R Prototype function, $R(min, max).

none

sliderValue

This option sets the initial slider value, provided that it is within the minimum and maximum values.

0

values

This option is an array of integers that tells the slider object the only legal values at which the slider can be set.

None

To create our slider for font-size switching, we need to pass only a couple of options to the options parameter when creating the Control.Slider object. The axis option defaults to horizontal, so we can skip this, but we do need to give the slider a default value to start at because we want our page to default to a medium font size. We will also want to set the increment option so that the slider moves to particular spots instead of being a smooth slider. Lastly, we want to set some values for our slider to define where the slider can actually go. Here is what this would look like:

new Control.Slider('myHandleWrapper', 'myTrackWrapper', {
    sliderValue: 50,
    increment: 12.5,
    values: [0, 25, 50, 75, 100]
});

As you can see, we also set the ids for the handle and track of our slider, which, in this case, are <div> elements that wrap <img> elements. The markup for such a slider would look like this:

<div id="myTrackWrapper">
    <div id="myHandleWrapper">
        <img id="imgSliderHandle" src="images/slider_handle.png"
            alt="This is the handle for the slider bar"
            title="This is the handle for the slider bar" />
    </div>
</div>

Figure 11-9 gives you a better idea of what our slider will look like.

A slider bar to control font-size switching

Figure 11-9. A slider bar to control font-size switching

We now have a slider, but it does not contain any of the functionality we need to make it a useful object in our application. Besides the options highlighted in Table 11-1, the options parameter can also take two callback functions, shown in Table 11-2.

Table 11-2. Callback functions for the Control.Slider object

Callback

Description

onChange

This function will be called whenever the slider object is finished moving and its value has changed. The slider value is passed as its parameter.

onSlide

This function will be called whenever the slider object is moved by dragging the handle. The slider value is passed as its parameter.

The callback function we are interested in is the onChange( ) function. This function will be called every time the slider is changed and has finished moving. Once this action occurs, we want our application to change to the appropriate font size. Luckily, for this action we can still use the styleSwitcher object from Example 11-5 without any modifications.

Once the page is loaded, we want to create our styleSwitcher object like we have before. Now, when we create our Control.Slider object, we will use the style that was loaded when we created our styleSwitcher object to seed as the sliderValue option that will be passed to our Control.Slider object. For example:

var slidebarSwitcher = new styleSwitcher('size'),
var slideValue = slidebarSwitcher.getActive( );

if (slideValue == 'size_smallest')
    slideValue = 0;
else if (slideValue == 'size_smaller')
    slideValue = 25;
else if (slideValue == 'size_larger')
    slideValue = 75;
else if (slideValue == 'size_largest')
    slideValue = 100;

new Control.Slider ('myHandleWrapper', 'myTrackWrapper', {
    sliderValue: slideValue,
    increment: 12.5,
    values: [0, 25, 50, 75, 100],
    onChange: function(p_value) {
        switch (p_value) {
            case 0:
                slidebarSwitcher.setActive('size_smallest'),
                break;
            case 1:
                slidebarSwitcher.setActive('size_smaller'),
                break;
            case 3:
                slidebarSwitcher.setActive('size_larger'),
                break;
            case 4:
                slidebarSwitcher.setActive('size_largest'),
                break;
            default:
                slidebarSwitcher.setActive('size'),
                break;
        }
    }
});

As you can see, we created our onChange( ) function inline as one of the parameters we passed to our Control.Slider object’s options parameters. With this, we now have a functional font-size slider object to use in our applications.

Creating Color Themes

I said that creating a customized font-size switching object was the easiest customization option we could develop for the user. I did not mean it was the easiest in terms of complexity or the amount of code involved. Rather, the font-size switching object is the easiest to create in terms of development and design time.

Creating a custom color-themes switching object is far easier codewise. There is no need to worry about browsers that support this or that, or having to write separate code for older browsers versus new browsers. What makes creating a custom color-themes switching object difficult is the amount of time it takes to create each individual theme, especially when a number of images are involved.

Remember the Zen

The important point to remember about creating multiple themes for an application is not to make things harder on yourself than you have to. Take the methods you learned in Chapter 10 when structuring markup for your page. Following the techniques that the CSS Zen Garden uses will make your life as a developer much easier. This is not to say that doing so will make building multiple themes a snap.

We will need to modify our styleSwitcher object from Example 11-5 to handle more conditions than it is currently capable of handling. Most likely both the color scheme and the structure will be different for each given theme. Therefore, when we make our stylesheet switch, we must change two files: the one that controls the color and the one that controls the structure. Example 11-6 shows the changes we would need to make to our existing styleSwitcher object for this to happen.

Example 11-6. styleSwitcher.js: The modified version of the styleSwitcher object

/**
 * @fileoverview Example 11-6.  styleSwitcher.js: The modified version of the
 * /styleSwitcher/ object.
 *
 * This file, styleSwitcher.js, contains the styleSwitcher object, which
 * represents one instance of a group of styles that the user can switch between.
 */

/**
 * This object, StyleSwitcher, contains all of the functionality to get and set
 * an alternative style chosen by the user from a list provided by the application.
 * It contains the following methods:
 *    - initialize(p_cookieName)
 *    - setActive(p_title)
 *    - getActive( )
 *    - getPreferred( )
 *    - loadStyle( )
 * This class requires that the titles of the stylesheets be of the form
 * /group/_/value/ so that it is easier to determine the groups each belongs to.
 * The preferred stylesheet for a group should have a title of /group/ - and the
 * cookie name should also be /group/.
 */
var styleSwitcher = Class.create( );
styleSwitcher.prototype = {
    /**
     * This member, _cookieName, stores the name of the cookie for later
     * use in the object.
     * @private
     */
    _cookieName: null,
    /**
     * This member,_multiple, lets the object know whether it should be
     * switching multiple files or just single files.
     * @private
     */
    _multiple: false,
    /**
     * This method, initialize, is the constructor for the class.  The cookie
     * name for the individual objects should be the group name.
     *
     * @member styleSwitcher
     * @constructor
     * @param {String} p_cookieName The name of the /group/ and the cookie.
     * @param {Boolean} p_multiple An optional parameter to tell the object
     *     whether multiple CSS files are involved or not.
     */
    initialize: function(p_cookieName, p_multiple) {
        this._cookieName = p_cookieName;
        if (p_multiple != 'undefined' && typeof p_multiple == 'boolean')
            this._multiple = p_multiple;
        this.loadStyle( );
    },
    /**
     * This method, setActive, takes the passed /p_title/ variable and sets
     * the appropriate alternate stylesheet as the currently enabled one.  It then
     * stores this choice into a cookie for future use.
     *
     * @member styleSwitcher
     * @param {String} p_title The title that is to be set as active.
     * @return Returns false so that the click event will be ignored.
     * @type Boolean
     * @requires Cookie This method uses the Cookie object to store the user's
     *     selection.
     * @see Cookie#set
     */
    setActive: function(p_title) {
        /* Get a list of the <link> elements in the document */
        var links = document.getElementsByTagName('link'),

        /*
         * Loop through the list, setting the appropriate <link> elements to
         * disabled, and set the <link> element with the title attribute equal to
         * /p_title/ to active.
         */
        for (var i = links.length; i > 0;) {
            /* Get the current <link> element */
            var iLink = links[i--];

            /* Is this element an appropriate stylesheet to mark? */
            if (iLink.getAttribute('rel').indexOf('style') != -1 &&
                    iLink.getAttribute('title')) {
                iLink.disabled = true;
                if (!this._multiple) {
                    /* Is this element the one we are looking for? */
                    if (iLink.getAttribute('title') == p_title)
                        iLink.disable = false;
                } else {
                    var underscoreCount = group.match(/_/g);
                    var titleName = group.split('_'),

                    if ((underscoreCount == 1 && titleName[0] == p_title) ||
                            (underscoreCount == 2 && (titleName[0] + '_' +
                            titleName[1]) == p_title))
                        iLink.disable = false;
                }
            }
        }
        /*
         * Set the cookie to the passed /p_title/ variable, and do not let it
         * expire for one year
         */
        Cookie.set(this._cookieName, p_title, 365);
        return (false);
    },
    /**
     * This method, getActive, returns the current active stylesheet node (<link>
     * element) for the page, provided that there is one to return.
     *
     * @member styleSwitcher
     * @return Returns the active stylesheet node, if one exists.
     * @type Node
     */
    getActive: function( ) {
        /* Get a list of the <link> elements in the document */
        var links = document.getElementsByTagName('link'),

        /*
         * Loop through the list until the active stylesheet is located, then
         * return it.
         */
        for (var i = links.length; i > 0;) {
            /* Get the current <link> element */
            var iLink = links[i--];

            /* Is this the currently active <link> element? */
            if (iLink.getAttribute('rel').indexOf('style') != -1 &&
                    iLink.getAttribute('title') && !iLink.disabled)
                if (!this._multiple) {
                    return (iLink.getAttribute('title'));
                } else {
                    var underscoreCount = group.match(/_/g);
                    var titleName = group.split('_'),

                    if (underscoreCount == 1)
                        return (titleName[0]);
                    else
                        return (titleName[0] + '_' + titleName[1]);
                }
        }
        return (null);
    },
    /**
     * This method, getPreferred, returns the preferred stylesheet title for
     * the page, provided that there is one to return.
     *
     * @member styleSwitcher
     * @return Returns the preferred stylesheet title, if one exists.
     * @type String
     */
    getPreferred: function( ) {
        /* Get a list of the <link> elements in the document */
        var links = document.getElementsByTagName('link'),

        /*
         * Loop through the list until the preferred stylesheet is located,
         * then return it.
         */
        for (var i = links.length; i > 0;) {
            /* Get the current <link> element */
            var iLink = links[i--];
            var group = iLink.getAttribute('title'),

            if (!this._multiple) {
                if (group.indexOf('_') == -1 && group == this._cookieName)
                    /* Is this the preferred <link> element? */
                    if (iLink.getAttribute('rel').indexOf('style') != -1 &&
                            iLink.getAttribute('rel').indexOf('alt') == -1)
                        return (group);
            } else {
                var underscoreCount = group.match(/_/g);
                var titleName = group.split('_'),

                if (underscoreCount == 1 && titleName[0] == this._cookieName)
                    /* Is this the preferred <link> element? */
                    if (iLink.getAttribute('rel').indexOf('style') != -1 &&
                            iLink.getAttribute('rel').indexOf('alt') == -1)
                        return (titleName[0]);
            }
        }
        return (null);
    },
    /**
     * This method, loadStyle, loads the stylesheet for the application,
     * attempting to first get it from the cookie, and if not from there, then the
     * preferred stylesheet for the page is selected instead.
     *
     * @member styleSwitcher
     * @requires Cookie This method uses the Cookie object to get the user's
     *     selection.
     * @see Cookie#get
     */
    loadStyle: function( ) {
        /* Get the cookie, and extract the appropriate title to set active */
        var cookie = Cookie.get(this._cookieName);
        var title = ((cookie) ? cookie : this.getPreferred( ));

        /* Set the active style sheet for the page */
        this.setActive(title);
    }
};

You will notice that the extra parameter passed to the initialize( ) method is not required and will be ignored by default. When it is present, it lets the object know that it will be switching multiple files instead of just one. The code is built in such a way that it does not matter how many files are associated with a given customization option, as long as they are named in a similar way.

The Rest Is the Same

Once we have all of this set up, everything else about the switching is the same as it was for the font-size switcher. A string is passed to our event handler when the given event is triggered (clicking a link or radio button). The string is the grouping name of the multiple files that would need to be changed with the switch. The following is an example of what all of the <link> elements might look like:

<link type="text/css" rel="stylesheet" media="screen"  title="theme_color"
    href="css/screen/themes/default_color.css" />
<link type="text/css" rel="stylesheet" media="screen"  title="theme_structure"
    href="css/screen/themes/default_structure.css" />
<link type="text/css" rel="alternate stylesheet" media="screen"
    title="theme_spring_color" href="css/screen/themes/spring_color.css" />
<link type="text/css" rel="alternate stylesheet" media="screen"
    title="theme_spring_structure" href="css/screen/themes/spring_structure.css" />
<link type="text/css" rel="alternate stylesheet" media="screen"
    title="theme_summer_color" href="css/screen/themes/summer_color.css" />
<link type="text/css" rel="alternate stylesheet" media="screen"
    title="theme_summer_structure" href="css/screen/themes/summer_structure.css" />
<link type="text/css" rel="alternate stylesheet" media="screen"
    title="theme_autumn_color" href="css/screen/themes/autumn_color.css" />
<link type="text/css" rel="alternate stylesheet" media="screen"
    title="theme_autumn_structure" href="css/screen/themes/autumn_structure.css" />

Throwing Ajax into the Mix

With some of the basics of customization out of the way, it is time to throw Ajax into the mix. Ajax will allow us to create more complex customizations for the user without requiring a total page refresh with each change in that customization. More work is involved in the approaches that follow, but more work is not necessarily a bad thing.

Preparing the Structure for Change

Any customizations that we would want to make through Ajax will require us to do some heavy manipulation of the Document Object Model (DOM) document. Therefore, it is important that our markup is structured to where we want it and that it is readily identifiable through id and class attributes. To give an example, let’s say we have two paragraphs on the page, both of which can be changed through manipulation when there is an Ajax call. The following would be a bad way to structure this markup:

<p>
    <!-- first paragraph data -->
</p><p>
    <!-- second paragraph data -->
</p>

This markup does what we want as far as displaying information to the user, but it makes it extremely difficult to manipulate with JavaScript. A better way to do this would be:

<div id="myParagraphContainer">
    <p id="paraSwitch01" class="switchable">
        <!-- first paragraph data -->
    </p>
    <p id="paraSwitch02" class="switchable">
        <!-- second paragraph data -->
    </p>
</div>

With this markup, it would be far easier to change the two paragraphs on the fly with a few lines of JavaScript code.

If you know your pages are going to be heavily manipulated with customizations that the user controls, it is better to overly mark up your document than to sparsely mark it up. Not all ids and classes need to be used in every situation, but it doesn’t hurt to have them there either. Backend changes are easier to make when the structure is already there and it does not need to be changed in any way. Using ids and classes liberally throughout your markup also allows for some unintentional commenting, which also never hurts!

Tip

Setting an id attribute for most elements within our markup on the page allows us to easily identify it within JavaScript. It also allows us to create hyperlinks to it. Most important, it allows us to override class style information with instance style information.

Arrays to Store Ever-Changing Information

A good trick to use when you know the data of several elements will change (most commonly to another language) is to utilize arrays to hold the data to display, and to use JavaScript to produce all the markup elements dynamically. For example, say you had a form you needed to offer in three different languages. It would require a lot of extra data to send the whole form to the user on a language change request. Likewise, it would require extra data to send every language to the client right away. A large amount of data is the enemy; it slows down the site load, which is something we always want to avoid. Let’s take a closer look at our form-language example.

The first thing to do is to set up your label array for the <label> elements that are associated with the <input> elements, as shown in Example 11-7. The <label> elements will change in the language-switching process.

Example 11-7. A <label> element array

/*
 * Example 11-7.  A <label> element array.
 */

var arrLabels = [];

arrLabels[0] = 'Enter your personal information.';
arrLabels[1] = 'Last Name: ';
arrLabels[2] = 'First Name: ';
arrLabels[3] = 'Middle Initial: ';
arrLabels[4] = 'Address: ';
arrLabels[5] = 'City: ';
arrLabels[6] = 'State: ';
arrLabels[7] = 'Zip Code: ';
arrLabels[8] = 'Phone Number: ';
arrLabels[9] = 'E-mail: ';

Now we need the JavaScript to load the form that dynamically creates the <form> element and its childNodes, utilizing the array in the process, as shown in Example 11-8.

Example 11-8. JavaScript to dynamically create a form

/*
 * Example 11-8.  JavaScript to dynamically create a form.
 */

/**
 * This function, loadForm, creates a new form with labels from the /arrLabel/
 * global array and places this into the <form> element's container element.
 */
function loadForm( ) {
    /* Create the string that will be the form */
    var strForm = '';

    strForm = '<form id="infoForm" action="submitInfo.php" method="post">';
    strForm += '<fieldset>';
    strForm += '<legend>' + arrLabels[0] + '</legend>';
    strForm += '<div><label for="nptLastName">' + arrLabels[1] +
        '</label> <input id="nptLastName" type="text" value="" /></div>';
    strForm += '<div><label for="nptFirstName">' + arrLabels[2] +
        '</label> <input id="nptFirstName" type="text" value="" /></div>';
    strForm += '<div><label for="nptMiddleInitial">' + arrLabels[3] +
        '</label> <input id="nptMiddleInitial" type="text" value="" /></div>';
    strForm += '<div><label for="nptAddress">' + arrLabels[4] +
        '</label> <input id="nptAddress" type="text" value="" /></div>';
    strForm += '<div><label for="nptAddress2"></label> <input id="nptAddress2" ' +
        'type="text" value="" /></div>';
    strForm += '<div><label for="nptCity">' + arrLabels[5] +
        '</label> <input id="nptCity" type="text" value="" /></div>';
    strForm += '<div><label for="nptState">' + arrLabels[6] +
        '</label> <input id="nptState" type="text" value="" /></div>';
    strForm += '<div><label for="nptZipCode">' + arrLabels[7] +
        '</label> <input id="nptZipCode" type="text" value="" /></div>';
    strForm += '<div><label for="nptTelephone">' + arrLabels[8] +
        '</label> <input id="nptTelephone" type="text" value="" /></div>';
    strForm += '<div><label for="nptEmail">' + arrLabels[9] +
        '</label> <input id="nptEmail" type="text" value="" /></div>';
    strForm += '</fieldset>';
    strForm += '</form>';

    /* Set the /innerHTML/ of the <div> element container to the /strForm/ */
    $('infoFormContainer').innerHTML = strForm;
}

Figure 11-10 shows this dynamically created form. After this, it is simply a matter of placing a call to the loadForm( ) function within the structure of the page, and the form will be built. Everything is set up to tackle the problem of a quick and easy language-switching site.

The dynamically created form

Figure 11-10. The dynamically created form

Changing Site Language with Ajax

The Internet is global, and as such, there has been a growing need for web applications to be multilingual to cater to this vast potential audience. The problem is that most solutions for different languages ask the user upfront what language to use, and then load pages built specifically with that language. If the user wants to switch to a different language, everything must be reloaded with the new language. This is not always a fast solution, because a large amount of JavaScript, CSS, and markup may need to be reloaded with the new language choice.

When the site is set up correctly, calls for changes in language can go much faster by utilizing the speed associated with Ajax and smaller data responses. Sending just the data within the given elements is much faster than reloading the page with all the CSS and JavaScript file loads that must also be downloaded, parsed, and applied.

The JSON to Send

I have used XML for all of the server responses to this point—not necessarily because I think the XML is better than a JavaScript Object Notation (JSON) response, but because JavaScript is usually easier to comprehend and more descriptive and, thus, is a better teaching aid. This solution specifically requires JSON, so that’s what you will see.

We will pretend that we have made a request for a different language for our form to the server. The server must make a response with data that supports the new language. Because we have the default language built with an array to hold values, it is a simple matter of formatting the response to utilize this fact. The response should be an array of values. The following is an example of what would be returned to match the default values from Example 11-7:

[
'Deine pers&#246;nlichen Informationen eintragen.',
'Familienname: ',
'Vorname: ',
'Mittlere Initiale: ',
'Adresse: ',
'Stadt: ',
'Zustand: ',
'Rei&#223;verschluss-Code: ',
'Telefonnummer: ',
'E-mail: '
]

Switching Out the Data

So, we have the data we need in the xhrResponse.responseText from the server. Now what? It is a simple matter of replacing the original array with the new JSON that was sent and rerunning the function that creates the form in the first place. Example 11-9 shows the JavaScript necessary to perform such an action.

Example 11-9. Switching out the label data

/* Example 11-9.  Switching out the label data. */

/**
 * This function, reloadForm, takes the XMLHttpRequest JSON server response
 * /xhrResponse/ from the server and sets it equal to the global <label> element
 * array /arrLabels/.  It then calls /loadForm/ which re-creates the form with the
 * new data.
 *
 * @param {Object} xhrResponse The XMLHttpReqest JSON server response.
 */
function reloadForm(xhrResponse) {
    /* /eval/ the JSON response to create an array to replace the old one */
    /* *** You should always validate data before executing the eval *** */
    arrLabels = eval(xhrResponse.responseText);
    /* Load the form with the global /arrLabels/ array */
    loadForm( );
}

After this code is executed, the form will look like Figure 11-11.

The form switched to a different language

Figure 11-11. The form switched to a different language

This example merely changed the text for a relatively small form, but the principle will work no matter what content needs changing. In this way, the developer can avoid loading more data than necessary, and can keep the application loading faster.

A Faster Alternative?

Using the Ajax method for changing the language in an application may not be the best solution in all circumstances. If a switch is called for in which a considerable amount of text will be involved (e.g., a manual), a faster alternative is to reload the whole page. In these instances, the developer must realize that there is only a minimal advantage, if any, to trying to do this sort of switching with Ajax. The tried-and-true methods sometimes are the best.

Repositioning Objects and Keeping Those Positions

This next user customization is for more modern applications that apply Web 2.0 functionality in them. Some of this functionality is in moving objects around on a page. When the application in question truly is living up to its name as an application, any changes the user makes should be saved for the next time that same user uses the application. One of these user-customizable options is the position of objects on a page.

For now, let’s assume the user can move objects on the screen without going into detail regarding how this is accomplished. We need to keep track of the final x and y coordinates of the object once it has moved, as well as what object was moved. The easiest way to store this information is in a simple multidimensional array, where the first index stores the object being moved and the second index stores an array containing the object’s x and y coordinates. When the page initially loads, there should be a call to the database to move any objects that were moved from their original position when the user last used the application. It is that simple to save position!

Dragging Objects Around

OK, seriously, first we need to allow the object to be moved or dragged around in the application. In Chapter 10, I introduced you to the Draggable script.aculo.us object. Using this object is the easiest way to move an object to a new position, and by using the snap object, you can exert the tiniest bit of control over where the object is to be placed.

Here is the code to create a Draggable object that stores the final x and y coordinates to a variable when the object stops moving:

new Draggable('objectContainer', {
    handle: 'objectHandle',
    snap: 20,
    starteffect: false,
    endeffect: function(p_element) {
        window.status = Position.cumulativeOffset(p_element);
    }
});

With this code, the final coordinates are set in the window.status property, but it is easy to imagine a more useful way to deal with them.

Storing Information in a Database

We could save the x and y coordinates of a draggable object in a cookie, like all the other customization options we have seen so far, but we are going to aim for something a little more permanent here. A database is a logical place to store this information, but what information are we to store for our draggable object?

First, we need to assume that there is a way to uniquely identify the user that is manipulating the application. So, we will assume that the user has a login ID stored somewhere, such as in a Session variable. Then we must store the element information: the element id, x coordinate, and y coordinate.

Here is a simple table to store this information.

Column

Type

loginID

INTEGER

elementID

VARCHAR(25)

xCoord

SMALLINT

yCoord

SMALLINT

We can create this table with the following SQL:

CREATE TABLE draggable_position (
    loginID          INTEGER              NOT NULL,
    elementID        VARCHAR(25)          NOT NULL,
    xCoord           SMALLINT             NOT NULL,
    yCoord           SMALLINT             NOT NULL,
    PRIMARY KEY (loginID),
    KEY (elementID)
);

Our client is going to need the server to perform two SQL commands. The first is to send the elements in the table for the user with the matching loginID and their x and y coordinates. The second is to save (or update) the table with the element and its coordinates for the user’s loginID.

Retrieving the information is as simple as the following SQL:

SELECT
    d.elementID,
    d.xCoord,
    d.yCoord
FROM
    draggable_position d
WHERE
    d.loginID = $loginID;

Inserting information would require the following SQL:

INSERT INTO draggable_position (
    loginID,
    elementID,
    xCoord,
    yCoord
) VALUES (
    $loginID,
    $elementID,
    $xCoord,
    $yCoord);

Updating an existing row would require the following SQL:

UPDATE
    draggable_position
SET
    xCoord = $xCoord,
    yCoord = $yCoordWHERE
    loginID = $loginID AND
    elementID = $elementID;

This is the basic idea for storing information in a database. Your mileage may vary.

Sending Changes with Ajax

Sending the changes in the position of an object in the application to the server to store in a database is a snap. Here is where we can actually utilize the object’s final coordinates. The inline function in the endeffect option will now contain an Ajax call to the database, passing it the information it needs:

endeffect: function(p_element) {
    var coorinates = Position.cumulativeOffset(p_element);

    Ajax.Request('savePosition.php',
        method: 'POST',
        parameters: 'id=' + p_element.id + '&x=' + coordinates[0] + '&y=' +
            coordinates[1]
    );
}

The server must take the passed parameters and either insert or update the database with this data. Example 11-10 shows what this would look like.

Example 11-10. The server-side code to store element position in a database

<?php
/**
 * Example 11-10.  The server-side code to store element position in a database.
 */
/**
 * The Zend Framework Db.php library is required for this example.
 */
require_once('Zend/Db.php'),
/**
 * The generic db.php library, containing database connection information such as
 * username, password, server, etc., is required for this example.
 */
require('db.inc'),

/* Set up the parameters to connect to the database */
$params = array ('host' => $host,
                 'username' => $username,
                 'password' => $password,
                 'dbname' => $db);

try {
    /* Were the parameters passed that needed to be? */
    if ($_POST['id'] && $_POST['x'] && $_POST['y']) {
        $login = $_SESSION['login_id'];
        $id = $_POST['id'];
        $x = $_POST['x'];
        $y = $_POST['y'];

        /* Connect to the database */
        $db = Zend_Db::factory('PDO_MYSQL', $params);
        /* Create the SQL string */
        $sql = "SELECT loginID FROM draggable_position WHERE loginID = $login "
            ."AND elementID = "$id"";
        /* Get the results of the query */
        $result = $db->query($sql);
        $new = -1;
        /* Was a row returned? */
        if ($row = $result->fetchRow( ))
            $new = $row['loginID'];
        /* Should this record be updated? */
        if ($new != -1) {
            $set = array (
                'xCoord' => $db->quote($x),
                'yCoord' => $db->quote($y)
            );
            $table = 'draggable_position';
            $where = "loginID = $login AND elementID = "$id"";

            /* Update the record */
            $rows_affected = $db->update($table, $set, $where);
        } else {
            $row = array (
                'loginID' => $login,
                'elementID' => $id,
                'xCoord' => $x,
                'yCoord' => $y
            );
            $table = 'draggable_position';

            /* Insert the record */
            $rows_affected = $db->insert($table, $row);
        }
    }
} catch (Exception $e) {}
?>

The server does not need to send anything back to the client with this example, because the client really can’t do anything, even if there is a problem with the database or server-side script.

Storing It All in the Database

I have provided you with some good examples of storing user customization information in a database. Now I will state the obvious: it is fine to continue saving user customizations in cookies, but we should also store this data in a database. The user, or even other software, can wipe out cookies. It would frustrate a user to have to redo his customizations because he lost them when cookies were deleted. Having the data stored in a database ensures that this will not happen.

An even better reason to store the information in a database is that this is an Internet application. The user may connect from any number of computers, and it would be good if the user’s preferences transferred to those computers as well. If cookies are the only means of storing user customizations, it would not be possible for the user to have the same preferences on different computers until he actually set up those preferences on each one.

From the developer’s or client company’s point of view, having this information stored in a database is a nice administrative tool. Information such as which customizations are most and least frequently used can be mined from the storage tables. This allows for the deletion of unused code and the possible creation of new customizations. Also, by seeing application usage trends, design teams can redesign elements of the web application for future releases based on this data.

There are no real disadvantages to having customizations stored on the server, aside from all the extra requests that the server must handle when a customization of the Ajax application occurs. If the web server can handle it, so can the developer!

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
35.171.45.182