Label To Top

By Ryan Hagan Last update Dec 14, 2007 — Installed 793 times.
// Copyright 2007 Ryan Hagan <ryan@ryanhagan.net>.
// All rights Reserved.
//
//
// RELEASE NOTES
// =============
// v1.1 (12/14/2007)
// * Added ability to control behavior through settings in script.
// * Added code to make unread count on labels appear in front of label instead of behind.
// 
// v1.0 (11/25/2007)
// * Initial release of script.
//
//
// KNOWN ISSUES
// ============
// * Only works with new Gmail interface.
// * Will not work in conjunction with Folders4Gmail.
// * Rewriting unread count on labels breaks the 'Go To Label' macro in Gmail Macros script which
//		is included with the Better Gmail Firefox Addon.
// 
//
//
// ==UserScript==
// @name           Label To Top
// @namespace      ryanhagan.net
// @include        http://mail.google.com/mail/*
// @include        https://mail.google.com/mail/*
// ==/UserScript==



// Program configuration
var UPDATE_LABEL_INTERVAL = 2000;
var REWRITE_LABEL_BOX_WITH_UNREAD_AT_TOP = true;
var REWRITE_LABELS_WITH_UNREAD_COUNT_IN_FRONT = true;




// Which version of gMonkey does this script use?
var GMONKEY_VER = "1.0";

// XPath searches used in code
var USER_LABELS_ROOT_SEARCH = ".//div[@class='XoqCub XPj4ef']//div[@class='XoqCub C9Pn8c']";
var USER_LABELS_TABLE_SEARCH = ".//table";
var USER_LABELS_TABLE_THEAD_SEARCH = ".//table/thead";
var UNREAD_LABELS_SEARCH = ".//tr//div[@class='yyT6sf PQmvpb']";
var READ_LABELS_SEARCH = ".//tr//div[@class='yyT6sf']";

// Regular Expressions used in code
var BREAK_LABEL_TEXT_AND_UNREAD_COUNT_APART_REGEXP = /^([\[\]\-\_\\\/\.\d\w\s]+?) \(([\d]+)\)$/;
var UNREAD_COUNT_IN_FRONT_REGEXP = /^\([\d]+?\) /;
var UNREAD_COUNT_IN_BACK_REGEXP = / \([\d]+?\)$/;

// Other constants
var LABEL_TEXT_TD_CELL_ATTRIBUTE = '';
var LABEL_COLOR_TD_CELL_ATTRIBUTE = 'BFvfre';


window.addEventListener('load', function() {

	unsafeWindow.gmonkey.load(GMONKEY_VER, function( gmail ) {
		// create intervalId variable
		var intervalId = null;

		// get reference to the user labels in the DOM
		navPaneElem = gmail.getNavPaneElement();
		var userLabelsElem = _XPathSearch(USER_LABELS_ROOT_SEARCH, navPaneElem).snapshotItem(0);

		// create an empty thead element on the user labels table where
		// we'll store the labels with unread content.
		labelTableElem = _XPathSearch(USER_LABELS_TABLE_SEARCH, userLabelsElem).snapshotItem(0);
		var newTHeadElement = document.createElement('thead');
		labelTableElem.insertBefore(newTHeadElement, labelTableElem.firstChild);

		// convenience references to keep from having to scan all labels each time
		// function is called.
		var unreadTableElem = labelTableElem.childNodes[0];
		var readTableElem = labelTableElem.childNodes[1];
		
		function _LabelsToTop()
		{
			// clear any interval callbacks to this function
			window.clearInterval(intervalId);

			if ( REWRITE_LABEL_BOX_WITH_UNREAD_AT_TOP )
			{
				// move unread items to the unread container
				unreadLabels = _XPathSearch(UNREAD_LABELS_SEARCH, readTableElem);
				_MoveLabelsToContainer(unreadLabels, unreadTableElem);

				// move read items to the normal container
				readLabels = _XPathSearch(READ_LABELS_SEARCH, unreadTableElem);
				_MoveLabelsToContainer(readLabels, readTableElem);
			}
			
			if ( REWRITE_LABELS_WITH_UNREAD_COUNT_IN_FRONT )
			{
				unreadLabels = _XPathSearch(UNREAD_LABELS_SEARCH, labelTableElem);
				_RewriteLabelsWithCountAtFront(unreadLabels);
			}

			// call this function again on a regular interval
			intervalId = window.setInterval(_LabelsToTop, UPDATE_LABEL_INTERVAL);
		}

		// run this script periodically
		gmail.registerViewChangeCallback(_LabelsToTop);
		_LabelsToTop();

	});

}, true);



function _XPathSearch( xpathExpression, contextNode ) {
	if (!xpathExpression) xpathExpression = document;
	return document.evaluate(xpathExpression, contextNode, null, XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null);
}


function _MoveLabelsToContainer(labels, toContainerElem)
{
	// loop over results, pull the <tr> out of the DOM
	labelsCount = labels.snapshotLength;
	for ( labelIndex = 0; labelIndex < labelsCount; labelIndex++ )
	{
		// first, back up the DOM tree until we get to a <tr>
		currElem = labels.snapshotItem(labelIndex);
		while ( currElem.tagName.toLowerCase() != 'tr')
		{
			currElem = currElem.parentNode;
		}
		
		// walk the toContainer and insert element at first space available
		toContainerCount = toContainerElem.childNodes.length;
		if ( toContainerCount == 0 )
		{
			toContainerElem.appendChild( currElem );
		}
		else
		{
			// Insert the label in alphabetical order.  This assumes that the list is already sorted in 
			// alphabetical order.  If it is not, then this won't work at all.  Unless another script
			// is messing with the order of your labels, this shouldn't be a problem.
			for ( containerLabelIndex = 0; containerLabelIndex < toContainerCount; containerLabelIndex++ )
			{
				if ( _GetLabelText(currElem).toLowerCase() < _GetLabelText(toContainerElem.childNodes[containerLabelIndex]).toLowerCase() )
				{
					toContainerElem.insertBefore(currElem, toContainerElem.childNodes[containerLabelIndex]);
					break;
				}
			}
			
			// if we got all the way through the list and haven't put the label anywhere, throw it down
			// at the end of the label list.
			if ( containerLabelIndex == toContainerCount && toContainerElem.lastChild != currElem )
				toContainerElem.appendChild( currElem );
		}
	}
}

function _RewriteLabelsWithCountAtFront(labels)
{
	// loop over results, pull the <tr> out of the DOM
	labelsCount = labels.snapshotLength;
	for ( labelIndex = 0; labelIndex < labelsCount; labelIndex++ )
	{
		// first, back up the DOM tree until we get to a <tr>
		currElem = labels.snapshotItem(labelIndex);
		while ( currElem.tagName.toLowerCase() != 'tr')
		{
			currElem = currElem.parentNode;
		}
		
		// rewrite the label
		if ( _GetLabelText(currElem).match(UNREAD_COUNT_IN_BACK_REGEXP) )
			_SetLabelText(currElem, _RewriteLabelWithCountAtFront(_GetLabelText(currElem)));
	}		
}

function _GetLabelText(elem)
{
	// since we're mostly working with <tr> tags here, I needed a helper function to grab the actual 
	// label text from the container.  First, we should check to make sure we've really been passed
	// a <tr>, because if we haven't been, who knows what this would return?
	var labelText = '';
	if ( elem.tagName.toLowerCase() == 'tr' )
	{
		labelText = elem.firstChild.firstChild.firstChild.firstChild.innerHTML;
		if ( labelText.match(UNREAD_COUNT_IN_FRONT_REGEXP) )
		{
			labelText = labelText.replace(UNREAD_COUNT_IN_FRONT_REGEXP, '');
		}
	}
	else
	{
		labelText = '[invalid reference]';
	}
	
	return labelText;
}
function _SetLabelText(elem, text)
{
	// since we're mostly working with <tr> tags here, I needed a helper function to set the 
	// label text in the container.  First, we should check to make sure we've really been passed
	// a <tr>, because if we haven't been, who knows what this would do?
	if ( elem.tagName.toLowerCase() == 'tr' )
		elem.firstChild.firstChild.firstChild.firstChild.innerHTML = text;
}

function _RewriteLabelWithCountAtFront(labelText)
{
	// find the unread count on the label
	var newLabelText = '';
	var unreadConvMatch = labelText.match(BREAK_LABEL_TEXT_AND_UNREAD_COUNT_APART_REGEXP);

	// if we get something that looks like a good match, then prepend the label with the unread count
	if ( unreadConvMatch && unreadConvMatch.length == 3 && parseInt(unreadConvMatch[2]) > 0 )
		newLabelText = '(' + unreadConvMatch[2] + ') ' + unreadConvMatch[1];
	else
		newLabelText = labelText;
		
	return newLabelText;
}