LJ: Taglister

By Afuna Last update Jul 20, 2007 — Installed 1,739 times.
// ==UserScript==
// @name			LJ: Taglister
// @namespace		afuna.livejournal.com
// @description		Fetch a taglist when you're updating your journal/editing entries
// @include			http://*.livejournal.com/update.bml*
// @include			http://*.livejournal.com/editjournal.bml*
// ==/UserScript==

/**
Usage notes:	
 - now works with all journals. Thanks, ciaran_h -- you're awesome :D
 - cmd-clicking /ctrl-clicking on the tags will open the tagspage in a new tab
 - when expanded, the list shows all tags. When collapsed, it shows the top $n tags based on usage frequency, where $n is a number you define. If there are many tags tied for $nth place, it shows all of them. The top $n tags are bolded for emphasis; if you don't like it, you can get around this by setting $n to 0 or a negative number
 - there is no auto-complete, but tags that match what you type will be marked by a gray border. Also, clicking on a tag in the list should update the textbox
 -the tags list will show up in both when you update and edit. It will also automatically detect the journal you're posting to
*/

function getArg(arg)
{
	// just in case there are other arguments
	journal = document.location.search.split("&");
	for(var i = 0; i < journal.length; ++i)
	{
		if((index = journal[i].indexOf(arg) )!= -1)
		{
			return journal[i].split("=")[1];
		}			
	}
}
function getJournal()
{
	// figure out what journal you're posting to now
	var usejournal = "";
	if(document.location.href.indexOf("update.bml") != -1)
	{
		if(document.location.search.indexOf("usejournal") != -1 )
		{
			usejournal = getArg("usejournal");
		}
		else
		{
			var journal = document.getElementById("usejournal");
			if(journal) 
			{
				usejournal = journal.value == "" ?
					document.getElementById("current_username").textContent : journal.value;
			}
		}
	}
	else	// editjournal.bml
	{
		usejournal = getArg("journal");
	}

	return usejournal;
}

function createTagItem(tag)
{
	var tagItem = document.createElement("a");
	tagItem.href="http://"+usejournal+".livejournal.com/tag/"+tag[0].replace(/ /g,'+');
	tagItem.innerHTML = tag[0];
	tagItem.freq = tag[1];
	tagItem.title = "visible: "+tagItem.freq;
	tagItem.setAttribute("match", false);
	tagItem.setAttribute("selected", false);
	tagItem.id = "taglister:"+tag[0];

	// below commented out by CH as it's handled now by autoHighlight.

//	// check whether the tags are already in the textbox
//	// still not perfect: nothing for when the user manually types in tags
//	// also, might have some false positives?
//	if(tagsTextbox.value.indexOf(tag[0] + ",") != -1 )
//	{
//		tagItem.className="selected";
//	}
	
	// do the same, but triggered by a userclick
	tagItem.addEventListener("click", function(event) {
		// don't stop propagation
		// so you can use cmd+click to open the tagspage in a new tab
		//event.stopPropagation();
		event.preventDefault();							
	
		var tagText = event.originalTarget.textContent;

		// if the tag is already in the textbox, then previously-run code will have
		// set its "selected" attribute.
		var alreadySelected = event.originalTarget.getAttribute("selected");
		alreadySelected = (alreadySelected && alreadySelected == "true");
//		var selectTag = document.getElementById("taglister:"+tagText);
//		var alreadySelected = (selectTag && (selectTag.getAttribute("match")));

//		// longer and more complicated but more robust?
//		var removeTag = new RegExp("(?:[ ]*(,)[ ]*"+tagText+"[ ]*,[ ]*"	// in-between tags 
//				+ "|^"+tagText+"[ ]*,[ ]*"
//					// beginning
//				+ "|" +tagText+"[, ]*$)");					// end
//
//		var alreadySelected = removeTag.test(tagsTextbox.value);
	
		if(alreadySelected == true)
		{
			var removeTag = new RegExp("(^|, *)"+tagText+"(?:, *|$)");
			tagsTextbox.value = tagsTextbox.value.replace(removeTag, "$1");
		}
		else
		{

			// what if it's already in the text box and you reselect it by typing?
			// if you type, don't put a comma, then decide to select the next one, it should end the most recent one with a comma, then go on
			if(tagsTextbox.value.match(/(?:^|, *)$/))
			{
				// always add no matter what
				tagsTextbox.value += tagText + ", ";
			}
			else
			{
				var match = event.originalTarget.getAttribute("match");
				if(match && match == "true")
				{
					var rindex = tagsTextbox.value.lastIndexOf(",");
					rindex = (rindex > 0 ? rindex+2 : 0);
					tagsTextbox.value = tagsTextbox.value.slice(0, rindex) + tagText + ", ";
				}			
				else // never falls through here?
				{
					tagsTextbox.value += ", " + tagText + ", ";
				}
			}
		}
		
		tagsTextbox.focus();
		highlightAll();
		
	}, true);
	return tagItem; 
}					

