2006-07-29

Blogger Categories

Change log:
  • 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:
  1. 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"
  2. 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>

  1. 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
    1. can edit the blog template ( Injecting handy JavaScript )
    2. can edit posts in Html, and even defining a template for it
    3. have syndication that doesn't alter your posts or escape it and that is available at the same domain
    4. 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.