Label To Top

By Ryan Hagan Last update Jul 11, 2009 — Installed 1,126 times. Daily Installs: 1, 0, 1, 0, 1, 1, 2, 3, 0, 4, 0, 1, 4, 1, 2, 3, 1, 0, 0, 1, 0, 1, 2, 2, 2, 2, 4, 3, 1, 1, 2, 1

There are 1 previous version of this script.

// Copyright 2007,2009 Ryan Hagan <ryan@ryanhagan.net>.
// All rights Reserved.
//
//
// RELEASE NOTES
// =============
// v2.0 (07/11/2009)
// * Updated to work with new GMail labels
//
// 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
// ============
// * 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/*
// @include        http://mail.google.com/a/*
// @include        https://mail.google.com/a/*
// ==/UserScript==

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


// Program configuration
var UPDATE_LABEL_INTERVAL = 10000;
var DOC_ROOT = top.frames[3].document;

// XPath searches used in code
var USER_LABELS_ROOT_ELEMENT_SEARCH = "//div/table[@class = 'cf nX']";
var UNREAD_LABELS_SEARCH = ".//tr//div[@class='n2 n1']";
var READ_LABELS_SEARCH = ".//tr//div[@class='n2']";

// 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]+?\)$/; 

// Program defaults
var MOVE_LABELS_TO_TOP_DEFAULT = "true";
var REWRITE_LABELS_WITH_COUNT_IN_FRONT_DEFAULT = "true";
var HIDE_READ_LABLES_DEFAULT = "true";


var L2T =
{
	init : function (gmail) 
	{
		// clear any interval callbacks to this function
		window.clearInterval(intervalId);
		
		// create the container for the unread labels
		L2T.UI.initUnreadLabelStorage();
		
		// create the control to hide the read label container
		if ( L2T.Config.getHideReadLabels() == 'true' ) { 
			L2T.UI.initHideUnhideReadLabelsControl();
		}
			
		var run = function() 
		{						
			// move labels to the top
			if ( L2T.Config.getMoveLabel2Top() == 'true' ) {
				// move unread items to the unread container
				unreadLabels = _XPathSearch(UNREAD_LABELS_SEARCH, L2T.UI.getReadLabelsContainer());
				L2T.UI.moveLabelsToContainer(unreadLabels, L2T.UI.getUnreadLabelsContainer());

				// move read items to the normal container
				readLabels = _XPathSearch(READ_LABELS_SEARCH, L2T.UI.getUnreadLabelsContainer());
				L2T.UI.moveLabelsToContainer(readLabels, L2T.UI.getReadLabelsContainer());			
			}
			
			// put the unread message count in front of the label
			if ( L2T.Config.getUnreadCountInFront() == 'true' )
			{
				unreadLabels = _XPathSearch(UNREAD_LABELS_SEARCH, L2T.UI.getUnreadLabelsContainer());
				L2T.UI.rewriteLabelsWithCountAtFront(unreadLabels);
			}
		}

		run();
		
		// run this script periodically and when the view changes in gmail
		gmail.registerViewChangeCallback(run);
		intervalId = window.setInterval(run, UPDATE_LABEL_INTERVAL);
	}
};

