Showing posts with label web development. Show all posts
Showing posts with label web development. Show all posts

Tuesday, May 06, 2008

Keystroke and Field Validation with JavaScript

Yesterday, I provided a simple tool to demonstrate the differences between keydown and keypress events. Today, I'm going to apply that knowledge toward making a lightweight form validation library. This library will prevent invalid characters from being entered in form input elements which is useful toward preventing malicious data from being entered (think XSS), as well as ensuring data integrity (data is in the valid format, all required fields present, etc.).

Sample Form:



Alpha
Alphanumeric*
Date
Decimal
Digit
Email
Hostname
IP address
Integer
Punctuation
Real number
Time
Whitespace
custom
type
format


* denotes required field.




The form above should only allow valid characters in each input field - this is your keystroke validation. Furthermore, for fields that require a specific format, when you leave a field with invalid formatted data it should provide an immediate visual indication of the error by setting a fieldWithErrors CSS class on invalid fields. Additionally, a fv:onInvalid event is fired on the invalid element to allow custom events handlers to provide additional actions. For example, enter a number in the date field and tab out. The field will be outlined in red from the fieldWithErrors CSS class and the message, "Date must be yyyy-mm-dd" will be shown by the handling of the fv:onInvalid event. Note: the CSS class will be removed automatically when the field is corrected, and a corresponding fv:onValid event will be fired to allow the custom code to clean up after itself.

Validation rules are defined as Regular Expressions. If you are not familiar with Regular Expressions here is a good reference. Additionally, many predefined regex rules for various validations can be found at the regex library.

Several common data types and formats are defined by default in the library, but it also provide the mechanism to add you own data types. In the custom entry, the type entry allows you to specify an on-the-fly keystroke validation regular expression, and the format entry allows you to specify an on-the-fly field format validation regular expression.

The Validate Form button demonstrates the action you would want to perform before a form submission. It invokes a validateAllFields function which just runs all the field validations (and invokes corresponding CSS rules and issues events). It also returns an array of the invalid field names in case you want to build a list of errors (like the Rails flash messages).


Usage Instructions

Sample Usage:

<script src="javascript/formValidation.js"></script>
<script>
var vf = new FormValidation();
vf.addValidationForField("date", "date", "date");
</script>

addValidationForField requires 3 parameters:

  1. The identifer of the field to add validation for

  2. The name of the data type to use for keystroke validation

  3. The name of the data format to use for field validation

In this sample all the names are the same, but they usually wouldn't be.

Just doing the above will prevent invalid characters from being entered in the data field and can indicate when the field is invalid.

When a field is changed, the format validation is run and will do two things.

  1. It will mark invalid fields are the fieldWithErrors CSS class. This can be used to appropriately highlight the error to the user. For example, the following CSS rule will outline the field in red:

    .fieldWithErrors {
    border: 3px solid red;
    }

  2. An event will also be issued in the field to indicate whether or not the field is valid. The events to watch for are fv:onInvalid and fv:onValid. These events can be used to hide/show error messages such the following which will display a pre-defined error message:

    $$('input').each(function(inputElem) {
    inputElem.observe("fv:onInvalid", function() {
    $(this.identify() + "-error").show();}.bind(inputElem));
    inputElem.observe("fv:onValid", function() {
    $(this.identify() + "-error").hide();}.bind(inputElem));
    });

    Or any number of other actions. For example, you may wish to display a popup error message such as those provided by Prototype Window or add messages to an error flash area as commonly done in Rails apps or toggle the submit button as enabled/disabled. Using these events allows you to add whatever actions you desire when the
    validation fails (or passes).



Download
formValidation.js

Monday, May 05, 2008

keydown vs. keypress (in JavaScript)

In my next post, I intend to discuss a validation library which performs keystroke and field level validation for HTML forms using JavaScript and regular expressions. However, before you can correctly understand how the validation works, as well as when you can test for various validation conditions, you have to recognize the different behaviors between the keydown and keypress events.

In the following text fields, type whatever you like and notice the different behaviors in the table below it.




EventcharCodekeyCodeDisplayShifted?
Keydown
Keypress


Some notable problem keys when dealing with JavaScript keystroke validation are the arrow keys and other editing keys (delete, home, end, insert) which should be allowed in order to edit a field value, however they can appear to be punctuation symbols which may not be desired in the field. Function keys and number pad keys are also problematic.



Here are some key observations regarding keypress events:

  • Firefox sends regular keys as charCodes and special keys as keyCodes

  • Safari sends both charCode and keyCode for regular keys but does not issue keypress events for special keys

  • Internet Explorer does not send charCodes, only keyCodes, and does not issue keypress events for special keys

  • Opera does not send charCodes, only keyCodes, but also issues keypress events for special keys making it impossible to distinguish between some keys such as '-' vs. Insert and '.' vs. Delete

  • Konqueror will send both charCode and keyCode for regular keys, but only keyCode for special keys


Friday, February 22, 2008

busyProcess: a visual indicator for long JavaScript tasks

Some time ago I wrote an article to demonstrate a simple method of displaying a processing/loading indicator for Ajax requests using prototype. That works great for Ajax requests, but let's assume you have some CPU intensive task that will be processed on the client side in JavaScript. This task will take several seconds to complete and you want to add some sort of visual indicator to the page while it's working so the user knows something is happening. enter the busyProcess function.

You could code your particular JavaScript task to display a processing indicator itself and remove it when it's done, however there are two problems you'll encounter. First, you'll probably never see the processing indicator since your task will immediately do its thing and not give control back to the browser in order to display the indicator before the task finishes and your code removed the indicator. Second, you'll end up adding the same code over and over around each function that may take a while to complete (DRY - Do not Repeat Yourself!).

busyProcess handles both of these situations. It is a flexible wrapper for any function passed in to it so that it can be used for any task, and it defers the execution of the function in order to allow the browser to render the visual indicator.

Example

Click this to be busy for 3 seconds.

Get to the code already!


/**
* busyProcess
*
* Add a busy indicator over an element while running the function it invokes
* @param {Object} element clicked on to invoke the task
* @param {Function} function to invoke after adding visual indicator
*/
function busyProcess(element, func) {
var busyIndicator = new Element("div", {id: "busy"});
busyIndicator.setStyle({zIndex: "100",
position: "absolute",
fontWeight:"bold",
height: "16px"});
Position.clone($(element), busyIndicator, {setWidth: false, setHeight: false});
busyIndicator.innerHTML = ' Processing...';
document.body.appendChild(busyIndicator);

// function needs to be deferred in order for the browser to render the
// busy indicator, but we need to wrap it in order to remove the busy indicator
// when it's done
func = func.wrap(
function(proceed) {
proceed();
busyIndicator.remove();
});
func.defer();
}



