Wednesday, May 28, 2008

Firefox bug: deceiving select onchange event

I have been experiencing a silly bug in Firefox for 2 years now. It happens on my Ubuntu (Gnome) and formerly on Debian (Gnome too). I tried it on a Windows machine but it didn't work.
As usual, I am lazy to try it on all available platforms, so please help me in your comments!

Bug description:


If a select element has no option selected (selectedIndex == -1), an outside-select click behavior deceives the browser to fire the onchange event for the select.

How to reproduce bug:



  1. Click to open a select element

  2. Move your mouse over any option (make sure the option is highlighted, but don't click on anything)

  3. Click outside the select twice

  4. As a result of the bug, the onchange event is fired with the option under mouse as the select value



To see an example, the bug is illustrated here

I tried it on Ubuntu (Gnome and KDE) and it happens too.
I tried also on Firefox for Windows (through wine) and it happens too!

Now what?
Read more...

SELECT threads FROM GMail ORDER BY sender, subject, thread_activity

GMail is a very good mail service, it has a lot of useful features, mainly searching. However, it misses a very important feature: sorting. Someone may argue, why sort while you can find any message using fast search? I say, sometimes you want to view your threads sorted alphabetically when you don't know the exact phrases to search with.

I wrote a Javascript bookmarklet (GMail Sorter) to add sort controls in your GMail Inbox view. Once you enable the bookmarklet, you can see the following above your threads:



Clicking on the links asc/desc sorts visible threads by sender, subject or thread activity. Actually I wrote this bookmarklet mainly for the thread activity feature. Sometimes I don't have time to check community messages, until I come back after several days to see a lot of unread messages there. I wished there could be a way to sort them by their activity: threads that more people have replied on seems more interesting, I need to read them first.

The bookmarklet is heavily based on another bookmarklet which adds sort controls to all tables in page. Actually what I did was customizing that bookmarklet to work on GMail Inbox and adding different sort criteria. The sorting code is taken almost as is.

If you just need to use the bookmarklet and don't care about some implementation details, you can skip to section "How to install" below.

How GMail Sorter is different



Let me refer to that bookmarklet as B and mine as M. In B, all tables are tampered. In M only the Inbox table is tampered. It was a challenge finding the Inbox. Guys at Google seem to obfuscate ids and class names for all DOM elements. For example, they are something like 1d3f, 3xua,...
I attacked this issue by finding the "widest table". The Inbox table happens to be the 2nd widest table, where the notification area that appears on top of the table is the 1st widest one. However, the Inbox table does have an id and the notification area does not. So I find it by getting the widest table having an id:


function getWidestTable(tables) {
var maxTable = undefined;
var maxWidth = -1;
for (var i = 0; i < tables.length; i++) {
var table = tables[i];
if (table.clientWidth >= maxWidth && table.id != '') {
maxWidth = table.clientWidth;
maxTable = table;
}
}
return maxTable;
}


Applying B on GMail will give you an alert: "This page does not contain any tables". Wow, all these tables and B can't find it! The problem is that B searches for table tags in current document. In M, I had to iterate through all iframes and search in tables in their documents. Shortly I discovered that the Inbox table exists in an iframed named "canvas_iframe", so the code became a little simpler:


function getAllTables() {
g_tables = toArray(document.getElementsByTagName('table'));
var iframe = document.getElementById('canvas_frame');
if (!iframe) return null;
var iframedoc = iframe.contentDocument.document || iframe.contentWindow.document;
iframetables = toArray(iframedoc.getElementsByTagName('table'));
if (iframetables.length)
g_tables = g_tables.concat(iframetables);
if (!g_tables.length)
return null;
return g_tables;
}


How to install:


In Firefox, Just create a new bookmark and paste the following code in the URL:


javascript:function toArray (c) {var a, k;a = new Array;for (k=0; k < c.length; ++k)a[k] = c[k];return a;}function insAtTop(par,child) {if (par.childNodes.length)par.insertBefore(child, par.childNodes[0]);else par.appendChild(child);}function countCols(tab) {var nCols, i;nCols = 0;for(i = 0; i nCols)nCols = tab.rows[i].cells.length;return nCols;}function makeHeaderLink(colNo, ord, regex, numeric) {var link;link = document.createElement('a');link.href = '#';link.onclick = function() {var __st = window == top ? window : top;__st.sortTable(colNo, ord, regex, numeric);return false;};link.appendChild(document.createTextNode((ord>0)? 'asc':'desc'));return link;}function makeSortControl(header, col, title, regex, numeric) {header.appendChild(document.createTextNode(title + ' ['));header.appendChild(makeHeaderLink(col, 1, regex, numeric));header.appendChild(document.createTextNode('/'));header.appendChild(makeHeaderLink(col, -1, regex, numeric));header.appendChild(document.createTextNode(']'));}function makeHeader(nCols) {var header, headerCell, i;header = document.createElement('span');/*put links in columns 2 and 4 only (sender, subject)*/makeSortControl(header, 2, 'Sender(s)');header.appendChild(document.createTextNode(' | '));makeSortControl(header, 4, 'Subject');header.appendChild(document.createTextNode(' | '));makeSortControl(header, 2, 'Thread Activity', /\(\d*\)$/, true);return header;}function getWidestTable(tables) {var maxTable = undefined;var maxWidth = -1;for (var i = 0; i= maxWidth && table.id != '') {maxWidth = table.clientWidth;maxTable = table;}}return maxTable;}function getAllTables() {g_tables = toArray(document.getElementsByTagName('table'));var iframe = document.getElementById('canvas_frame');if (!iframe)return null;var iframedoc = iframe.contentDocument.document || iframe.contentWindow.document;iframetables = toArray(iframedoc.getElementsByTagName('table'));if (iframetables.length)g_tables = g_tables.concat(iframetables);if (!g_tables.length)return null;return g_tables;}(function () {/* ---- main() ---- ENTRY POINT HERE ------------------------- */g_tables = getAllTables();if (!g_tables) {alert("It seems that this script is not compatible with your gmail version (no tables), giving up!");return;}inboxtable = getWidestTable(g_tables);if (inboxtable.id.length<4) {alert("It seems that this script is not compatible with your gmail version (no inbox), giving up!");return;}var control = makeHeader(countCols(inboxtable));var tableparent = inboxtable.parentNode.parentNode;while(tableparent.previousSibling.style.display == 'none')tableparent = tableparent.previousSibling;tableparent.previousSibling.appendChild(control);/*tableparent.parentNode.insertBefore(control, tableparent);*/}) ();function compareRows(a,b) {if (a.sortKey == b.sortKey)return 0;return (a.sortKey < b.sortKey) ? g_order : -g_order;}function compareRowsNumeric(a,b) {if (a.sortKey == b.sortKey)return 0;return ((a.sortKey + '').match(/\d+/) - (b.sortKey + '').match(/\d+/)) * g_order;}function sortTable(colNo, ord, regex, numeric) {var table, rows, nR, bs, i, j, temp;g_order = ord;g_colNo = colNo;g_tables = getAllTables();table = getWidestTable(g_tables);rows = new Array();nR = 0;bs = table.tBodies;for (i = 0; i < bs.length; ++i)for(j=0; j < bs[i].rows.length; ++j) {rows[nR] = bs[i].rows[j];temp = rows[nR].cells[g_colNo];if (!temp)rows[nR].sortKey = '';else if (!regex)rows[nR].sortKey = temp.textContent.toLowerCase();else {var val = temp.textContent.toLowerCase().match(regex);rows[nR].sortKey = val ? val : '';}++nR;}if (numeric)rows.sort(compareRowsNumeric);else rows.sort(compareRows);for (i = 0; i < rows.length; ++i)insAtTop(table.tBodies[0], rows[i]);}