L2T.UI = 
{
	initUnreadLabelStorage : function()
	{
		// create the unread labels container
		this.createUnreadLabelStorage();
	},
	
	createUnreadLabelStorage : function() 
	{
		// get a reference to the main user labels container
		var userLabelsContainer = this.getUserLabelsContainer();
		// create the table element that will store the unread labels
		var newTableElement = document.createElement('table');
		newTableElement.setAttribute('class','cf nX');
		newTableElement.setAttribute('cellpadding','0');
		var newTBodyElement = document.createElement('tbody');
		newTBodyElement.id = 'unreadLabels';
		newTableElement.appendChild(newTBodyElement);
		// insert the unread labels table into the top of the main labels container
		userLabelsContainer.insertBefore(newTableElement, userLabelsContainer.firstChild);
	},
	
	getUserLabelsContainer : function()
	{
		// try to get the element using the ID first
		var userLabelsContainer = DOC_ROOT.getElementById('userLabels');
		if ( userLabelsContainer ) return userLabelsContainer;
	 	// if that's not successful, get reference to the user labels in the DOM
		var userLabelsContainer = _XPathSearch(USER_LABELS_ROOT_ELEMENT_SEARCH, DOC_ROOT).snapshotItem(1);
		while ( userLabelsContainer.nodeName.toLowerCase() != 'div')
			userLabelsContainer = userLabelsContainer.parentNode;
		userLabelsContainer.id = 'userLabels';
		return userLabelsContainer;
	},
	
	initHideUnhideReadLabelsControl : function()
	{
		// create the hide/unhide control for the read labels
		this.createHideUnhideReadLabelsControl();
		// hide the read labels
		L2T.UI.Handler.hideReadLabels();
		// add event handler for control
		this.getHideUnhideReadLabelsControl().addEventListener('click', L2T.UI.Handler.showHideReadLabelsContainer, false);
	},
	
	createHideUnhideReadLabelsControl : function() 
	{
		// create control
		var newTableElement = document.createElement('table');
		newTableElement.setAttribute('class','cf nX');
		newTableElement.setAttribute('cellpadding','0');
		newTableElement.innerHTML = "<tbody><tr class='Alfa2e'><td class='nL'></td><td align='right'><a id='hideReadLabelsControl' href='javascript:null;'>Show Labels</a></td></tr></tbody>";
		
		// insert control directly above the read labels container
		var userLabelsRootElem = this.getUserLabelsContainer();
		userLabelsRootElem.insertBefore(newTableElement, userLabelsRootElem.childNodes[1]);
	},
	
	getReadLabelsContainer : function()
	{
		// try to get the element using the ID first
		readLabelsContainer = DOC_ROOT.getElementById('readLabels');
		if ( readLabelsContainer ) return readLabelsContainer;
		// if that's not successful, get the last child of the main labels container
		parentElem = this.getUserLabelsContainer();
		readLabelsContainer = parentElem.childNodes[ parentElem.childNodes.length - 1 ];
		readLabelsContainer.firstChild.id = 'readLabels';
		return readLabelsContainer.firstChild;
	},
	
	getHideUnhideReadLabelsControl : function()
	{
		// try to get the element using the ID first
		hideReadLabelsControl = DOC_ROOT.getElementById('hideReadLabelsControl');
		if ( hideReadLabelsControl ) return hideReadLabelsControl; else return false;
	},
	
	getUnreadLabelsContainer : function()
	{
		// try to get the element using the ID first
		unreadLabelsContainer = DOC_ROOT.getElementById('unreadLabels');
		if ( unreadLabelsContainer ) return unreadLabelsContainer; else return false;
	},
	
	moveLabelsToContainer : function(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 ( this.getLabelText(currElem).toLowerCase() < this.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 );
			}
		}
	},
	
	getLabelText : function(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.childNodes[1].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;
	},
	
	rewriteLabelsWithCountAtFront : function(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 ( this.getLabelText(currElem).match(UNREAD_COUNT_IN_BACK_REGEXP) )
				this.setLabelText(currElem, this.rewriteLabelWithCountAtFront(this.getLabelText(currElem)));
		}		
	},
	
	rewriteLabelWithCountAtFront : function(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;
	},	
	
	setLabelText : function(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.childNodes[1].firstChild.firstChild.firstChild.innerHTML = text;
	}
};


L2T.UI.Handler = 
{
	showHideReadLabelsContainer : function(e)
	{
		var currElem = e.target;
		if (L2T.UI.Handler.getReadLabelsVisible()) { L2T.UI.Handler.hideReadLabels(); } else { L2T.UI.Handler.showReadLabels(); }
		currElem.innerHTML = (L2T.UI.Handler.getReadLabelsVisible()) ? 'Hide Labels' : 'Show Labels';
	},
	
	hideReadLabels : function()
	{
		L2T.UI.getReadLabelsContainer().style.display = 'none';
	},
	
	showReadLabels : function()
	{
		L2T.UI.getReadLabelsContainer().style.display = '';
	},
	
	getReadLabelsVisible : function()
	{
		if ( L2T.UI.getReadLabelsContainer().style.display == 'none' )
			return false;
		else 
			return true;
	}	
};

L2T.Config = 
{
	getMoveLabel2Top : function() {
		moveLabel2Top = this.getValue("moveLabel2TopSetting", MOVE_LABELS_TO_TOP_DEFAULT);
		return moveLabel2Top;		
	},
	
	setMoveLabel2Top : function(value) {
		this.setValue("moveLabel2TopSetting", value);
	},
	
	getUnreadCountInFront : function() {
		unreadCountInFront = this.getValue("unreadCountInFrontSetting", REWRITE_LABELS_WITH_COUNT_IN_FRONT_DEFAULT);
		return unreadCountInFront;		
	},
	
	setUnreadCountInFront : function(value) {
		this.setValue("unreadCountInFrontSetting", value);
	},
	
	getHideReadLabels : function() {
		hideUnreadLabels = this.getValue("hideReadLabelsSetting", HIDE_READ_LABLES_DEFAULT);
		return hideUnreadLabels;		
	},
	
	setHideReadLabels : function(value) {
		this.setValue("hideReadLabelsSetting", value);
	},
	
	getValue : function(setting, defaultValue) {
		var value = (GM_getValue(setting) != undefined) ? GM_getValue(setting) : defaultValue;
		return value;
	},
	
	setValue : function(setting, newValue) {
		GM_setValue(setting, newValue);
	}
};


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


// create intervalId variable
var intervalId = null;

window.addEventListener('load', function() {
	unsafeWindow.gmonkey.load(GMONKEY_VER, function( gmail ) {
		// kick off the program
		L2T.init(gmail);
	});
}, true);