Ok, so how does it work?

The function takes in two arguments. The first is the element clicked on. This is used as the location of the popup processing indicator as the item clicked on is what invoked the action for the user. Since the prototype $() function is used, this can be an element id as well. The second argument is the function to invoke.

busyProcess starts by building the processing indicator. This can be customized to your preferences. Just be sure the indicator has a z-index greater than anything other layers on the screen, and that is has absolute positioning. I then utilize the prototype Position.clone to position the indicator over the element clicked on.

Now we get to the more interesting part. The indicator needs to go away when the task is done. But how do we know when it's done? You could make the task issue a custom event and register an event listener here to catch it in order to remove the processing indicator. However, then any function you use busyProcess with will need to issue that event when it's done. That could get messy, and easy to forget. So, instead we wrap the original function to have it remove the processing indicator when it's done.

So, how do you use this?

For a simple example, let's assume we want to sort some large table by different columns when the user clicks on little triangle icons indicating ascending or descending.
For simplicity I'm just going to add the code inline on an onclick attribute in the HTML. As a general practice I usually try to register event handlers in my JavaScript code so that the HTML has no JavaScript in it.


<td id="zipcode">Zip Code
<img src="images/sort-asc.gif" alt="ascending sort icon"
onclick="busyProcess(this, function() {sortTableBy(this.up('td').identify());}.bind(this));" />
</td>


I intentionally made that a little more complicated than it had to be just to demonstrate that you can use inline anonymous functions as well.

Optional (but useful) addition

In order to give a more obvious indication that work is being done and to prevent the user from clicking on anything else until it's done you may want to either darken or lighten the rest of the page. To do this, add another layer at the beginning of the busyProcess function like:

var lightenScreen = new Element("div", {id: "lightenScreen",
'class': "lightenBackground"});
document.body.appendChild(lightenScreen);

where the lightenBackground class is defined in CSS as:

.lightenBackground {
background-color: white;
opacity: 0.5; /* Safari, Opera */
-moz-opacity: 0.50; /* FireFox */
filter: alpha(opacity = 50); /* IE */
z-index: 20;
height: 100%;
width: 100%;
background-repeat: repeat;
position: fixed;
top: 0px;
left: 0px;
cursor: wait;
}

or the corresponding darkening version:

.darkenBackground {
background-color: black;
opacity: 0.2; /* Safari, Opera */
-moz-opacity: 0.20; /* FireFox */
filter: alpha(opacity = 20); /* IE */
z-index: 20;
height: 100%;
width: 100%;
background-repeat: repeat;
position: fixed;
top: 0px;
left: 0px;
cursor: wait;
}

Just be sure to remove this layer as part of the wrap by adding the line: lightenScreen.remove();

Tuesday, January 22, 2008

My ImageFlow with Lightbox packaged sample

Per a request from Clemens to my last post, I've packaged my revised version of ImageFlow and Lightbox2.

In addition, I've made my sample application issue an Ajax request to get the list of images to demonstrate how you can dynamically build the photo album. Here is the code:

// sample application
var MyApp = function() {
var imgTemplate = new Template('#{caption}');

function initializeAlbum(response) {
var imageList;
var imgHTML = '';

try {
imageList = response.responseJSON;
} catch (e) {
console.error(e, "\n", response.responseText);
}

// IE bug : for all other browsers an imageList.each() works great here,
// however IE doesn't like it so we revert to a for loop
for (var i = 0; i < imageList.length; i++) {
var filename = imageList[i];
// IE bug : IE computes an incorrect value for imageList.length resulting
// in undefined filenames in the loop, check for that
if (filename) {
// for simplicity, extract the base filename for the caption.
// Caption could also be defined for each image in the JSON response data.
var caption = filename.split('/').pop().split('.')[0].gsub("%20", " ");
imgHTML += imgTemplate.evaluate({filename : filename, caption : caption});
}
}

$('images').innerHTML = imgHTML;
// IE bug : IE needs a moment to let the innerHTML change register before proceeding
// so we add a small timeout to let it catch its breath
setTimeout(ImageFlow.initialize, 10);
}

return {
loadAlbum : function(albumName) {
$('startButton').hide();
$('photoAlbum').show();
var myAjax = new Ajax.Request('imageList.php?albumName=' + albumName, {
method: 'get',
onSuccess: initializeAlbum
});
}
};

}();

The initial button you see in the sample application, invokes the loadAlbum() function when clicked as shown:

<input type="button" value="Click here to load photo album" onclick="MyApp.loadAlbum('FlickrEyeCandy');"></input>

Download the packaged sample application shown below.



Update January 23, 3pm EST: It was reported that the sample application didn't work in IE. This was due to some deficiencies in IE which I have accounted for now. I have pointed them out in the code with the comments beginning with IE bug

Friday, January 18, 2008

ImageFlow with Lightbox Lite

I like the idea of using ImageFlow with Lightbox2, however, I don't like that it required a tweaked version of ImageFlow, nor that I then had to modify my image list to wrap the Lightbox2 elements around them. Additionally, Lightbox2 adds a lot of functionality that's not being using in this case (all the next/previous image controls, image caching, etc), and it also wants to initialize itself during the window.onload event which I don't want in my Ajax apps.

So, wrote my own Lightbox Lite. Well, technically, I extracted my own Lightbox Lite from the Lightbox2 code with a few modifications.

To begin with, I added this block of HTML to my web page. The Lightbox2 code constructs this dynamically in its initialize function, but I don't see the advantage of that vs just hard coding it into the page to begin with.

<div id="photo" style="display:none">
<div id="lightbox">
<div id="outerImageContainer">
<div id="imageContainer">
<img id="lightboxImage" style="display:none"></img>
<div id="imageloading">
<img src="images/loadingImage.gif">
</div>
</div>
</div>
<div id="imageDataContainer" style="display:none">
<div id="imageData">
<div id="imageDetails">
<span id="imagecaption"></span>
</div>
<div id="imageBottomNav" onclick="LightBoxLite.reset();" style="float:right" >
<img src="images/close-button.png" alt="Close" title="Close">
</div>
</div>
</div>
</div>
</div>