// called whenever we toggle from all tags to frequent tags
// also called whenever we change the value of the frequency limit
function updateFrequentlyUsed(tagNodes, toggle)
{
	freqlimit = GM_getValue("mostFrequentlyUsed",5);

	// make sure that the limit is not more than your number of tags
	freqlimit = tagNodes.length < freqlimit ? tagNodes.length: freqlimit;

	if(freqlimit <= 0)			
	{
		for( var i= 0; i < tagNodes.length; ++i)
		{
			tagNodes[i].style.display = "inline";
			tagNodes[i].style.fontWeight= "normal";
		}
		return;
	}
	/* get the most frequently used tags
		will not necessarily return *freqlimit*, may return more if
		several tags are tied for *freqlimit*th place
	*/ 
	for( var i= 0; i < tagNodes.length; ++i)
	{
		if(tagNodes[i].freq >= frequenttags[freqlimit-1][1])
		{		
			if(toggle)
				tagNodes[i].style.display = "inline";
			tagNodes[i].style.fontWeight = "bold";
		}
		else
		{
			if(toggle)
				tagNodes[i].style.display = "none";
			tagNodes[i].style.fontWeight = "normal";
		}

	}
}

function setupTagstoggle()
{
	tagslisttoggle.expanded = GM_getValue("expanded", true);
	updateFrequentlyUsed(tagNodes, true);			
	if(tagslisttoggle.expanded)
	{
		tagslisttoggle.innerHTML = "<<";
		for(var i = 0; i < tagNodes.length; ++i)
		{
			tagNodes[i].style.display = "inline";
		}
	}
	else
	{
		tagslisttoggle.innerHTML = ">>";
	}
}

// tagslisttoggle - what you click on to toggle between expanded and unexpanded taglist
function toggleTagsExpansion(tagslisttoggle)
{	
	tagslisttoggle.expanded = GM_getValue("expanded", true);

	if(!tagslisttoggle.expanded)
	{
		tagslisttoggle.innerHTML = "<<";
		for(var i = 0; i < tagNodes.length; ++i)
		{
			tagNodes[i].style.display = "inline";
		}
	}
	else
	{
		tagslisttoggle.innerHTML = ">>";
		updateFrequentlyUsed(tagNodes, true);			
	}
	
	tagslisttoggle.expanded = !tagslisttoggle.expanded;
	GM_setValue("expanded", tagslisttoggle.expanded);
}

