Saturday, November 14, 2009

Disabling Tabs in a dijit.layout.TabContainer

I've started using tab containers a little more recently. Looking through the dojocampus articles and dojo api docs, it looks like there isn't an API for disabling clicks on a particular tab. I'm not sure why this is the case - perhaps it has something to do with accessibility (I've always had intentions of learning more about accessibility on the web, but I've never really had the time)

At any rate, here's a quick and simple solution to disabling tabs in a tab container:
dojo.forEach(dijit.byId("myTabContainer").tablist.getChildren(), dojo.hitch(this, function(item, index, array) {
dojo.attr(item, "disabled", true);
item.onClick = function() {};
}));

Unfortunately, there doesn't seem to be a simple way to re-enable these clicks. Also, it'd be really sweet if you could simply hide some of the tabs in a tab container, but it sounds like you can only add/remove them.

Sunday, September 13, 2009

dojo updates

You gotta give the dojo guys credit. Thanks to @uhop for following up on my previous blog post. Even though my bug was pretty edge case, its great to see the dojo community going the extra mile to fix it. Its small things like this which make all the difference.

On another dojo related note, I attended the DDD at AOL in Mountain View on Thursday. Always good times hanging out with the dojo crew. Some really cool stuff in the pipeline too. It was great to get some samples of the dojo/django collaboration happening with Tobias and Mike's drawing samples were really cool too - I didn't know stuff like that was possible in the browser. Looking forward to dojo 1.4 already. Rob Christiansen from Adobe AIR popped in too - sounds like there's a lot in the pipeline for AIR 2.0. Maybe the dair dojo project will get rejuvenated with the arrival of AIR 2.0 and dojo 1.4

Wednesday, September 2, 2009

dojo drag and drop in AIR

Looking at common.js in the dojo.dnd package, I see this nice utility function:
Paste your text here.dojo.dnd._isMac = navigator.appVersion.indexOf("Macintosh") >= 0;
dojo.dnd._copyKey = dojo.dnd._isMac ? "metaKey" : "ctrlKey";

dojo.dnd.getCopyKeyState = function(e) {
// summary: abstracts away the difference between selection on Mac and PC,
// and returns the state of the "copy" key to be pressed.
// e: Event: mouse event
return e[dojo.dnd._copyKey]; // Boolean
};

However, this doesn't seem to work in AIR (only tested running on mac so far).

Switching the code to look like:
dojo.dnd._isMac = navigator.appVersion.indexOf("Macintosh") >= 0;
dojo.dnd._isAir = navigator.appVersion.indexOf("AdobeAIR") >= 0;
dojo.dnd._copyKey = dojo.dnd._isMac && !dojo.dnd._isAir ? "metaKey" : "ctrlKey";

seems to do the trick

Tuesday, June 16, 2009

IE 6 usage ... boooournn

I was curious to see how the announcement that IE 8 would be included in an automated update would affect the Google Analytics for the application I work on. I took a months sample of IE usage before and after the announcement just to see how the number stacked up. Here's what I found:

BEFORE:
7.0 68.18%
6.0 20.50%
8.0 11.27%

AFTER:
7.0 58.39%
6.0 21.96%
8.0 19.59%

Unfortunately, I don't see IE 6 disappearing any time soon. Neither does ppk.

Heh ... unless this tricks a few users into upgrading.

In other browser news, I just saw this site springing up: http://dowebsitesneedtolookexactlythesameineverybrowser.com/

Wednesday, June 3, 2009

Idle Handler with dojo