Put it in the Bookmarks Toolbar folder so that it is always visible. Anytime you need to enable the bookmarklet, open your Inbox view (or any messages view) and click on the bookmark you have just created. You will see a sort control added on top of the table. Play and enjoy!

How to modify:


If you are a Javascript geek and need to play with the code, here is a clear source where you can play with. After playing you will have to remove all new lines and prepend "javascript:" to form a valid URL that will fit in a bookmark. Don't worry, here is a one-liner to do this task:

(echo "javascript:" ; cat sorttables.js) | tr -d "\n\t" > sorttables_bookmarklet.js

This will read in the file sorttables.js that contains your code and outputs a file sorttables_bookmarklet.js where you can grasp its contents and paste it in your bookmark URL.
Read more...

Thursday, May 8, 2008

Upgrading unupgradable Firefox extensions


To their bad fortune, many of you may have already upgraded to Firefox 3 beta. Ouch!
You may have also tried to upgrade your extensions and found most of them failed to do :(
Now you have either of 2 solutions:


  1. downgrade back to Firefox 2
  2. force upgrade of your extensions

"2" seems nice, huh?
OK, lets go straight, now follow me on these:


  1. Any extension is an .xpi file (pronounced as zippy), download the .xpi. If you click on a .xpi link in Firefox it will be installed instead, so you can get it with any other browser, or right-click and Save as... You can also type the following in a shell: wget XPI_URL
  2. Extract the .xpi with any archive manager (it is actually a ZIP file renamed)
  3. Open install.rdf with any text editor
  4. Locate the line: ... and replace this value to something like 3.0b5. This will make your extension pretend to work on up to FF3 beta5!
  5. Now zip back your files and drag on any Firefox open window and it will automatically install.
  6. Enjoy!

IMPORTANT:


  • The new archive you created in step 5 MUST have the extension .xpi
  • Files in the archive should be directly on the root not inside an internal directory
  • The extension will claim to work on your browser, but it may not function as you expect, simply because the extension maker did not test it on your version. So forcing it to work is solely on your responsibility with NO WARRANTY!


Read more...

Busting Javascript alerts

Sometimes it is annoying to see alerts every now and then in web pages. It is not practical to switch off alerts altogether (see this), however it may seem convenient to get control over alerts and do whatever actions: mute, show somewhere else, ...
It is simple like overriding the window.alert function:


window.alert = function(msg)
{


 // do whatever, for example:
document.title = "busted alert: " + msg
var div = document.getElementById('divid').innerHTML = msg

}

Of course this is applicable for window.confirm and window.prompt too.

This may appear trivial and useless. However, you can elevate its potential in 2 ways:

First: You can bust alerts of embedded iframes by getting a reference to its content window:


function loaded(iframe) {


 var iframe = document.getElementById('iframeid')
// or iframe = document.frames[0]
// or iframe = document.frames[frame_name]
// or just use the iframe function argument above
iframe.contentWindow.alert = function(msg) {...}

}

You will have to put previous code in the onload function of the iframe:


<iframe onload="loaded(this)></iframe>

I tried this on both Firefox2 and IE7 and it worked with no problems. Other browsers?

The other way to make this useful is to install Greasemonkey and create a new user script containing the overriding code.
You will have to access the unsafe window object to do so:


unsafeWindow.alert = function(msg){...}

This way you can do anything to alerts coming from any site that will run the user script.


Read more...