Update (10 May 2012): I’ve added a new post that has all the various bug fixes to date as well as new code showing how to get the code working as a user script in Chrome as well as Greasemonkey.
Here’s an update to my Gmail label column resizing code that has a bit more logic to detect other columns that may be present.
The previous versions worked well with Gmail’s out-of-the-box settings where only two columns were present, but if any plugins or Gmail labs had been enabled that added extra columns (e.g. Gmail’s “right-side chat” lab, etc.), then things started to go pear-shaped.
This update adds code to detect any space that doesn’t belong to either the label or main columns, and ensures that at least that much space is spare when resizing the columns or the window. In theory, this should work with any Gmail labs or other plugins that simply add a right-hand column to the screen.
I’ve also added code to specifically work with the Gmail “right-side chat” lab, as it modified the page structure by inserting tables around the main elements. Unfortunately, resizing table elements is a different ball-game altogether, and so I’ve had to traverse into each of the table cells and attempt to find the element that controls their width. While this works (at the moment), it does make an assumption that there is only one such element per cell / column (which is the case with the “right-side chat” lab). If a lab / plugin does anything fancy, this update will probably not work as expected.
So, I don’t expect this to work with all plugins or labs, as they’ll each have their own way of modifying the page structure, but I have tested it with the “right-side chat” lab and it seems to work very nicely.
If you use any other Gmail labs or plugins that add extra columns, please leave a comment below to let me know whether it works OK for you… there’s no way I’d be able to test every add-on available
There’s one extra update to this version of the code: the “mousedown” event handler calls “preventDefault” on the event object. I found that Firefox would occasionally start a native drag, where the grab bar would move but the mousemove event would not fire. This fixes that issue.
The code
As usual, I’ll start with the no-whitespace bookmarklet version:
javascript:gmonkey.load(2,function(o){var i=o.getNavPaneElement(),l,r,c=o.getActiveViewElement(),d=c.ownerDocument,b=d.body,t=0,x,w,n,g,Q='querySelector',W='width',U='parentNode',P='px',S='style',k=function(){r[W]=b[o]-l[o]-c+P};while(c.compareDocumentPosition(i)&2)c=c[U];while(i[U]!=b)i=i[U];n=c[U='childNodes'],w=[n[0],n[1]];if(c.cells)for(o in w){r=w[o][U];for(l in r)if(parseInt(t+r[l][S][W],10)){w[o]=r[l];break}}l=w[0],r=w[1][S],c=b[o='offsetWidth']-l[o]-w[1][o];g=i.appendChild(d.createElement('div'));n=l[Q]('[role^=n]');r[(U='cssText')+2]=n[Q]('[role]')[S][U];while(t+=n.offsetTop,n=n.offsetParent);g[S][U]=W+':4px;position:absolute;z-index:9;left:'+(l[o]-5)+'px;top:'+t+'px;bottom:0;cursor:ew-resize;cursor:col-resize;background:url(data:image/gif;base64,R0lGODlhAwAEAIAAAL+/v////yH5BAAAAAAALAAAAAADAAQAAAIERGKnVwA7)';top[n='addEventListener']('resize',k,0);g[n]('mousedown',function(e){e.which==1&&(x=l[o]-e.pageX,r[U+3]=r[U],r[U]+=r[U+2],e.preventDefault())},0);d[n]('mousemove',function(e){x&&(w=e.pageX+x,l[S][W]=w+P,g[S].left=w-5+P,k())},0);d[n]('mouseup',function(e){e.which==1&&(x=0,r[U]=r[U+3],k())},0)})
Even at 1123 bytes, all the modern browsers I tested seemed to handle this as a bookmarklet without any problems at all.
Here’s the same code, with a small amount of whitespace:
gmonkey.load(2, function(o) { var i=o.getNavPaneElement(), l, r, c=o.getActiveViewElement(), d=c.ownerDocument, b=d.body, t=0, x, w, n, g, Q='querySelector', W='width', U='parentNode', P='px', S='style', k=function() { r[W]=b[o]-l[o]-c+P; }; while(c.compareDocumentPosition(i)&2) c=c[U]; while(i[U]!=b) i=i[U]; n=c[U='childNodes'], w=[n[0], n[1]]; if (c.cells) for(o in w) { r=w[o][U]; for(l in r) if (parseInt(t+r[l][S][W], 10)) { w[o]=r[l]; break; }} l=w[0], r=w[1][S], c=b[o='offsetWidth']-l[o]-w[1][o]; g=i.appendChild(d.createElement('div')); n=l[Q]('[role^=n]'); r[(U='cssText')+2]=n[Q]('[role]')[S][U]; while(t+=n.offsetTop, n=n.offsetParent); g[S][U] = W + ':4px;position:absolute;z-index:9;left:' + (l[o]-5) + 'px;top:' + t + 'px;bottom:0;cursor:ew-resize;cursor:col-resize;background:url(data:image/gif;base64,R0lGODlhAwAEAIAAAL+/v////yH5BAAAAAAALAAAAAADAAQAAAIERGKnVwA7)'; top[n='addEventListener']('resize', k, 0); g[n]('mousedown', function(e) { e.which==1 && (x=l[o]-e.pageX, r[U+3]=r[U], r[U]+=r[U+2], e.preventDefault()); }, 0); d[n]('mousemove', function(e) { x && (w=e.pageX+x, l[S][W]=w+P, g[S].left=w-5+P, k()); }, 0); d[n]('mouseup', function(e) { e.which==1 && (x=0, r[U]=r[U+3], k()); }, 0); });
And the full monty:
// Use the Gmail Greasemonkey API to reliably find the 2 main elements (left- and right-hand columns). You do not need Greasemonkey installed to use the API. // See http://code.google.com/p/gmail-greasemonkey/wiki/GmailGreasemonkey10API for more details (note: documentation is out of date) // // Load the API. It will run our callback function when loaded, passing a GmailAPI object gmonkey.load(2, function(o) { /* Set up variables. To keep the code size down, some are re-used. There should be no global namespace pollution i initially points to the parent element of the left-hand nav, ultimately points to the parent element of the grab bar l initially used as a loop counter, ultimately points to the element that is resized to change the left-hand column width r initially used as a temporary loop variable, ultimately points to the style object of the element that is resized in the right-hand column c initially points to the parent element of the active view, then parent element of all columns, then used to hold the width of columns 3-n (if present) d this points to the document in the frame we use b this points to the body t this is used to calculate the top coordinate of the grab bar x this holds the initial x delta between mouse click and left-hand column width w initially used to point to the left-hand and main column elements, then during dragging, this holds the new width of the left-hand column n initially holds pointers to all the columns elements, then the left-hand nav, then holds the string 'addEventListener' g this points to the grab bar element Q this holds the string 'querySelector' W this holds the string 'width' U used for many things, most notably holding the strings 'parentNode', 'childNodes', and 'cssText' P this holds the string 'px' S this holds the string 'style' k this is the function that sets the width of the right-hand column */ var i=o.getNavPaneElement(), l, r, c=o.getActiveViewElement(), d=c.ownerDocument, b=d.body, t=0, x, w, n, g, Q='querySelector', W='width', U='parentNode', P='px', S='style', k=function() { r[W]=b[o]-l[o]-c+P; }; // Update c and i to point to their intended elements while(c.compareDocumentPosition(i)&2) c=c[U]; while(i[U]!=b) i=i[U]; // Update n to hold pointers to all the column elements (even if more than 2 are present), and w to point to just the first 2 n=c[U='childNodes'], w=[n[0], n[1]]; // Detect if a table has been used for layout... This happens with some of the Gmail labs (e.g. Right-side chat) // If present, find the element inside each table cell that is controlling its width by looking for an element // that has an explicit width style set that is > 0. This isn't perfect, but it's better than nothing :-) if (c.cells) for(o in w) { // Loop over the first two table cells r=w[o][U]; // Find all of the cell's child elements for(l in r) // And loop over them if (parseInt(t+r[l][S][W], 10)) { // Does the current child element have a non-0 style width explicitly set? w[o]=r[l]; // If so, update the current item in w to point to it break; // Assume there's only 1 width-controlling element (not perfect, I know) } } // Update l and r to point to their intended elements, and determine how much space is taken up // by any other columns (if present) by subtracting the label & main columns widths from the body width l=w[0], r=w[1][S], c=b[o='offsetWidth']-l[o]-w[1][o]; // Create the grab bar, and insert into DOM g=i.appendChild(d.createElement('div')); // Copy the inline style from the "Compose" button. At present, the only inline style is a browser-specific user-select style used to prevent text selection. // This is applied to the right-hand column while dragging is underway to stop the text being selected. It is removed afterwards so that text can be copied. // As I wanted to keep the code size as small as possible, it was easier to let Google do the browser sniffing and copy their style rather than deal with the vendor prefix approach. // Store this in a property called 'cssText2' of the right-hand column's style object n=l[Q]('[role^=n]'); r[(U='cssText')+2]=n[Q]('[role]')[S][U]; // Calculate the top position for the grab bar while(t+=n.offsetTop, n=n.offsetParent); // Style the grab bar g[S][U] = W + ':4px;position:absolute;z-index:9;left:' + (l[o]-5) + 'px;top:' + t + 'px;bottom:0;cursor:ew-resize;cursor:col-resize;background:url(data:image/gif;base64,R0lGODlhAwAEAIAAAL+/v////yH5BAAAAAAALAAAAAADAAQAAAIERGKnVwA7)'; // Add a resize event to the window to update the size of the right-hand column as the window is sized // While Gmail does this anyway, it doesn't take into account any size change to the left-hand column top[n='addEventListener']('resize', k, 0); // Add event listeners. mousedown is added to the grab bar, mousemove and mouseup to the document g[n]('mousedown', function(e) { // Only trigger with LMB on grab bar. Stores initial x coord and width of left-hand column // Save a copy of the existing right-hand column style so we can restore it later, then disable text selection // Call e.preventDefault() to stop a native drag operation from starting (this happens a lot in Fx) e.which==1 && (x=l[o]-e.pageX, r[U+3]=r[U], r[U]+=r[U+2], e.preventDefault()); }, 0); d[n]('mousemove', function(e) { // If we have the mouse button pressed, work out the x delta then update the column widths and the grab bar position x && (w=e.pageX+x, l[S][W]=w+P, g[S].left=w-5+P, k()); }, 0); d[n]('mouseup', function(e) { // If LMB is released, stop the mousemove code from running and restore the right-hand column style // k() needs to be called again, as restoring the style alters the width e.which==1 && (x=0, r[U]=r[U+3], k()); }, 0); });
How to run the code
See the instructions in my previous post. Greasemonkey users should note that I’ve seen odd behaviour when waiting for the window’s onload event to fire: occasionally, 2 grab bars would show up. For this reason, I recommend not including that line of code, and so the only change (other than the script header) is to prefix “gmonkey” with “unsafeWindow.”.
Known issues
As per last version, minus the “right-side chat” item