After reading Nicholas Zakas' blog posting about handling idle listeners with YUI, I decided to take a bash at porting his logic to dojo. It turned out to be pretty simple - here's the code:
dojo.declare("my.IdleListener", [], {

_mouseMoveHandle: null,
_keyDownHandle: null,
_timeout: 30000,

isRunning: function() {
return this._enabled;
},

isIdle: function() {
return this._idle;
},

start: function(newTimeout) {
this._enabled = true;
this._idle = false;
if (typeof newTimeout == "number") {
this._timeout = newTimeout;
}
this._mouseMoveHandle = dojo.connect(dojo.doc, "onmousemove", this, "_handleUserEvent");
this._keyDownHandle = dojo.connect(dojo.doc, "onkeydown", this, "_handleUserEvent");
// set a timeout to toggle state
this._idleTimeout = setTimeout(dojo.hitch(this, "_toggleIdleState"), this._timeout);
},

stop: function() {
this._enabled = false;
// clear any pending timeouts
clearTimeout(this._idleTimeout);
// detach the event handlers
dojo.forEach([this._mouseMoveHandle, this._keyDownHandle], function(item, index, array) {
if(item) {
dojo.disconnect(item);
}
});
},

_handleUserEvent: function() {
// clear any existing timeout
clearTimeout(this._idleTimeout);
if (this._enabled) {
// if the user is just waking us up again, toggle the idle state.
// otherwise, reset the timeout with a new timeout
this._idle ? this._toggleIdleState() : this._idleTimeout = setTimeout(dojo.hitch(this, "_toggleIdleState"), this._timeout);
}
},

_toggleIdleState: function() {
this._idle = !this._idle;
this._idle ? dojo.publish("idle", []) : dojo.publish("active", []);
}
});

To start your idle handler with a default timeout of 30 seconds, all you need to do is:
 var idleListener = new my.IdleListener();
idleListener.start(30000);

Then in whatever parts of your application deal with polling network resources, all you need to do is:
dojo.subscribe("idle", function() {

});
dojo.subscribe("active", function() {

});

For jQuery users, Paul Irish has ported the code. too.

Just posted an update of this code to github. Check it out!

Tuesday, May 19, 2009

Migrating to dojo 1.3

Recently enough I just pushed some software which included an upgrade to dojo 1.3. Prior to this, I was using dojo 1.0.2, so this migration to the latest version of dojo represented around 2 years of work from the dojo guys.

Having previously migrated from dojo 0.4.3 to a dev build of dojo 0.9 to dojo 1.0.2, I've had my fair share of experience with dojo upgrades. Having previously worked on framework code in a prior job, I'm always ready to try out the latest a greatest versions of frameworks. My main reasons behind the migration to dojo 1.3 were:

1. Official support for Chrome and IE 8.

2. Speed speed speed. I'm not sure what the numbers are like in taskspeed for dojo 1.0.2, but the 1.3 numbers stack up pretty well against some of the major JS toolkit vendors on the market at the moment.

3. I wanted to start exploring some of the new APIs available in dojo 1.3 such as dojo.data, BusyButton, dojo.place and dojo.create

4. Start exploring the dair extensions for integration between dojo and Adobe AIR.

5. Testing. One of the areas where the application I work on is definitely deficient is automated testing. I wanted to start looking into DOH and the dojo robot.

So, I had plenty of reasons to migrate and the move couldn't have gone easier. I honestly only got stung once where once of my custom widgets had a member variable called _created which stepped on the _created member variable in dijit._Widget. The vast majority of headaches were more related to svn and importing the 1.3 version of dojo into my repository than actually migrating my code to use the new dojo APIs. Kudos to the dojo dev team for keeping such a clean and consistent API.

Already looking forward to the 1.4 version of dojo ...

Wednesday, May 6, 2009

plugd and Google Maps

I just started looking into integrating Google Maps into an application. Given the breath of features offered by Google Maps, its no surprise that the JavaScript download is pretty hefty -> ~72KB with an empty cache for main.js. Also, including Google Maps in a page brings down around 20 extra images (depending on the default size of your map).

So, I started looking into ways for deterministically loading Google Maps as there are certain sections of the application I work on where Google Maps is not needed. Looking at the Google Ajax APIs, this seems like the easiest way to load Google Maps (and other Google APIs for that matter):


<script type="text/javascript"
src="http://www.google.com/jsapi?key=ABCDEFG"></script>

