Change log:
This is a clean (hopefully), self-contained, client side solution to Blogger Categories To View its' features check this Demo as my blog isn't fat enough to exploit categorization :( .
(This post was written prior to Blogger beta relaunch which amongst it's features "labeling posts" ..pretty much "categories" , though i'll continue to maintain this script )
to Add Categories to your blog follow:
- 2006-07-29 | V0.1b | Initial release
- 2006-08-13 | V0.11b | fix -> Category tag escaped for long/image/video posts
- 2006-08-14 | aware of blogger beta relaunch :-)
- 2006-08-18 | V0.111b | fix->more robust fix for escaped tags (the last fix failed against malformed xml.
This is a clean (hopefully), self-contained, client side solution to Blogger Categories To View its' features check this Demo as my blog isn't fat enough to exploit categorization :( .
(This post was written prior to Blogger beta relaunch which amongst it's features "labeling posts" ..pretty much "categories" , though i'll continue to maintain this script )
to Add Categories to your blog follow:
Users Guide:
- When Composing the post switch to "Edit Html" and the following
<category cats="category1, category2, ...., categoryN"></category>
Tip: to make this in an automated fashion add it to the "Post Template" form "Settings- > Formatting" - Apply the following to the "Template"
- Insert between section <head></head><script type="text/javascript" >
/* Prototype JavaScript framework, version 1.4.0_pre10_ajax
* (c) 2005 Sam Stephenson <sam@conio.net>
*
* This is a downcut version for AJAX by Alexander Kirk http://alexander.kirk.at/
*
* Prototype is freely distributable under the terms of an MIT-style license.
*
* For details, see the Prototype web site: http://prototype.conio.net/
*
/*--------------------------------------------------------------------------*/
var Prototype = {
Version: '1.4.0_pre10_ajax',
emptyFunction: function() {},
K: function(x) {return x}
}
var Class = {
create: function() {
return function() {
this.initialize.apply(this, arguments);
}
}
}
var Abstract = new Object();
Object.extend = function(destination, source) {
for (property in source) {
destination[property] = source[property];
}
return destination;
}
Object.inspect = function(object) {
try {
if (object == undefined) return 'undefined';
if (object == null) return 'null';
return object.inspect ? object.inspect() : object.toString();
} catch (e) {
if (e instanceof RangeError) return '...';
throw e;
}
}
Function.prototype.bind = function(object) {
var __method = this;
return function() {
return __method.apply(object, arguments);
}
}
Function.prototype.bindAsEventListener = function(object) {
var __method = this;
return function(event) {
return __method.call(object, event || window.event);
}
}
Object.extend(Number.prototype, {
toColorPart: function() {
var digits = this.toString(16);
if (this < 16) return '0' + digits;
return digits;
},
succ: function() {
return this + 1;
},
times: function(iterator) {
$R(0, this, true).each(iterator);
return this;
}
});
var Try = {
these: function() {
var returnValue;
for (var i = 0; i < arguments.length; i++) {
var lambda = arguments[i];
try {
returnValue = lambda();
break;
} catch (e) {}
}
return returnValue;
}
}
/*--------------------------------------------------------------------------*/
var PeriodicalExecuter = Class.create();
PeriodicalExecuter.prototype = {
initialize: function(callback, frequency) {
this.callback = callback;
this.frequency = frequency;
this.currentlyExecuting = false;
this.registerCallback();
},
registerCallback: function() {
setInterval(this.onTimerEvent.bind(this), this.frequency * 1000);
},
onTimerEvent: function() {
if (!this.currentlyExecuting) {
try {
this.currentlyExecuting = true;
this.callback();
} finally {
this.currentlyExecuting = false;
}
}
}
}
/*--------------------------------------------------------------------------*/
function $() {
var elements = new Array();
for (var i = 0; i < arguments.length; i++) {
var element = arguments[i];
if (typeof element == 'string')
element = document.getElementById(element);
if (arguments.length == 1)
return element;
elements.push(element);
}
return elements;
}
var Ajax = {
getTransport: function() {
return Try.these(
function() {return new ActiveXObject('Msxml2.XMLHTTP')},
function() {return new ActiveXObject('Microsoft.XMLHTTP')},
function() {return new XMLHttpRequest()}
) || false;
}
}
Ajax.Base = function() {};
Ajax.Base.prototype = {
setOptions: function(options) {
this.options = {
method: 'post',
asynchronous: true,
parameters: ''
}
Object.extend(this.options, options || {});
},
responseIsSuccess: function() {
return this.transport.status == undefined
|| this.transport.status == 0
|| (this.transport.status >= 200 && this.transport.status < 300);
},
responseIsFailure: function() {
return !this.responseIsSuccess();
}
}
Ajax.Request = Class.create();
Ajax.Request.Events =
['Uninitialized', 'Loading', 'Loaded', 'Interactive', 'Complete'];
Ajax.Request.prototype = Object.extend(new Ajax.Base(), {
initialize: function(url, options) {
this.transport = Ajax.getTransport();
this.setOptions(options);
this.request(url);
},
request: function(url) {
var parameters = this.options.parameters || '';
if (parameters.length > 0) parameters += '&_=';
try {
if (this.options.method == 'get')
url += '?' + parameters;
this.transport.open(this.options.method, url,
this.options.asynchronous);
if (this.options.asynchronous) {
this.transport.onreadystatechange = this.onStateChange.bind(this);
setTimeout((function() {this.respondToReadyState(1)}).bind(this), 10);
}
this.setRequestHeaders();
var body = this.options.postBody ? this.options.postBody : parameters;
this.transport.send(this.options.method == 'post' ? body : null);
} catch (e) {
}
},
setRequestHeaders: function() {
var requestHeaders =
['X-Requested-With', 'XMLHttpRequest',
'X-Prototype-Version', Prototype.Version];
if (this.options.method == 'post') {
requestHeaders.push('Content-type',
'application/x-www-form-urlencoded');
/* Force "Connection: close" for Mozilla browsers to work around
* a bug where XMLHttpReqeuest sends an incorrect Content-length
* header. See Mozilla Bugzilla #246651.
*/
if (this.transport.overrideMimeType)
requestHeaders.push('Connection', 'close');
}
if (this.options.requestHeaders)
requestHeaders.push.apply(requestHeaders, this.options.requestHeaders);
for (var i = 0; i < requestHeaders.length; i += 2)
this.transport.setRequestHeader(requestHeaders[i], requestHeaders[i+1]);
},
onStateChange: function() {
var readyState = this.transport.readyState;
if (readyState != 1)
this.respondToReadyState(this.transport.readyState);
},
evalJSON: function() {
try {
var json = this.transport.getResponseHeader('X-JSON'), object;
object = eval(json);
return object;
} catch (e) {
}
},
respondToReadyState: function(readyState) {
var event = Ajax.Request.Events[readyState];
var transport = this.transport, json = this.evalJSON();
if (event == 'Complete')
(this.options['on' + this.transport.status]
|| this.options['on' + (this.responseIsSuccess() ? 'Success' : 'Failure')]
|| Prototype.emptyFunction)(transport, json);
(this.options['on' + event] || Prototype.emptyFunction)(transport, json);
/* Avoid memory leak in MSIE: clean up the oncomplete event handler */
if (event == 'Complete')
this.transport.onreadystatechange = Prototype.emptyFunction;
}
});
Ajax.Updater = Class.create();
Ajax.Updater.ScriptFragment = '(?:<script.*?>)((\n|.)*?)(?:<\/script>)';
Object.extend(Object.extend(Ajax.Updater.prototype, Ajax.Request.prototype), {
initialize: function(container, url, options) {
this.containers = {
success: container.success ? $(container.success) : $(container),
failure: container.failure ? $(container.failure) :
(container.success ? null : $(container))
}
this.transport = Ajax.getTransport();
this.setOptions(options);
var onComplete = this.options.onComplete || Prototype.emptyFunction;
this.options.onComplete = (function(transport, object) {
this.updateContent();
onComplete(transport, object);
}).bind(this);
this.request(url);
},
updateContent: function() {
var receiver = this.responseIsSuccess() ?
this.containers.success : this.containers.failure;
var match = new RegExp(Ajax.Updater.ScriptFragment, 'img');
var response = this.transport.responseText.replace(match, '');
var scripts = this.transport.responseText.match(match);
if (receiver) {
if (this.options.insertion) {
new this.options.insertion(receiver, response);
} else {
receiver.innerHTML = response;
}
}
if (this.responseIsSuccess()) {
if (this.onComplete)
setTimeout(this.onComplete.bind(this), 10);
}
if (this.options.evalScripts && scripts) {
match = new RegExp(Ajax.Updater.ScriptFragment, 'im');
setTimeout((function() {
for (var i = 0; i < scripts.length; i++)
eval(scripts[i].match(match)[1]);
}).bind(this), 10);
}
}
});
Ajax.PeriodicalUpdater = Class.create();
Ajax.PeriodicalUpdater.prototype = Object.extend(new Ajax.Base(), {
initialize: function(container, url, options) {
this.setOptions(options);
this.onComplete = this.options.onComplete;
this.frequency = (this.options.frequency || 2);
this.decay = 1;
this.updater = {};
this.container = container;
this.url = url;
this.start();
},
start: function() {
this.options.onComplete = this.updateComplete.bind(this);
this.onTimerEvent();
},
stop: function() {
this.updater.onComplete = undefined;
clearTimeout(this.timer);
(this.onComplete || Ajax.emptyFunction).apply(this, arguments);
},
updateComplete: function(request) {
if (this.options.decay) {
this.decay = (request.responseText == this.lastText ?
this.decay * this.options.decay : 1);
this.lastText = request.responseText;
}
this.timer = setTimeout(this.onTimerEvent.bind(this),
this.decay * this.frequency * 1000);
},
onTimerEvent: function() {
this.updater = new Ajax.Updater(this.container, this.url, this.options);
}
});
</script> - Change <body> to <body onload="getAtom(event)">
- Add a placeholder for the category list in the menu sidebar (or wherever)<h2 class="sidebar-title">Categories <font color="red" style="font-size:large" ><sup>BETA</sup > </font></h2> <img id="waiting" src="http://photos.blogger.com/img/icon_inprogress.gif"/> <ul id="catList"></ul>
- Insert before </body><script type="text/javascript" >
/**
*Blogger Categories
*@author Eslam Ahmed Al-Morshdy (e_morshdy At acm Dot org
*@license GPL
*/
log = function(msg){
document.getElementById('out').value +=msg;
};
thenode=null;
lastcat=null;
theparent=null;
function handle_onmouseover(event)
{
var element = (window.event!=undefined)? window.event.srcElement: event.target;
var parent= element.parentNode;
if(parent.tagName != "LI"){ return;};
if( parent.getElementsByTagName("A").length == 0 ){ return;};
var cat = parent.getElementsByTagName("A")[0]["theCategory"];
if(lastcat==cat){return;};
if(thenode != null)
{
thetitle['style']['fontWeight'] = 'normal';
thetitle['style']['fontSize']= 'small';
thenode['style']['display']='none';
};
lastcat=cat;
thetitle=parent.getElementsByTagName("A")[0];/*category title*/;
thetitle['style']['fontWeight']='bolder';
thetitle['style']['fontSize']='large';
thenode = parent.childNodes[2];/*the list*/;
thenode["style"]["display"]='list-item';
};
function handle_onmouseout(event)
{
var element = (window.event!=undefined)? window.event.srcElement: event.target;
if(element.tagName!="DIV"){return;};
if(thenode != null)
{
thetitle['style']['fontWeight'] = 'normal';
thetitle['style']['fontSize']= 'small';
thenode['style']['display']='none';
thenode=null;
thetitle=null;
lastcat=null;
};
};
var e;
function showResponse(event)
{
if (event)
e=event;
/*Parsing the Atom*/;
if(!e || !e.responseXML)
{
reportError();
return;
};
response = e;
root=e.responseXML.documentElement
/*log(responseText);*/
root.getElementsByTagName("content")
var entries = root.getElementsByTagName("entry");
categories = {};
categories['all']=[];
categories['other']=[];
if(entries==null)
{
return;
}
else
{ for(var entryCount = 0; entryCount < entries.length ; entryCount++)
{
//log("Starting entry :"+entryCount);
var contents = entries[entryCount].getElementsByTagName("content");
if( (null == contents )||(contents.length == 0 ))
{
reportError();
return;
}
if( contents[0].getAttribute("mode") == null )
{
var category = entries[entryCount].getElementsByTagName("category");
if(( null==category )||( category.length ==0))
{
cats="";
}
else
{
var cats = category[0].getAttribute("cats");
};
}else
{
var text ;
if(contents[0].textContent){text = contents[0].textContent}
else if(contents[0].text){text = contents[0].text}
else{reportError();return;}
var start= text.indexOf("<category");
var end= text.indexOf("</category>");
var cats = text.slice(start,end);
start = cats.indexOf("cats");;
end = cats.indexOf("\">");
cats = cats.slice(start+6,end)
}
cats = cats.toLowerCase();
cats = cats.split(",");
//log(cats+'\n');
if(cats.length==0 || cats[0]=="")
{
//log("OTHER")
link = entries[entryCount].getElementsByTagName("link");
for(var x = 0 ; x < link.length ; x++ )
if(link[x].getAttribute("rel") == "alternate" )
{
link=link[x];
var found=true;
break;
};
if(!found){
continue;
};
var title = link.getAttribute("title") ;
var link = link.getAttribute("href") ;/*destructive*/;
categories['other'][categories['other'].length]={'link':link,'title':title};
categories['all'][categories['all'].length]={'link':link,'title':title};
}
else
{
link = entries[entryCount].getElementsByTagName("link");
for(var x = 0 ; x < link.length ; x++ )
{
if(link[x].getAttribute("rel") == "alternate" )
{
link=link[x];
break;
};
};
var title = link.getAttribute("title") ;
var link = link.getAttribute("href") ;/*destructive*/;
for(var x = 0 ; x < cats.length ; x++)
{
if(categories[cats[x]] == undefined)
{
categories[cats[x]]=new Array();
};
var indx = categories[cats[x]].length;
categories[ cats[x] ][ indx ]={'link':link,'title':title};
categories['all'][categories['all'].length]={'link':link,'title':title};
};
};
};
};
for(var x =0 ; x < categories['all'].length ; x++)
{
for(var y =x+1 ; y < categories['all'].length ; y++)
{
if(categories['all'][x]['link'] == categories['all'][y]['link'])
{
categories['all'].splice(y,1);
y--;
};
};
};
/*Building UI elements*/;
computedLinks={};
computedCategories={};
for(var x in categories)
{
var item = document.createElement("li");
var subitem = document.createElement("A");
var subitem2 = document.createElement("FONT");
var text = document.createTextNode(x);
var text2= document.createTextNode('('+categories[x].length+')');
if(subitem.attachEvent)
{
subitem.attachEvent('onclick',handle_onmouseover);
}
else
{
subitem.setAttribute('onclick',"handle_onmouseover(event)");
};
subitem2['color']="orange";
subitem['style']['cursor']="pointer";
subitem.theCategory=x;
subitem2.appendChild(text2);
subitem.appendChild(text);
item.appendChild(subitem);
item.appendChild(subitem2);
computedCategories[x]=item;
computedLinks[x]= document.createElement("ul");
computedLinks[x]['style']['display']="none";
for(var y = 0 ; y < categories[x].length;y++ )
{
var link = document.createElement("a");
link["href"]=categories[x][y]['link'];
var text = document.createTextNode(categories[x][y]['title'] );
link.appendChild(text);
var listItem = document.createElement("li");
listItem['style']['fontWeight']="normal";
listItem['style']['fontSize'] = 'small';
listItem.appendChild(link);
computedLinks[x].appendChild(listItem);
};
computedCategories[x].appendChild(computedLinks[x]);
};
/*clear waiting spin*/;
document.getElementById("waiting")['style']['display']='none';
/*Appending categories to the catlistt*/;
var tempList = document.getElementById("catList");
/*First remove empty categories mistakes*/
if(computedCategories[''] != undefined)
{
delete computedCategories[''];
};
/*Second append in the list*/
for(var x in computedCategories )
{
tempList.appendChild(computedCategories[x]);
};
/*Registering Collapse to the sidebar*/;
var tempSidebar= document.getElementById("sidebar");
if(tempSidebar.attachEvent)
{
tempSidebar.attachEvent("onmouseout",handle_onmouseout) ;
}
else
{
tempSidebar.setAttribute("onmouseout","handle_onmouseout(event)");
};
};
function getAtom()
{
var temp = window.document.location.toString();
var url = temp.substr(0, 7 + temp.substr( 7 , temp.length ).indexOf('/') ) + "/atom.xml";
reportError=function()
{
document.getElementById("waiting")['style']['display']='none';
var theitem = document.createElement("li");
var subitem = document.createElement("FONT");
var text= document.createTextNode("\n\n\nfailed request for:\n "+url+"\n\n propably you are in preview of blog template");
subitem['color']="red";
subitem.appendChild(text);
theitem.appendChild(subitem);
document.getElementById("catList").appendChild(theitem);
};
var pars = '';
try
{
var myAjax = new Ajax.Request(url,
{method:'get',
parameters:pars,
asynchronous:true,
onFailure:reportError,
onSuccess:showResponse});
}
catch (e)
{
reportError();
};
};
doEval = function(event)
{
alert('how') ;
eval( document.getElementById('in').value );
};
</script>
- You are done feel free to comment anyfeedback is most welcome
Developer Notes:
- Blogger supported categories from a while!! yes it did when it introduced syndication (don't know when)?! lets' see what we have
- can edit the blog template ( Injecting handy JavaScript )
- can edit posts in Html, and even defining a template for it
- have syndication that doesn't alter your posts or escape it and that is available at the same domain
- No thanks ..... but Thanks!! that's enough for introducing categorization
- The accompanying script parses the corresponding atom syndication of your blog searching for the <category> tag in each <entry> This is totally self contained solution to categorizing blogger
- no third party to which you hand the view of your content
- no redirection to a foreign page (social-book marking blogger-search)
- no server abuse
- each page impression acts as if some one reading ur atom syndication
- Blog template + the script ~32kb could be stripped to ~24kb
- no expected localization problems
- totally client side processing.
- Asynchronous no blocking (AJAX-Based).
- posts count
- not that painful (but thanks to them i was forced to do it)
Behave! - works out of the box yet is highly configurable
- handle old posts: viewable from all and other tags no need to re-publish them
- free as in Willy ;-)
- Issues
- Categories aren’t available in preview of a modified template?
- Categories rely on parsing the Atom feed available relative to your blog at
http://name.blogspot.com /atom.xml
but the domain from which the preview is fetched is a sessioned access to
http://www.blogger.com/blog-preview.g
and xmlHTTPrequest couldn't (and shouldn't) be issued for pages that doesn't share same domain.
- Why not use RSS instead of Atom for the parsing issue?
- Atom preserves < > of the embedded tag unlike rss that escapes it.
- not necessarily long/image/video posts do get escaped :-( but this fix did the trick
- no, the parser requires a well formed xml which isn't gauranteed
I had to search for tags in the textual representation of the <content> tag. - Script size: a stripped down version of prototype by Alexander Kirk was used.
- A Downside: The embedded tag <category> </category> breaks the conformance of the post to HTML but luckily it's gracefully ignored by rendering browsers and (Firefox, IE and safari ) feed-readers.
- Feedback: Feature requests, comments, error reports, uncaught exceptions :(, are most welcome.