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.
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.
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.
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).
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.
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.
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; }
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).
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.
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 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.
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.
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:
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.
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.
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.
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.
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.
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/).
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.
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) {}
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>
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.
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.
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 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 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 |
---|---|---|
| This option will move the starting point on the x-axis for the handle in relation to the track. |
|
| This option will move the starting point on the y-axis for the handle in relation to the track. |
|
| This option sets the
direction that the slider will move in. This value is either
|
|
| This option will lock the slider so that it cannot be moved. | None |
| This option is the
| None |
| This option is the
| None |
| This option defines
the relationship of value to pixels. A value of |
|
| This option sets the maximum value that the slider will move. | (The tracked length in pixels adjusted by the increment) |
| This option sets the minimum value that the slider will move. |
|
| This option allows
for the use of range for the | none |
| This option sets the
initial slider value, provided that it is within the
|
|
| 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 id
s 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.
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 |
---|---|
| 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. |
| 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.
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.
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.
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" />
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.
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!
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.
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 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.
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önlichen Informationen eintragen.', 'Familienname: ', 'Vorname: ', 'Mittlere Initiale: ', 'Adresse: ', 'Stadt: ', 'Zustand: ', 'Reißverschluss-Code: ', 'Telefonnummer: ', 'E-mail: ' ]
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.
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.
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.
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!
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.
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 |
|
elementID |
|
xCoord |
|
yCoord |
|
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 =$yCoord
WHERE loginID =$loginID
AND elementID =$elementID
;
This is the basic idea for storing information in a database. Your mileage may vary.
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.
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!
35.171.45.182