...

google.load("maps", "2");


Adding plugd into the mix, you can also cut down on loading the Google Ajax API JavaScript using the addScript method:


dojo.addScript("http://www.google.com/jsapi?key=" + key, dojo.hitch(this, function() {
var mapsLoaded = function() {
dojo.publish("/gmaps/loaded", []);
}
var langArray = dojo.locale.split("-");
google.load("maps", "2", {"callback" : mapsLoaded, "language": langArray[0] + "_" + langArray[1].toUpperCase()});
}));


And code which depends on Google Maps being loaded should listen to the "/gmaps/loaded" topic before trying to access any Google Maps APIs.

Tuesday, April 14, 2009

Connecting labels to inputs using dojo

When using dijit, you have to be careful not to include any id elements in your template HTML file. Otherwise, you can end up with elements with the same id in your document which can lead to a lot of head scratching. Having been bitten by the "multiple elements having the same id in my document" bug, I started stripping out all ids from my template files, replacing them with dojoAttachPoint attributes.

All was going fine and I wasn't getting mixed up in id clashing anymore. However, there was one drawback - my labels wouldn't work anymore. Code which previously looked like this:

<li>
<input id="simple" type="radio" dojoattachevent="onclick: simpleClicked"/>
<label for="simple">${statics.i18n.simple}</label>
</li>

no longer worked because it had changed to this:

<li>
<input dojoAttachPoint="simple" type="radio" dojoattachevent="onclick: simpleClicked"/>
<label for="simple">${statics.i18n.simple}
</li>

The label's for attribute expects to reference an id and not a dojoAttachPoint. Previously I was able to toggle the 'simple' checkbox input by clicking on the label. I always tend to click on labels instead of radio/checkboxes (mostly because they're bigger and easier to click), so I saw this move away from id-based inputs as a degradation in the usability of the web application.

So, I hacked together a piece of code which is invokable after every widget is set up and appended to the dom:

connectLabelsToInputs: function(query, widget) {
dojo.query(query, widget.domNode).forEach(function(item, index, array) {
if(widget[item.getAttributeNode("for").value] && (widget[item.getAttributeNode("for").value].type == "checkbox" || widget[item.getAttributeNode("for").value].type == "radio")) {
var attrs = widget[item.getAttributeNode("for").value].attributes;
for(var i = 0; i < attrs.length; i++) {
if(attrs[i].value.indexOf("onclick") != -1) {
dojo.addClass(item, "clickable");
dojo.connect(item, 'onclick', widget, dojo.trim(attrs[i].value.substring(attrs[i].value.indexOf(":") + 1, attrs[i].value.length)));
}
}
if(!dojo.hasClass(item, "clickable")) {
dojo.addClass(item, "clickable");
dojo.connect(item, "onclick", dojo.hitch(widget, function() {
widget[item.getAttributeNode("for").value].checked = widget[item.getAttributeNode("for").value].checked == false ? true : false;
}));
}
}
}, widget);
}

This code makes the assumption that you have a class called 'clickable' which looks something like this:

.clickable {
cursor: pointer;
}

So, if I know that a widget I just created contains labels and checkboxes or radio buttons, all I need to do is invoke:

common.connectLabelsToInputs("fieldset ol li label", this);

and my labels are clickable again.

Sunday, April 12, 2009

dijit.Dialog's underlay

dijit.Dialogs are really handy for getting the user's attention. However, they can be a little intrusive if they're overused (think annoying JavaScript popups) as they take control away from the user.

Its always advisable to give the user a way out of the dialog. A cancel button, or the little x in the top right hand corner of the dialog should be visible at all times. For whatever reason though (more than likely my CSS skills suck), the little x at the top of the dialog box sometimes disappears behind a scroll bar in IE after I resize the dialog.

One escape route which should always be visible to the user is the underlying web page. Should they click on the underlay, you could interpret that interaction as the user wanting the dialog to disappear. Here's some dojo code I cooked up to allow that interaction to happen:


handleOverlayClick: function(dialogName) {
if(!this._dialogHandles) {
this._dialogHandles = new dojox.collections.Dictionary();
}
if(!this._dialogHandles.item(dialogName)) {
this._dialogHandles.add(dialogName, dojo.connect(dojo.byId(dialogName + "_underlay"), "onclick", dijit.byId(dialogName), "hide"));
dojo.connect(dijit.byId(dialogName), "hide", dojo.hitch(this, function() {
// cleanup
dojo.disconnect(this._dialogHandles.item(dialogName));
this._dialogHandles.remove(dialogName);
}));
}
}


Thanks to @phiggins on the dojo IRC channel for help with this.

Sunday, March 29, 2009

Internet Explorer fun and games

This is a kind of mixed bag posting on a few IE issues I came across recently:

IE 8 Released
So, IE 8 was released recently. At the SXSW CSS3 wars talk, Sylvain Galineau of Microsoft spoke about the ~7000 w3c CSS2 tests MS have passed as part of their standards push for IE 8. Internet Explorer gets a lot of bad press (man, I'm sick of basing IE), but hopefully this will be a turning point for future releases of Internet Explorer. Cool to hear that IE 8 is now the most CSS 2.1 compliant browser on the market. Wonder when it'll be pushed as an automatic update.

IE 8 Released - my pages look like sh1t
Buy yourself some time with the IE 7 meta-tag:


<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7"/>


Checkboxes
So, dynamically creating checkboxes in IE 6 is a little different to every other browser I develop for. For whatever reason, I couldn't get my checkbox to be checked by default. That is until I found out about the defaultChecked property.

Apparently, if you try to set the checked property of a checkbox before you add it to the DOM in IE 6, IE6 drops the checked property and sets the checkbox as unchecked by default. But, if you use the defaultChecked property, it'll heed your intentions and hey presto, your checkbox is checked.

Web SuperPreview
Speaking of IE, Web Super Preview looks like a good tool. Kinda like IE on steroids.

Friday, March 13, 2009

Running Wireshark on MAC OSX




If I have to open Wireshark at all, I know I'm in trouble. Its a fantastic tool, but as a web developer, I default to Firebug. I've only ever really needed Wireshark for debugging IE network issues.

I've been taking a look at Adobe AIR recently which has a very similar Firebug like console called AIRIntrospector. However, when attempting to execute Jsonp requests in Adobe AIR, my timeout callbacks were being invoked and the requests weren't showing up in the "XHR" tab in AIRIntrospector. This was despite the fact that I could see the apache logs filling up with requests on the API server I was trying to hit.

So, I tried firing up Wireshark to listen and inspect the response from the API server only to be greeted with:


The capture session could not be initiated ((no devices found) /dev/bpf0: Permission denied).


After a few quick Googles, I found this helpful post. Apparently, you need to start Wireshark as root. So,


$cd /Applications/Wireshark.app/Contents/MacOS
$sudo ./Wireshark


After that quick sudo, I could listen for API responses without needing to rely on the AIRIntrospector.

Listening for pasted input

A lot of the input forms I work on defaults the "Submit" button to disabled. Added to client side validation, this default usually prevents the user from submitting forms without required fields. I usually attach onkeyup events to each of the form fields to see whether I should enable the "Submit" button for a particular form.

However, onkeyup doesn't catch everything. If the user pastes input into a field using their mouse, onkeyup doesn't catch that event. So, the user is left with an input field which appears filled, but the "Submit" button remains disabled.

The solution is pretty simple. You can attach something like this to your input fields:



<input type="text" dojoAttachEvent="onkeyup: checkSubmitButton, oninput: checkSubmitButton, onpaste: checkSubmitButton">



More on oninput and onpaste

Friday, March 6, 2009

dojo builds and regexp

Just saw a tweet fly by which made me think of a possible blog entry.

I'm more inclined to use Firebug and console.* statements to debug my client side dojo code. I find it easier than rebuilding my code base every time I want to make a change. The one downside to this is my client side code has console statements scattered throughout the code, thus making my the download heavier.

So, as part of my build process, I use the following code to strip out all console.* statements and replace them with a semi-colon.


my @generated_files = (
"htdocs/my-build/$revision/dojo/dojo/main.js",
"htdocs/my-build/$revision/dojo/dojo/registration.js",
"htdocs/my-build/$revision/dojo/dojo/poller.js",
"htdocs/my-build/$revision/dojo/dojo/extras.js"
);
foreach my $generatedFileName (@generated_files) {
my $in_file = $generatedFileName;
my $out_file = $generatedFileName . ".stripped.js";
open DATA, "$in_file" or die "can't open $in_file $!";
my @file_lines = ;
close (DATA);
open DATAOUT, ">$out_file" or die "can't open $out_file $!";
foreach my $line (@file_lines) {
$line =~ s/(?<!:)console\.(?:warn|log|error|con|info)(\((?:(?>[^()]+)|(?{1}))*\));/;/gi;
print DATAOUT "$line";
}
close (DATAOUT);
}


The good people over at http://regexadvice.com/ helped me out with that expression and saved me ~5KB after GZIP compression - not too shabby.

This regular expression does not filter every console.* statement out of your code. For example if the regexp parser encounters:


test ? console.warn("Success") : console.warn("Failure");


it will not strip it down to:


test ? ; : ; ;


As that isn't valid JavaScript.

I'm pretty sure this problem could be fixed for the latest and greatest versions of dojo with a new parameter which you can pass automatically to the build script, but I'm still on 1.0.2 so I'll continue with this until I upgrade to 1.3.

Monday, March 2, 2009

Catching dijit's Dialog close event

I tend to use dijit Dialogs whenever I need the user to make a decision before proceeding with any other actions. They're especially useful for server error conditions and asking the user how they'd like to react to something like an API failure.

However, popping up dialogs can be annoying to users. I know I'm guilty of scanning dialog text and just hitting the 'x' button and ignoring the message. There are two ways you can prevent the user from doing this with dojo.

1. The most obvious solution: Hide the 'x' button. This is easily done with some simple CSS:


#myDialog .dijitDialogTitleBar .dijitDialogCloseIcon {
display:none;
}


