2005-10-25

Blogger hack: inline comment faces

It's nice seeing who is talking to you, and the comment faces on blogger comment pages are a nice touch. So we would expect to see them when inlining the comments in our blogs too, right?



Wrong! Because, for some reason, no template tag exports commenter profile images to us. It sucks to want that, under such circumstances. And since the Blogger profiles are served from www.blogger.com and our blog pages are not, we can't do anything about circumventing this through means of AJAXing profile the pages to parse out the image URLs, either.

But what we can do, is show the whole profile pages to our visitors in <iframe> tags. Or, let's say, a portion of it, around the spot where user images go. It's a very ugly kludge, but it's an ugly problem, too. Fortunately for us, Blogger profile pages are quite static and predictable in how they position the profile owner's image. All right, so it's doable -- let's get down to business!

First off, you want to save your current template. Who knows, you might decide you liked it better this way, or you might end up ruining something -- and since nobody will be able to restore it afterward but yourself, this is the time to help the future you. Paste it into your favourite editor (or Notepad), and save it on disk somewhere.

Hint: don't get back to your browser now to edit it now; instead, save the template again under a new name, and keep editing it with a Real Editor. This is a very good habit, both since browsers typically lack the good editing facilities editors don't, and since browsers have more of a tendency of suddenly doing bad things, at least in my experience.

Now, let's dig up the part of our template where user comments go. Mine looked like this before I started (yours might not be exactly the same, but feel free to copy the layout of mine when it's done, if you are not fluent with HTML):
<BlogItemComments>
<li><a name="c<$BlogCommentNumber$>"></a>
<div class="comment-body">
<p><$BlogCommentBody$></p>
</div>
<p class="comment-data">By <$BlogCommentAuthor$>, on
<a href="#c<$BlogCommentNumber$>">
<$BlogCommentDateTime$>
</a><$BlogCommentDeleteIcon$>
</p>
</li>
</BlogItemComments>

Now, let's add a spot where our commenter's face goes, and another tiny bit that we'll come to need in a few moments:
<BlogItemComments>
<li><a name="c<$BlogCommentNumber$>"></a>
<div class="comment-head"></div>
<div class="comment-body">
<p><$BlogCommentBody$></p>
</div>
<p class="comment-data commented-by">By <$BlogCommentAuthor$>, on
<a href="#c<$BlogCommentNumber$>">
<$BlogCommentDateTime$>
</a><$BlogCommentDeleteIcon$>
</li>
</BlogItemComments>

Let's style this. In the <style type="text/css"> tag far up in the template, we'll mark the facial territory, setting it up to be 136 by 124 pixels (I did some trial and error measuring, and this seems to work fairly well at least for me) and go on the right:
.comment-head {
overflow:hidden;
height:124px;
width:136px;
float:right;
padding:0;
margin:0;
}

.face-clipper {
margin-left:-32px;
margin-top:-126px;
overflow:hidden;
height:250px;
width:168px;
}

The second class description defines another div tag inside the comment-head div, which we will be adding dynamically with javascript, for the purpose of clipping away the non-face portions of the profile, as best we can. (In theory, we ought to have been able to add this div to the static parts of the template, but I experienced some buggy behaviour with ghost text appearing in my firefox beta using that method.)

The rest, we will do with javascript. I will start off with one very useful method the Document Object Model still somehow lacks, but which we can mimic ourselves: document.getElementsByClassName(). Mine takes two parameters: the class name and an optional tag name, which further limits the returned node set to elements of the appropriate kind. Just to be kind to really old, crummy Internet Explorers which lack the document.getElementsByTagName, which the DOM does have in all modern browsers, let's start with cooking our own, as needed; I suggest putting this bit in your <head> tag, somewhere far up in the beginning of your template:
<script type="text/javascript"><!--
if( document.all && !document.getElementsByTagName )
document.getElementsByTagName = function( nodeName )
{
if( nodeName == '*' ) return document.all;
var result = [], rightName = new RegExp( nodeName, 'i' ), i;
for( i=0; i<document.all.length; i++ )
if( rightName.test( document.all[i].nodeName ) )
result.push( document.all[i] );
return result;
};
--></script>

And, using this tool, we now build document.getElementsByClassName() too (add this before the last line with the closing tag above):
document.getElementsByClassName = function( className, nodeName )
{
var result = [], tag = nodeName||'*', node, seek, i;
var rightClass = new RegExp( '(^| )'+ className +'( |$)' );
seek = document.getElementsByTagName( tag );
for( i=0; i<seek.length; i++ )
if( rightClass.test( (node = seek[i]).className ) )
result.push( seek[i] );
return result;
};

Now it's time for the real action; tuck in this function after the above methods:
function showFaces()
{
var comments, comlinks, i, node, re, by;
re = new RegExp( '^http://www.blogger.com/profile/\\d+', 'i' );
comments = document.getElementsByClassName( 'comment-head', 'div' );
comlinks = document.getElementsByClassName( 'commented-by' );
for( i=0; i<comments.length; i++ )
{
by = comlinks[i].getElementsByTagName( 'a' ).item( 0 );
if( !by || !re.test( by.href ) ) continue;
node = document.createElement( 'div' );
node.className = 'face-clipper';
node.innerHTML = '<iframe width="200" height="250" src="' +
by.href + '" frameborder="0" scrolling="no" />';
comments[i].appendChild( node );
}
}

This will track down all comment-head divs, find the URL to the person who wrote the comment (hence the commented-by class, to find them) and add a face-clipper div with a profile iframe inside. The extra attributes there make sure it doesn't get any ugly borders, scrollbars and whatnot, and we don't need it any bigger than the dimensions listed to find the face. In case the commenter was not a blogger user, we simply skip adding the extra tags.

To have make this work, in case your comment markup looked any differently, let's backtrack to the HTML code a little and look carefully at the bold bits in the HTML and in this piece of code; we need to match them up properly, or things will not work. We must make sure that the commented-by class name is set on the first tag surrounding the <$BlogCommentAuthor$> tag. (In my example, this was on a <p> tag. Now, starting there, we count how many <a> tags there are between the start of this tag and the <$BlogCommentAuthor$> tag; zero, in my example. This number goes in the item() ellipsis. (In case you have any other magic Blogger tags that expand to <a> tags, count those too.) If you get the number wrong, there won't be any faces at all, because the code will skip all comments, figuring they were not links to blogger users.

If you want to be really sure, you can make the <$BlogCommentAuthor$> tag into a <span class="commented-by"> <$BlogCommentAuthor$> </span> construct, and keep the zero from my example. Just don't have the commented-by class name in any more than one place in the HTML.

There is only one thing left now: invoke the code from somewhere. I prefer doing it with an onload handler, so the comment faces don't get loaded until all the rest of the page has been fetched. Find the <body> tag and add our code, making it <body onload="showFaces();"> instead. Try the template, save it and republish your blog.

You're done! Enjoy!

blog comments powered by Disqus