Then I added the relevant CSS rules to my stylesheet:

#photo {
z-index: 51;
position: absolute;
top: 5px;
left: 1px;
width: 100%;
}

#outerImageContainer {
background-color: white;
width: 250px;
height: 250px;
margin: 0 auto;
}

#imageContainer {
padding-top: 10px;
}

#imageloading {
position: absolute;
top: 40%;
left: 47.5%;
}

#imageDataContainer {
font: 10px Verdana, Helvetica, sans-serif;
background-color: white;
margin: 0 auto;
line-height: 1.4em;
overflow: auto;
width: 100%;
padding-bottom: 5px;
}

#imageData {
padding: 0 10px;
color: #666;
}

#imageData #imageDetails {
width: 80%;
float: left;
text-align: left;
}

#imageData #imageCaption {
font-weight: bold;
}

Next I added the Lightbox Lite code to my JavaScript (note, you still need prototype.js and script.aculo.us):

LightBoxLite = function() {

var borderSize = 10;
var resizeDuration = 0.6;

/**
* resizeImageContainer
*
* @param {Number} desired width
* @param {Number} desired height
*/
function resizeImageContainer( imgWidth, imgHeight) {

// get current width and height
var widthCurrent = $('outerImageContainer').getWidth();
var heightCurrent = $('outerImageContainer').getHeight();

// get new width and height
var widthNew = (imgWidth + (borderSize * 2));
var heightNew = (imgHeight + (borderSize * 2));

// scalars based on change from old to new
var xScale = ( widthNew / widthCurrent) * 100;
var yScale = ( heightNew / heightCurrent) * 100;

// calculate size difference between new and old image, and resize if necessary
var wDiff = widthCurrent - widthNew;
var hDiff = heightCurrent - heightNew;

if (!( hDiff === 0)) {
new Effect.Scale('outerImageContainer', yScale, {scaleX: false, duration: resizeDuration, queue: 'front'});
}
if (!( wDiff === 0)) {
new Effect.Scale('outerImageContainer', xScale, {scaleY: false, delay: resizeDuration, duration: resizeDuration});
}

$('imageDataContainer').style.width = widthNew + "px";

showImage();
}

/**
* updateDetails
*
*/
function updateDetails() {
var caption = $('lightboxImage').src.split('/').pop().split('.')[0].gsub("%20", " ");
$('imagecaption').innerHTML = caption;

new Effect.Parallel(
[ new Effect.SlideDown( 'imageDataContainer', { sync: true, duration: resizeDuration, from: 0.0, to: 1.0 }),
new Effect.Appear('imageDataContainer', { sync: true, duration: resizeDuration }) ]
);
}

/**
* showImage
* Display image and begin preloading neighbors.
*/
function showImage() {
$('imageloading').hide();
new Effect.Appear('lightboxImage', { duration: resizeDuration, queue: 'end', afterFinish: updateDetails });
}

function checkForPreloadComplete(imgPreloader) {
if (imgPreloader.complete) {
$('lightboxImage').src = imgPreloader.src;
resizeImageContainer(imgPreloader.width, imgPreloader.height);
} else {
setTimeout(checkForPreloadComplete.bind(this, imgPreloader), 100);
}
}
return {
/**
* displayImage
* display an image in a lightbox
*
* @param {String} URL of image
*/
displayImage : function(imageUrl) {
$('imageloading').show();
$('lightboxImage').hide()
$('imageDataContainer').hide()
$('photo').show();

var imgPreloader = new Image();
imgPreloader.src = imageUrl;

// once image is preloaded, resize image container
setTimeout(checkForPreloadComplete.bind(this, imgPreloader), 100);
},

reset : function() {
$('photo').hide();
$('imageloading').show();
$('lightboxImage').hide()
$('imageDataContainer').hide()
$('lightboxImage').src = "";
$('outerImageContainer').style.width = "250px";
$('outerImageContainer').style.height = "250px";


}
};
}();

The only alteration you have to do to your ImageFlow images is to add a call to the LightBoxLite.displayImage function in the longdesc attribute of your images, which should currently have the address of the original image to display when you click on it in the image flow. So, instead of:

<img src="reflect.php?img=myimage.jpg" longdesc="myimage.jpg" alt="myimage" />

It becomes:

<img src="reflect.php?img=myimage.jpg" longdesc="javascript:LightBoxLite.displayImage('myimage.jpg')" alt="myimage" />


Here is an example of it being used.

Update January 20: I tweaked a couple CSS rules to correctly center the loading indicator for Internet Explorer.

Update January 23: I received a request to package my modifications. See my new blog entry where I have done that in addition to demonstrating how to add Ajax to receive the image list.

Thursday, January 17, 2008

ImageFlow (improved)

Finn Rudolph has created a very nice "cover flow" type of control in JavaScript called ImageFlow (which is an improvement of Michael L. Perry's Cover flow).

It's a very nice package and quite easy to use! However, it currently has a few code shortcomings. My primary issues with it was that it defined everything in a global scope (with common names that easily clash with other functions) and that it assumes the photos are in a static page and thus initializes itself when the web page loads. The later issue was the biggest thing I needed to fix since I wanted to use it in an Ajax application where I'm getting the images as a result of an XMLHttpRequest.

  • I added an "ImageFlow" scoping around the whole package so that all the variables and particularly the functions aren't global to my whole application (and potentially clashing).

  • I added "var" declarations for many of the variables which didn't specify "var" so that they would not become fully global variables, but only global to the ImageFlow scope.

  • I changed the "onload" function to be an "initialize()" function so that I could execute it when my page was ready

  • I made all the variables and functions totally private to ImageFlow with the only publicly visible function being "intialize()"

  • Since the images in the image flow div are now loaded sometime after the page is loaded, I added a check to the initialize function determine when all images are really loaded (otherwise some would be sized wrong).


Now, whenever you're ready for the ImageFlow to be displayed, just call "ImageFlow.initialize()"

If you're interested in these updates, you can download my altered imageflow.js. I have also submitted them to Finn, so hopefully they will be adopted into the official version.

NOTE: The file was updated on January 18, 10:00am EST after running it through JSLint -- which I should have done before posting it originally.

Update January 18: I've added a new article describing the addition of a scaled down version of Lightbox2 without modifying ImageFlow.

Wednesday, December 19, 2007

Accordion select list

Kevin Miller has created an accordion widget using prototype and script.aculo.us. In this article I'll demonstrate how to build a selection list that has sub-sections utilizing the accordion control.

Here is a demonstation comparing a basic select list control and the accordion select list:


To make the control work, you first add a read-only text box with the drop down button image floating to the right of it, for example:

<span style="float: right">
<img id="accSelectToggle" src="images/btn_dropdown.png" width="21" height="16" alt="Expand list" title="Expand list" />
</span>
<input type="text" id="accSelect" title="List" readonly="readonly" value="Option 1.1" />


You then define a div which contains the accordion control, such as:

<div id="accSelectOptions" class="accordion_container" style="display: none">
<h1 class="accordion_toggle">Category 1</h1>
<div class="accordion_content">
<ul>
<li value="option11">Option 1.1</li>
<li value="option12">Option 1.2</li>
<li value="option13">Option 1.3</li>
</ul>
</div>
...
</div>


Add your preferred CSS styling. In my example the CSS looks like:

#accSelect {
width: 169px;
height: 14px;
border-style: none;
padding-left: 4px;
}

.accordion_container {
z-index: 10;
position: absolute;
width: 198px;
border: solid 1px black;
}

.accordion_toggle {
display: block;
padding: 0.3em 0.5em;
font-weight: normal;
font-size: 1em;
text-decoration: none;
outline: none;
border-bottom: 1px solid #def;
color: #000000;
cursor: pointer;
margin: 0px;
background-image: url('images/expand.gif');
background-position: 99% 50%;
background-repeat: no-repeat;
padding-right: 5px;
background-color: #9BE;
}

.accordion_toggle_active {
color: #ffffff;
font-weight: bolder;
background-image: url('images/ns-expand.gif');
background-color: #47B;
}

.accordion_content {
background-color: #ffffff;
overflow: auto;
display: block;
}

.accordion_content ul {
padding-left: 1em;
margin: 0;
padding-right: 0;
padding-bottom: 0px;
list-style-type: none
}

.accordion_content li {
margin: auto;
padding: 0px;
}

.highlight {
background-color: #DDD;
}


And finally add a little JavaScript to make the magic happen:

function toggleAccordionSelect(event) {
var image = event.target;
var collapseIcon = "images/btn_dropdown-selected.png";
var expandIcon = "images/btn_dropdown.png";

if ($('accSelectOptions').style.display === 'none') {
image.src = collapseIcon;
image.alt = "Collapse List";
image.title = "Collapse List";
$('accSelectOptions').show();
}
else {
image.src = expandIcon;
image.alt = "Expand List";
image.title = "Expand List";
$('accSelectOptions').hide();
}
}

function toggleHighlight(event) {
var item = event.target;
item.toggleClassName('highlight');
}

function selectAccordionOption(event) {
var selectedOption = Event.element(event);

// update the selection input field value to the selected display value
$('accSelect').value = selectedOption.innerHTML;

// internal value is stored as an attribute, do whatever you need to with it
var realValue = selectedOption.getAttribute('value');

// hide the accordion list
toggleAccordionSelect({target: $('accSelectToggle')});
}


var accordionSelect = new accordion('accSelectOptions', {
classNames : {
toggle : 'accordion_toggle',
toggleActive : 'accordion_toggle_active',
content : 'accordion_content'
},
onEvent : 'mousedown'
});

// By default, open first accordion in the drop down
var accordionToggles = $$('#accSelectOptions .accordion_toggle');
accordionSelect.activate(accordionToggles[0]);

// register event observers
Event.observe('accSelectToggle', 'mousedown', toggleAccordionSelect);
$$('#accSelectOptions .accordion_content li').each(function(accSelectOption) {
Event.observe(accSelectOption, 'mousedown', selectAccordionOption);
Event.observe(accSelectOption, 'mouseover', toggleHighlight);
Event.observe(accSelectOption, 'mouseout', toggleHighlight);
});

Aborting Ajax requests (for prototype.js)

Sometimes your slick Ajax application may have submitted a request that you no longer care about and want to abort. For example, perhaps you've implemented a type-ahead feature and the user has now typed another character prior to the first results returning. Or perhaps you fetch values for other parts of a form based on user selections and the user changed their choice prior to the first response returning.

I was surprised to see that the prototype.js library does not include an abort method for its Ajax.Request object. So, here's my implementation of Ajax.Request.abort():

/**
* Ajax.Request.abort
* extend the prototype.js Ajax.Request object so that it supports an abort method
*/
Ajax.Request.prototype.abort = function() {
// prevent and state change callbacks from being issued
this.transport.onreadystatechange = Prototype.emptyFunction;
// abort the XHR
this.transport.abort();
// update the request counter
Ajax.activeRequestCount--;
};


To use this function, you need to keep a handle on the Ajax request you want to abort. So, somewhere in you code you'll have something like:

var myAjaxRequest = new Ajax.Request(requestUrl, {[request options]});


If you want to abort that request, simply call:


myAjaxRequest.abort();


Update - Feb. 22, 2008:

It was pointed out in the comments that the Ajax.activeRequestCount can occasionally become negative. I have been able to replicate that situation, while at the same time confirming that it does not consistently happen. This leads me to believe that it's most likely a timing issue such as the abort is issued after the response has already started to be received and/or processed so that both the response processing decrements the counter as well as the abort.

My personal work-around for this is to add these lines to the end of my onComplete handler:

if (Ajax.activeRequestCount < 0) {
Ajax.activeRequestCount = 0;
}

I don't want to remove the counter decrement from the abort function or else cleanly aborted requests will leave the activeRequestCount > 0 when there are no real outstanding requests.

If anyone has a better solution, I'd be interested to hear from you.

Thursday, December 13, 2007

Performance-based Web App Functionality

In the development of some CPU intensive web applications, I've come to realize that sometimes I need to throttle down some of the features of the application in order to maintain good response times for a better user experience. So, I've added what I refer to as jsBogoMips to some of my applications.



The above example shows throttling of visual effects, but it can be used to throttle other things as well. For example, in the application I actually developed this for, while it does use script.aculo.us visual effects, what I really needed to be throttled was the generation of an SVG chart from an arbitrary amount data elements received in XML data. So, based on the jsBogoMips value, I control how many data points are charted.

If you're interested in some alterative JavaScript performance testing, you might also be interested in Celtic Kane's JavaScript Speed Test and Performance Tests for Opera 9.5.

Friday, August 17, 2007

Automatically Minimize (Minify) JavaScript

One of the 13 Simple Rules for Speeding Up Your Website is to Minify your JavaScript.

Sidenote: These rules can be easily checked using the YSlow add-on to Firebug.

The recommended tool to minify your JavaScript is JSMin. However, the default use of JSMin is to minimize your JavaScript in a separate step before you deploy your code and then have two JavaScript files - the original JavaScript you use to develop the code and the production "minified" version. For me the problem with that is the fact you have two versions to keep track of and keep in sync. Plus before you deploy your code, you need to make sure all your HTML pages use the minified filename not the original version.

Fortunately, there's a port of JSMin to PHP which allows you minify your JavaScript on the fly, but the usage instructions make it appear that you need to change your script source to point to separate PHP file for each JavaScript, or have a single PHP with all your JavaScript files hard-coded in it -- which is a nice option to get multiple JavaScript files with a single request.

What I'm going to show in this post is how to use jsmin-php to create a filter on your server so that all your JavaScript files are transparently minified as your retrieve them.

First off, download the current version of jsmin-php and save it in your directory. As the current version at the time of this writing is jsmin-1.1.0.php, I'll assume that is the file name.

Next, save the script below as jsmin.php:

<?php
// PHP filter to invoke JSMin on all JavaScript files
// see http://code.google.com/p/jsmin-php/
require_once('jsmin-1.1.0.php');

$js_file = pathinfo($_SERVER['ORIG_PATH_INFO']);
$js_path = substr($js_file['dirname'], 11);
$js_file = $js_file['basename'];

$charset = "utf-8";
$mime = "text/javascript";

header("Content-Type: $mime;charset=$charset");

// Output a minified version of JavaScript file
if (file_exists($js_file)) {
echo JSMin::minify(file_get_contents($js_file));
} else {
echo JSMin::minify(file_get_contents($js_path . '/' . $js_file));
}

?>


Note: If you change the name of this script, you'll probably need to change the 11 on the $js_path definition. That rips the script name (/jsmin.php/) off the beginning of the incoming path on the request so I can use a relative path to find scripts in a subdirectory. It should be the length of the script name plus 2 (for the leading and trailing '/'s).

Now, update your .htaccess file and add the lines:

# make all JavaScript files be minimized via JSMin
AddHandler jsmin .js
Action jsmin /jsmin.php


That should be it! If you fetch a JavaScript file from your server, it should now be minified.


Here are a few quick additions you could add to make YSlow happy.

At the beginning of jsmin.php, adding the line:

ob_start("ob_gzhandler");

should GZIP the output for you.



Also, where the Content-Type header is set, adding the following lines will add an expiration of 49 hours (needs to be > 48 hours to by sufficiently "far future" to make YSlow happy. If you really want to cut it close you could use (60 * 60 * 48) + 1):

$offset = 60 * 60 * 49;
$ExpStr = "Expires: " . gmdate("D, d M Y H:i:s", time() + $offset) . " GMT";
header($ExpStr);

Tuesday, August 14, 2007

Synthesizing Events in JavaScript

According to JavaScript: The Definitive Guide (4th ed), section 19.2.9 on Synthesizing Events, it says,

Unfortunately, at the time of this writing, the synthetic event API is not supported by Netscape 6, nor by the current version of Mozilla



But, it does work in the current version of Firefox (and IE). I had a situation where I needed to simulate events, so I wrote the following function to make it simple:


// simulateEvent
//
// simulate a user action
//
function simulateEvent(eventType, targetElement) {
var event;
targetElement = $(targetElement);

if (targetElement) {
// check for IE
if (window.ActiveXObject) {
event = document.createEventObject();
targetElement.fireEvent("on"+eventType,event);
} else {
switch (eventType) {
case "abort":
case "blur":
case "change":
case "error":
case "focus":
case "load":
case "reset":
case "resize":
case "scroll":
case "select":
case "submit":
case "unload":
event = document.createEvent("HTMLEvents");
event.initEvent(eventType, "true", "true");
break;
case "click":
case "mousedown":
case "mousemove":
case "mouseout":
case "mouseover":
case "mouseup":
event = document.createEvent("MouseEvents");
event.initMouseEvent(eventType, true, true, window, 1, 0, 0, 0, 0, false, false, false, false, 0, null);
break;
}
targetElement.dispatchEvent(event);
}
}
};

Thursday, March 08, 2007

Top Web Technologies

Jim Rapoza of eWeek compiled his list of the Top Web Technologies of All Time as a collection of slides.

Here is his list:

  • XML
  • HTML
  • Netscape Navigator
  • HTTP
  • Apache
  • NCSA Mosaic
  • CERN httpd
  • Internet Explorer 3
  • NCSA httpd
  • Firefox
  • SSL
  • ViolaWWW
  • WAIS
  • CGI
  • Internet Information Server
  • Squid
  • Java
  • HotMetal
  • Flash
  • PHP
  • Dreamweaver
  • RSS
  • WebTrends
  • Blogger
  • PlaceWare
  • Lynx
  • Perl
While I would definitely concur on many items on the list, there are others that I would disagree with or substitute.

Regarding the standardized protocols and languages: XML, HTML, HTTP, RSS, SSL, Java, PHP, Perl, RSS, and even Flash, I have no objections. They are certainly the foundations of the web. However, I do think he's missing a biggie of JavaScript which, while it had a dubious start and I avoided it for quite a while, is shaping out to be the scripting language of Web 2.0.

Regarding his list of browsers: NCSA Mosaic, Netscape Navigator, and Internet Explorer, Firefox? Absolutely. ViolaWWW? I don't think so. Yes, it came out before NCSA Mosaic, but Mosaic was really more responsible for bringing the WWW to the masses, and if you really want to show the start of the web browsers, why not point to the actual original browser named WorldWideWeb by Tim Berners-Lee? Spyglass? Come on, it's just a commercial re-branding of NCSA Moasic. Yes, it became Internet Explorer, but you could just as well say NCSA Moasic became Internet Explorer and not bother to mention this interim step. Similarly, Netscape was first called Mosaic Netscape, so do we need to list that too? Is wasn't really significant in itself. Lynx? Sure, it comes in handy from time to time, but I don't think it's a top web technology. Opera? It's a nice browser and all, but I don't think it's ever made that much of an impact overall. Yes, it introduced some new ideas like mouse gestures, but I just don't think it was that significant. Why not Safari for the Mac, or Galleon or Konqueor on Linux? Same thing. They're nice, but more of an also ran than a top web technology.

Then we have our web servers: CERN httpd and NCSA httpd which begot Apache, no question. IIS? I guess in some circles, but Apache is still significantly more important. If it's on his list because it provides the .Net platform, what about Tomcat, JBoss, or other commercial products like WebSphere and BEA for J2EE? I don't think I'd put IIS on the list, but if I did, I think I'd be remiss not to include Tomcat or some of the other Java application servers.

Next are HTML editors HotMetal and DreamWeaver. Personally, I think the Netscape/Mozilla Composer and/or FrontPage had a wider impact toward bringing HTML editing and composition to the masses originally. Jim makes the comment that, nearly all serious Web developers now use Dreamweaver, but I know plenty of serious web developers who don't use Dreamweaver so I think that was a personal bias on his part.

Finally he have various web services like WAIS and Blogger. WAIS had it's place as a search technology (though more familiarly recognized as Gopher), but I think Yahoo and subsequently Google had a more significant impact in organizing the Web to make it useful to the masses. It's hard to classify it as a Web technology when Gopher/WAIS was essentially replaced by the web. Blogger is certainly important and can stay on the list, but if it is, I think some other services should also be on the list. For example, Yahoo's provision for free email (as well as Hotmail, GMail, and others) was more important toward getting people to use the web for online services. Also, services like MySpace are more significant toward advancing the collaboration ideas of Web 2.0. And is PlaceWare really more significant than all the other similar solutions? Why not CUSeeMe or NetMeeting?

Also, what about other key technologies that make the web work like databases (MySQL anyone?).

Just my thoughts. Any other suggestions?

Thursday, March 01, 2007

Simple draggable HTML using prototype.js

I know there are plenty of JavaScript packages out there to allow you to create draggable elements on your HTML page, but to me, most of them appear to be overly complicated for a simple function and are often part of a larger framework that you may not need.

The solution I'm providing today does depends on the prototype.js package (I'm using version 1.5 currently) which allows the code size to stay smaller.

Here's how to make any HTML element draggable in 5 easy steps.

  1. Add the class of draggable to whatever you want to move. Actually, you can use any class identifier you want, but draggable is what I'm looking for in my code.

  2. Give your draggable element a position style (position: absolute; or position: relative;) and an initial top and left value. Generally, you'd want to put this in your styles in an external CSS with the draggable class, but the top and left values need to be set as an attribute in the HTML. Hint: setting the position to relative with the top and left values set to 0px will allow it to start located where it normally would be in the browser. You'll probably want to give it a higher z-index and solid background-color as well, but that's up to you.

  3. Include the 3 drag event handling functions I'll describe below in your code somewhere

  4. Register all your draggable elements to the event listeners.

  5. Have fun dragging window elements around.


The event handlers


Before you can use the following functions, you need to define some variables to hold some information during the drag. If you make the subsequent functions part of another containing object, you can simply prepend all occurrences of these variables in the functions with this. and they will be stored in the containing object instead.

var dragObj;
var cursorStartX;
var cursorStartY;
var elementStartX;
var elementStartY;
var dragGofn;
var dragStopfn;

startDrag

startDrag will initialize a mouse drag event. It captures the initial location of the mouse when beginning the drag in order to calculate movement later. It also captures the current coordinates of the dragged element for correct positioning as the location of the mouse are most likely not the origin of the element being dragged. Finally, it associates the additional event observers to the element to capture the mouse movement (onmousemove) and release of the mouse button (onmouseup).

function startDrag(event) {
dragObj = Event.element(event);
// if the target of the event is not a "draggable" element, then search
// through it's parents for an element that is draggable.
// if we can't find a draggable element before reaching the root document
// element, then bail out
while (!Element.hasClassName(dragObj,"draggable")) {
dragObj = $(dragObj).up('.draggable');
}
cursorStartX = Event.pointerX(event);
cursorStartY = Event.pointerY(event);
elementStartX = parseInt(dragObj.style.left, 10);
elementStartY = parseInt(dragObj.style.top, 10);

Event.observe(dragObj, 'mousemove', dragGofn);
Event.observe(dragObj, 'mouseup', dragStopfn);
}

dragGo

Once the drag event has been initiated by the startDrag function, the dragGo function will update the location of the element being dragged so that it moves around the screen with the mouse.

function dragGo(event) {

var x, y;
x = Event.pointerX(event);
y = Event.pointerY(event);

// move the target object
dragObj.style.left = (x - cursorStartX + elementStartX) + "px";
dragObj.style.top = (y - cursorStartY + elementStartY) + "px";

Event.stop(event);
}

dragStop

Once the user has release the mouse button, we need to stop observing the mouse movements, so the dragStop function will un-register the event handlers.

function dragStop(event) {
// Stop capturing mousemove and mouseup events.
Event.stopObserving(dragObj, 'mousemove', dragGofn);
Event.stopObserving(dragObj, 'mouseup', dragStopfn);
dragObj = null;

Event.stop(event);
}

Registering the draggable elements


You're almost ready to start moving items on the screen, but first you have to register all your draggable elements to the startDrag function so that they will be notified of the drag events. So, we utilize prototype's element-by-classname selector to find all the draggable elements and associate the event listener to them.

Note: in order to un-register the event listeners, you have to have the handle to the event listener you used to register it originally. Thus, we used the variables dragGofn and dragStopfn to register and un-register the event listeners. These variables are defined here during the original event registration.

function registerEvents() {
dragGofn = dragGo.bindAsEventListener();
dragStopfn = dragStop.bindAsEventListener();

$$(".draggable").each(function(draggableElem) {
Event.observe(draggableElem, 'mousedown', startDrag.bindAsEventListener(), true);
});

}