The one drawback to this is that the user can hit the escape button and that'll close the dialog.

2. Attach the following two callbacks to your dialog:


this._userClosedDialogHandle = dojo.connect(dijit.byId("myDialog"), "hide", this, "_handleCloseDialog");
this._userClosedEscDialogHandle = dojo.connect(dijit.byId("myDialog").containerNode, 'onkeypress', function (evt) {
key = evt.keyCode;
if(key == dojo.keys.ESCAPE) {
this._handleCloseDialog();
}
});


The one caveat with this method is that you need to disconnect both handles in the _handleCloseDialog method. Otherwise, you'll find yourself in an infinite loop pretty quickly.

Tuesday, February 17, 2009

Handy Firefox plugin

This was pointed out to me today - Window Resizer allows you view your own browser in different resolutions. Not everyone has a massive 30" display!! I've definitely made the mistake in the past to develop for just my own resolution. Would be really cool if automated testing frameworks like D'OH or Selinium could be integrated with this plugin so you could run your test suits against a list of resolutions you need to support.

Google Analytics has a sub tab which lets you know the screen resolutions of your visitors. Its under Visitors -> Browser Capabilities -> Screen Resolutions.

Sunday, February 8, 2009

dojo and the enter key

Attaching a listener for the enter key on a form can help a lot from a usability perspective. If your users are more familiar with tabbing through a forms using only keys, asking them to use the mouse just to submit the form can be a little irritating. dojo comes to the rescue with a simple dojoattachevent. All you need to do is add the a dojoAttachEvent to your template file:


dojoattachevent="onkeyup: checkForEnter"


Then, in your widget logic, all you need to do is:


checkForEnter: function(keyEvent) {
if(keyEvent.keyCode == 13) {
if(!dojo.isFF) {
if(this._validateForm()) {
this._createAccount();
}
}
}
}


Oddly enough, Firefox is able to recognize the enter key and submits the form all by itself.

dojo campus has quite a few keys defined so you can capture a lot of what your users are trying to achieve with your web application.

Monday, January 19, 2009

dojo internationalization: easy as 1 .. 2 .. 7

The internationalization APIs in dojo are pretty similar to the internationalization support in Java - at least I found more similarities between dojo i18n and Java than I did between dojo i18n and PHP's gettext. However, getting up and running with dojo i18n support can be a little tricky. Here's a few simple steps I followed to get started with dojo internationalization.

1. Include the list of locales you want to support in your build script. I use Perl to build my dojo application, so it ended up looking a like this:


system("sh build.sh profileFile=$trunk/build/dojo/profile.js releaseDir=../../../build/$revision/ action=release version=$revision localeList=en-us,ja-jp");


2. Set the locale in the djConfig at the top of your page:


var djConfig = {
parseOnLoad: true,
usePlainJson: true,
locale: determineUserLocale()};


The determineUserLocale method here simply returns a locale in the format "en-us", "ja-jp" based on the user's preferences.

3. Wherever your dojo.require's usually are, add the following


dojo.requireLocalization("my.class", "i18n");


This is responsible for downloading a resource bundle called i18n.js which is located under the my.class module.

4. Create an I18n base class (any widget I use which needs internationalization support needs to extend from this widget):


dojo.provide("my.class.I18n");

dojo.require("dijit._Widget");
dojo.requireLocalization("my.class", "i18n");

dojo.declare(
"my.class.I18n",
[dijit._Widget],
{
statics: { i18n: {}},

preamble: function() {
this.statics.i18n = dojo.i18n.getLocalization("my.class", "i18n");
},

translate: function(key, params) {
params = dojo.isArray(params) ? params : [params];
return !params ? this.statics.i18n[key] : dojo.string.substitute(this.statics.i18n[key], params);
}
}


5. Included my.class.I18n in any widget which required internationalization support:


dojo.provide("my.class.TopNav");

dojo.require("my.class.I18n");

dojo.declare(
"my.class.TopNav",
[my.class.I18n, dijit._Templated],
{
templatePath: dojo.moduleUrl("my.class", "templates/TopNav.html"),

postCreate: function() {
...
},

onClick: function() {
...
},

_postApiCall: function(res) {
this.successNode.innerHTML = this.translate("success", [res]);
}


6. Altered my TopNav.html file to be automatically populated by internationalized strings:


${statics.i18n.homeLink}


7. Included internationalized strings in a file called i18n.js under the my.class directory:


homeLink: "Home",
success: "Success ${0} !",


Allowing different languages in the same application should be easy from here on in. All you need to do is set up your directory structure as follows:


my
class
nls
en-us
i18n.js
ja-jp
i18n.js


One option you have for switching between languages is to refresh the full page with the new locale passed in as a query string variable:


loadNewLanguage: function(newLocale) {
var oldSearch = "";
if(window.location.search.length > 0) {
if(window.location.search.indexOf("?Locale=") != -1) {
var localeToRemove = window.location.search.substring(window.location.search.indexOf("?Locale="), (window.location.search.indexOf("?Locale=") + 14));
oldSearch = "&" + window.location.search.replace(localeToRemove, "");
} else {
oldSearch = "&" + window.location.search.substring(1, window.location.search.length);
}
}
window.location = ((window.location.href.substring(0, window.location.href.indexOf(window.location.search)) + "?Locale=" + newLocale + oldSearch + window.location.hash));
}


The determineUserLocale() function mentioned at the top of this article will need to be able to prioritize and Locale query string parameters.

While these steps should get you up and running with dojo's internationalization APIs, there's plenty of i18n and l10n resources available directly on the dojo website. I found these links useful: