Welcome to Code Couch

Resizing the left-hand labels column in Gmail – support for extra columns

Posted by at 6:43am on February 19, 2012.

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
	// 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
	// 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 :-)

Post to Twitter


There are 8 responses to this post.

  1. Hi Dan, Sorry, it just doesn't seem to like the xobni plugin. I've posted some screenshots on Photobucket - http://s618.photobucket.com/albums/tt268/heylonghair/Screenshots/ along with some comments in the descriptions. If you need any more info just let me know. Ray
  2. A quick update: If you change the code from reading "z-index:9" to "z-index:1", the bar will not obscure the popups that occur when hovering over a person name in the chat module. Thanks to Jason for pointing this out in my previous post.
  3. [...] an interesting 'feature' I found while updating my popular Gmail label column resizer script: A way to access certain parts of your mailbox without having to sign [...]
  4. Hi, thanks for this great script. I'm using Chrome, and the bookmarklet version works great, but I can't make the script version to work. I installed it by drag-droping the .user.js file on Chrome. I can find the script in the extensions list, but still, when I load GMail, The left column cannot be resized. What am I doing wrong ?
  5. I don't think you're doing anything wrong at all. I've just tried and get the same error. It looks like Chrome doesn't run GM scripts in the same way that GM does. Perhaps there's a proper GM extension for Chrome that will resolve this issue? I don't use GM at all, so have never really delved into it that much, sorry!
  6. Hi again Benco. I've found a way to get the GM version working in Chrome with minimal work required. I've a few more updates to make to the script anyway, so will add a new post later containing the details. Once I've posted the new code, I'll add another comment to this post pointing to the update.
  7. I've added a new post dated 10 May 2012 that shows how to get this working as a user script in Chrome as well as Greasemonkey. See the update at the top of this post for details.
  8. All: For anyone finding that this code no longer works since late March 2013, please read my latest post dated 31st March 2013. It offers an updated wrapper that works with the latest Gmail changes.

Leave a reply

You must either log in or enter your name and email address to post a comment.

Your email address will not be published.

  • You do not need to log in to comment, but you can if you wish.
  • Log in