Note: the final true on the Event.observe tells prototype to request capturing instead of bubbling. This is necessary for this function to work in Safari (see Kir's blog)

Monday, February 26, 2007

Adding a Processing indicator with Prototype.js

Here's a simple method to add a Processing/Loading/Working indicator to indicate when an Ajax request is being processed with prototype.js.

First, define a hidden div (or other preferred HTML element) which will be displayed to indicate that some background processing is going on. Mine happens to look like:

<div id="loading" style="display:none">Loading...
<div style="background-color: white; position: absolute; top: 0px; right: 0px; width: 5px; height: 5px; cursor:pointer;"
onclick="$('loading').hide()"></div>
</div>

In my code, I added another div on the upper right corner which I can click to close this indicator in case something goes wrong with the Ajax event handling which I'll provide in a moment.

I then added a little CSS magic to make it look the way I wanted. In my case, I want the indicator to be on top of everything, centered on the page, with a processing image () on the right side of the text. The background color is white with a double gray border and large, bold font.

#loading {
z-index: 100;
position: absolute;
top: 40%;
left: 40%;
background-image: url("../images/progress-running.gif");
background-repeat: no-repeat;
background-position: 5px;
background-color: white;
padding-left: 25px;
padding-top: 8px;
border-style: double;
border-color: #c0c0c0;
width: 120px;
height: 30px;
font-size: 1.5em;
font-weight: bolder;
}


Ok, so now I have the indicator I want to popup. This can be tailored to your preferences. Now, you simply tell prototype to display it whenever it is processing an Ajax request. This is done by registering some event listeners to the prototype Ajax class.

// register event listeners on the Ajax requests to show/hide the processing indicator
Ajax.Responders.register({
onCreate: function() {
if (Ajax.activeRequestCount === 1) {
$('loading').show();
}
},
onComplete: function() {
if (Ajax.activeRequestCount === 0) {
$('loading').hide();
}
}
});


That's it! Now you should get an indicator whenever you issue an Ajax request that goes away whenever the processing is done.

Friday, February 23, 2007

Cookies and Flash for local storage

In previous posts I provided code for using cookies in JavaScript. I've now enhanced that code to utilize the flash storage mechanism which was described by Christoph Khouri, however his page seems to be down currently, so I'll provide the files you need. Note: this is the same technique used by the Dojo Storage module, without the need for Dojo which adds extra overhead costs.

Using flash storage alleviates the 4k size limit of cookies and initially provides 100k of storage seamlessly and can be increased to any size by the user. It also eliminates unnecessary network traffic as all the cookies are transmitted to the server all the time, and makes it harder for prying eyes to see what's being stored as vs browsing the cookies in the browser.

In order to use the flash storage, first, download flashStorage.zip which came from Christoph Khouri's site. Next, download my current cookies.js file.

Now, include the flashStorage JavaScript and my cookies.js JavaScript in the header of your HTML page:

<script type="text/javascript" src="jscript/flashStorage/swfobject.js">lt;script>
<script type="text/javascript" src="jscript/flashStorage/flashStorage.js"><script>
<script type="text/javascript" src="jscript/cookies.js"></script>


Next, somewhere in the body your page add the following code:

<div style="float: right;" id="storageHolder">
<embed type="application/x-shockwave-flash"
src="jscript/flashStorage/storage.swf" id="storageMovie" name="storageMovie" bgcolor="#ffffff" quality="high"
swliveconnect="true" wmode="transparent" height="137" width="214" />
</div>
<script type="text/javascript">
var so = new SWFObject("jscript/flashStorage/storage.swf", "storageMovie", "50", "10", "8", "#ffffff");
so.addParam("swLiveConnect", "true");
so.addParam("wmode", "transparent");
so.write("storageHolder");

//Initialize the flash storage by passing the id of the flash movie
Storage.init("storageMovie");
</script>


That's it! Now whenever you create a cookie like Cookies.create("myCookie", "myValue") it will actually store that in the flash storage instead. However, if you don't wish to include the Flash storage component, it will default to store the value in a browser cookie instead.

Maybe sometime in the future I'll add detection to see if the Flash plugin is installed in addition to the Storage object and default to standard cookies if Flash isn't installed.
Update: I've added a check for the Flash plugin being installed as a pre-requisite to using the Flash storage object in addition to the check for the JavaScript code/object being included. FYI, my check for Flash for those needing to do the same thing is:

var hasFlash = (navigator.plugins &&
(navigator.plugins["Shockwave Flash"] || navigator.plugins["Shockwave Flash 2.0"])) ||
(navigator.mimeTypes &&
navigator.mimeTypes['application/x-shockwave-flash']);

Ajax Cross-Domain Issue

A common solution to the XMLHttpRequest cross-domain issue (aka cross site scripting or XSS), where you can only communicate with the originating domain, is to configure Apache to use either mod_proxy or mod_rewrite for URL rewriting and a pass-through proxy.

With Apache you need to enable the proxy functions (configure --enable-proxy). If you want to handle it all with mod_proxy, then define you proxy settings in httpd.conf such as:

ProxyPass /doExt http://external.com/doAction
ProxyPassReverse /doExt http://external.com/doAction

If you want to use mod_rewrite then turn on mod_rewrite in httpd.conf or .htaccess with RewriteEngine on and define your rewriting rules such as:

RewriteRule ^/doExt$ http://external.com/doAction [P]

Then when you post to your server via /doExt, Apache will pass the request on to the external site.

That's all well and good if you have access to the Apache configuration or the server is already configured with the proper modules. But, when your on a hosted environment, that's not always feasible. Another solution would be to provide a proxy servlet to make the remote request for you and return the result. What I am providing is in Java and PHP, but it could be written you language of choice.

Java proxy


The following servlet class will invoke some external URL on behalf of the original request and return the result from the external request as the result of the original local request. Note: In the following code I hard-coded the real destination. However, it could be determined from a parameter in the original request just as easily.


import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.URL;
import java.util.Enumeration;

/**
* This is a simply proxy class in order to bypass AJAX security contraints.
*
*/