function fetchTags()
{
usejournal = getJournal();

// reset list and fetch a new one
tagslist.innerHTML ="Fetching tags...";
// don't show toggle until there are actually tags to toggle
tagslisttoggle.innerHTML = "";


GM_xmlhttpRequest({
	method: "GET",
	url: "http://"+usejournal+".livejournal.com/tag/?format=light",	  // redirect on comm
	onload: function(details) {
		// create an IFRAME to write the document into. the iframe must be added	
		var iframe = document.createElement("IFRAME");
		iframe.style.visibility = "hidden";
		iframe.style.position = "absolute";
		iframe.style.height = "0";
		if(iframe)
		{			
			document.body.appendChild(iframe);
//			iframe.contentWindow.location.href = location.baseURI+"?usescheme=global";
			
			// write the received content into the document
			iframe.contentDocument.open("text/html");
			iframe.contentDocument.write(details.responseText);
			iframe.contentDocument.close();
			// wait for the DOM to be available, then do something with the document
			iframe.contentDocument.addEventListener("DOMContentLoaded", function() {
				/* fetch the data from the tags page */
				var ljtagiterator = iframe.contentDocument.evaluate("//ul[@class='ljtaglist']/li[text()]", iframe.contentDocument, null, XPathResult.UNORDERED_NODE_ITERATOR_TYPE, null);
				ljtaglist = new Array();
				var nextobj = ljtagiterator.iterateNext();
				while (nextobj) {
					ljtaglist.push([ nextobj.childNodes[0].innerHTML, parseInt(nextobj.lastChild.nodeValue.match(/\d+/)) ]);
					nextobj = ljtagiterator.iterateNext();
				}
				
				frequenttags = ljtaglist.slice(0);
				frequenttags.sort(function(a,b) { return (b[1] - a[1]); } );
				tagNodes = new Array();
				tagslist.innerHTML = "";				
				for (var i = 0; i < ljtaglist.length; i++) {
					temp = tagNodes.push(createTagItem(ljtaglist[i]));
					tagslist.appendChild(tagNodes[temp-1]);
					tagslist.appendChild(document.createTextNode(" "));
				}

				setupTagstoggle(tagslisttoggle);
				
				freqtext.addEventListener("change", function() {
					var freqlimit = (freqtext.value != "") ? freqtext.value : 0;
					GM_setValue("mostFrequentlyUsed", freqlimit);
					if(GM_getValue("expanded"))
					{						
						updateFrequentlyUsed(tagNodes, false);
					}
					else
					{
						updateFrequentlyUsed(tagNodes, true);
					}
				}, false);		// end CHANGE

        highlightAll();
			}, false);			// end DOMCONTENTLOADED
		}
		else
		{
			tagslist.innerHTML = "Error in fetching tags.";
		}

	}
});

}


function trim (str) {
	return str.replace(/^\s+/, '').replace(/\s+$/, '');
}

// eventually do something more complicated
function match(tagName, current, tag)
{
	if(tagName == current)
	{
		tag.style.borderColor = "gray";
		tag.setAttribute("match", false);
		tag.className = "selected";
		tag.setAttribute("selected", true);
		return 1;
	}
	else
	{
		if(tagName.match("^"+current))
		{
			tag.style.borderColor = "gray";
			tag.setAttribute("match", true);
			return 1;
		}
		else
		{
			tag.style.borderColor = "transparent";
			tag.setAttribute("match", false);
			return 0;
		}
	}
}

function resetMatches()
{
	for(var i = 0; i < ljtaglist.length; ++i)
	{
		tagNodes[i].style.borderColor = "transparent";
		tagNodes[i].setAttribute("match", false);
		tagNodes[i].setAttribute("selected", false);
		tagNodes[i].className = "";
	}
}

function autoHighlight()
{
	var autocomplete = highlightAll();
	// need to finish this, but the 'autocomplete' var is now the first string matched.
}

function highlightAll()
{
	var textBoxVal = document.getElementById("prop_taglist").value;
	var tagList = textBoxVal.split(/, */);
	resetMatches();

	var offset = -1;
	if(textBoxVal.match(/(?:^|, *)$/))
	{
		offset = 0;
	}

	// first highlight all the stuff that's already there
	for(var i = 0; i < (tagList.length) + offset; i++)
	{
		var current = trim(tagList[i].toLowerCase());
		if(current.length > 0)
		{
			var tag = document.getElementById("taglister:"+current);
			if(tag)
			{
				tag.className = "selected";
				tag.setAttribute("selected", true);
			}
		}
	}

	var firstmatch = "";
	if(offset == -1)
	{
		// now do the last one, for which we need to highlight possibilities too.
		var lastone = trim(tagList[tagList.length-1].toLowerCase());
		if(lastone.length > 0 )
		{
			for(var i = 0; i < ljtaglist.length; ++i)
			{
				if((match(ljtaglist[i][0], lastone, tagNodes[i])) && (firstmatch == ""))
				{
					firstmatch = ljtaglist[i][0];
				}
			}
		}
	}
	return firstmatch;
}

// hidden option purely for myself (so I don't have to worry about syncing files)
function uglifyUpdateBml()
{
	loc = document.getElementById("prop_current_location");
	music = document.getElementById("prop_current_music");
	mood = document.getElementById("mood_preview");
	upic_sel = document.getElementById("userpic_select_wrapper");
	upic_img = document.getElementById("userpic");
	
	previewlabel = document.createElement("label");
	previewlabel.className="left";
	previewlabel.innerHTML="Preview:";
	
	loc.parentNode.removeChild(loc.parentNode.firstChild);
	loc.parentNode.replaceChild(upic_sel, loc);
	
	music.parentNode.removeChild(music.parentNode.firstChild);
	music.parentNode.replaceChild(previewlabel, music.parentNode.firstChild);
	music.parentNode.replaceChild(upic_img,music);
	upic_sel.removeChild(document.getElementById("lj_userpicselect"));
	upic_img.parentNode.appendChild(mood);
	
	mood.style.left = "0";
	mood.style.position="static";

}

if(window == top && (document.getElementById("updateForm") != null ))
{		
	if(GM_getValue("uglify", false) == true)
	{
		uglifyUpdateBml();
	}
	tagNodes = new Array();
	ljtaglist = new Array();
	/*display the tagslist as a cloud of clickable tags*/
	tagslistpkg = document.createElement("p");
	tagslistpkg.id = "tagslistpkg";
	tagslistpkg.className ="pkg";

	tagslist = document.createElement("div");
	tagslist.id = "tagslist";
	
	tagslistlabel = document.createElement("label");
	tagslistlabel.className = "left";	
	tagslistlabel.innerHTML = "Tag List:"; 
	
	freqtext = document.createElement("input");
	freqtext.size = 1;
	freqtext.value = GM_getValue("mostFrequentlyUsed", 5);
	freqtext.className = "text";
	freqtext.style.cssFloat = "none";
	
	tagslisttoggle = document.createElement("a");
	tagslisttoggle.href = "";
	tagslisttoggle.addEventListener("click", function(event) {
		event.stopPropagation();
		event.preventDefault(); 

		toggleTagsExpansion(this);
	}, false);
	tagslistlabel.appendChild(document.createElement("br"));
	toplabel = document.createElement("span");
	toplabel.innerHTML = "Top ";
	toplabel.style.fontWeight = "normal";
	toplabel.style.fontSize = "small";
	tagslistlabel.appendChild(toplabel);
	tagslistlabel.appendChild(freqtext);				
	tagslistlabel.appendChild(tagslisttoggle);
	
	tagslistpkg.appendChild(tagslistlabel);
	tagslistpkg.appendChild(tagslist);
	
	tagsTextbox = document.getElementById("prop_taglist");			
	tagsTextbox.parentNode.insertBefore(tagslistpkg, tagsTextbox.nextSibling.nextSibling);

	// force contents of (nonempty) textbox to end with a comma
	var endWithComma = new RegExp("([^, ]+)[ ]*[,]?[ ]*$");
	tagsTextbox.value = tagsTextbox.value.replace(endWithComma, "$1, ");

	tagsTextbox.addEventListener("keyup", autoHighlight, false);
	/*style it, yez?*/
	GM_addStyle("#tagslistpkg a {text-decoration: none; padding: 0px 3px; border: 1px solid transparent;} #tagslistpkg a.selected {background-color: #ffffcc;} ");
	
	// fetch the tags on load	
	fetchTags();
	
	if(document.location.href.indexOf("update.bml") != -1)
	{
		// fetch the tags if you change the journal you're posting to	
		var journalselect = document.getElementById("usejournal");
		if(journalselect)
			journalselect.addEventListener("change", fetchTags, false); 
	}

}