public class ServerProxy extends javax.servlet.http.HttpServlet {

private static final long serialVersionUID = 1L;

public void doGet(javax.servlet.http.HttpServletRequest request,
javax.servlet.http.HttpServletResponse response)
throws javax.servlet.ServletException, java.io.IOException {
performTask(request, response);
}

public void doPost(javax.servlet.http.HttpServletRequest request,
javax.servlet.http.HttpServletResponse response)
throws javax.servlet.ServletException, java.io.IOException {
performTask(request, response);
}

public void performTask(javax.servlet.http.HttpServletRequest request,
javax.servlet.http.HttpServletResponse response)
throws javax.servlet.ServletException, java.io.IOException {

// Get the external URL to invoke
String realUrl = "http://www.external.com";

// Copy the request parameters from the original request
Enumeration paramNames = request.getParameterNames();
String paramName;
while (paramNames.hasMoreElements()) {
paramName = (String) paramNames.nextElement();
realUrl = realUrl.concat(paramName + "="
+ request.getParameter(paramName));
if (paramNames.hasMoreElements()) {
realUrl = srmSoaUrl.concat("&");
}
}

// real URL will be returning XML data
response.setContentType("text/xml");
PrintWriter out = response.getWriter();

// invoke the real URL and copy the result into the response
// for the original request
URL real = new URL(realUrl);
BufferedReader in = new BufferedReader(
new InputStreamReader(real.openStream()));
String inputLine;
while ((inputLine = in.readLine()) != null)
out.println(inputLine);
in.close();

return;
}

}


PHP proxy


Here is a similar solution using PHP:

$remoteServer = "http://www.external.com";
$path = $_GET[”path”];
$proxyTarget = $remoteServer.$path;
$connection = curl_init($proxyTarget);
curl_setopt($connection, CURLOPT_HEADER, false);
curl_setopt($connection, CURLOPT_RETURNTRANSFER, true);
$data = curl_exec($connection);
curl_close($connection);
header(”Content-Type: text/xml”);
echo $data;

Monday, January 15, 2007

Erasing all your cookies

In a previous post I provided some [non-original] code to do basic cookie manipulation in JavaScript. Well, if you're now using cookies to save a whole bunch of user settings and preferences for your web application, you may want to provide an option to let users clear all their saved settings (or perhaps just provide a hidden hot-spot so you can clear your settings for testing purposes which is why I needed this function).

You could keep track of all your cookie names and call the eraseCookie function repeatedly yourself for each cookie. Or, you could name all your cookies with a common prefix for your whole application and use the following function to clear them all out for you.


function eraseCookiesStartingWith(prefix) {
var allCookies = document.cookie.split(';');
for(var i=0;i < allCookies.length;i++) {
var aCookie = allCookies[i];
// remove any whitespace from start of cookie name
while (aCookie.charAt(0)==' ') aCookie = aCookie.substring(1,aCookie.length);
var aCookieName = (aCookie.split('='))[0];
if (aCookie.indexOf(prefix) == 0) {
createCookie(aCookieName,"",-1);
}
}
}

Wednesday, November 08, 2006

Using XML, XPath, and XSLT with JavaScript

There are several JavaScript XML libraries available to try to provide a cross-browser XML library. I've previously cited zXML in my Inline SVG article, there's also Sarissa and XML for <Script> and I'm sure others.

However, when I started using them I discovered various problems and limitations with them for what I needed to do. So, I decided to write my own cross-browser XML routines. I wanted an XML library that was as lightweight as possible, doing just what I needed and utilizing as much of the built in browser capabilities as possible.

In this article, I will first present a simple XML class which just holds the XML DOM object returned from an AJAX request and provides functions to get and change node values based on XPaths. Secondly, I will give a simple XSLT class to preform XSL Transformations.

The complete code for the XML and XSL library is available here

XML


First off is the constructor which determined if we will be using IE (ActiveX) functions or W3C standard functions for XML manipulation and either stores the XML DOM returned as the responseXML from an XMLHttpRequest(XHR) or creates an empty DOM object for later manipulation.
//
// XML
//
function XML(xmlDom) {
this.isIE = window.ActiveXObject;
if (xmlDom != null) {
this.xmlDom = xmlDom;
} else {
// create an empty document
if (this.isIE) {
Try.these (
function() { axDom = new ActiveXObject("MSXML2.DOMDocument.5.0"); },
function() { axDom = new ActiveXObject("MSXML2.DOMDocument.4.0"); },
function() { axDom = new ActiveXObject("MSXML2.DOMDocument.3.0"); },
function() { axDom = new ActiveXObject("MSXML2.DOMDocument"); },
function() { axDom = new ActiveXObject("Microsoft.XmlDom"); }
);
this.xmlDom = axDom;
} else {
this.xmlDom = document.implementation.createDocument("", "", null);
}
}
};

In case you're not getting the XML data from an XHR response, I also provided a load function to load an XML file from an URL.
// load
//
// Loads an XML file from an URL
XML.prototype.load = function(url) {
this.xmlDom.async = false;
this.xmlDom.load(url);
};

Next, I want to be able to find a single node in the XML DOM based on an XPath. The getNode function will do that for me.
// getNode
//
// get a single node from the XML DOM using the XPath
XML.prototype.getNode = function(xpath) {
if (this.isIE) {
var result = this.xmlDom.selectSingleNode(xpath);
} else {
var evaluator = new XPathEvaluator();
var result = evaluator.evaluate(xpath, this.xmlDom, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
}
return result;
};

More often, I don't need the actual DOM node object, but the value of that node, so I provide the getNodeValue a function to get the value of a node.
// getNodeValue
//
// get the value of a the element specified by the XPath in the XML DOM
XML.prototype.getNodeValue = function(xpath) {
var value = null;
try {
var node = this.getNode(xpath);

if (this.isIE &&amp; node) {
value = node.text;
} else if (!this.isIE && node.singleNodeValue) {
value = node.singleNodeValue.textContent;
}
} catch (e) {}

return value;
};

If you're just looking for a particular node/value, the previous functions are fine, but more often you need a group of node or node values (ex. all the book titles). So, the full JavaScript file also contains corresponding functions to get multiple nodes (getNodes) and node values as an array (getNodeValues).

At some point, you'll probably want to access this XML DOM as XML data (string), so we need a function to serialize the XML from the DOM to a String. The getNodeAsXml function will do that for you. If you pass in the root XPath ("/") it will serialize the complete XML file. Also, the included prettyPrintXml function can be used to escape the HTML characters.
// getNodeAsXml
//
// get the XML contents of a node specified by the XPath
XML.prototype.getNodeAsXml = function(xpath) {
var str = null;
var aNode = this.getNode(xpath);
try {
if (this.isIE) {
str = aNode.xml;
} else {
var serializer = new XMLSerializer();
str = serializer.serializeToString(aNode.singleNodeValue);
}
} catch (e) {
str = "ERROR: No such node in XML";
}
return str;
};


So far, we've only read the XML data we received. But we may also want to change the contents of the XML. So, here are the functions necessary to change existing node values (updateNodeValue