Anne van Kesteren

You request; we serve!

Today was the day of two exams and javascript to get over it. Only the latter is relevant here. For my first couple of hours with the language — although I admit I used it before — I learned quite a lot. It’s like most things: when you start doing it, you actually learn it. Today would not have been a great javascript day without the help of Sjoerd Visscher. Sjoerd is not only my colleague, but also the Wikipedia of javascript, javascript in Internet Explorer and everything.

When I started playing a little I found out about .href. After alerting it, it seemed Mozilla resolved the URI. Wait, let me finish. As this appeared to be correct per the interface of the A element — I never understood interfaces completely till now — I had to find another way. Fortunately I have some DOM knowledge and I quickly tried .getAttribute('href') which worked.

After playing around a bit more I fired up Internet Explorer. To my surprise it didn’t work out. All the DOM hype around the web — mostly on weblogs — made me think that IE is pretty good with that, however, .href apparently equals .getAttribute('href') in IEs implementation. And quickly .getAttribute('href').substring(9) was replaced with the ever more ugly regular expression and IE compatible equivalent: .href.match(/comment-(\d+)/)[1]. Ugh! Did I mention Sjoerd already?

Same thing happened with .setAttribute. I was trying to set an onclick event. Here’s what I ended up with: theLink.onclick = new Function("", "replyToComment("+comments[i].firstChild.href.match(/comment-(\d+)/)[1]+")");. Not a pretty sight. For completeness, here are the functions:

function replyToComment(commentId){
 var comment,url,link,metaP,commentAsString,textarea;
 comment = document.getElementById('comment-'+commentId);
 url = "http://annevankesteren.nl"; // without slash
 link = comment.lastChild.firstChild.href.substring(comment.lastChild.firstChild.href.match(/#/).index,url.length);
 metaP =  comment.removeChild(comment.lastChild); 
 commentAsString = comment.innerHTML;
 commentAsString = commentAsString.replace(/<[^>]+>/g, function(m) { return m.toLowerCase(); });
 commentAsString = "<blockquote cite=\""+link+"#comment-"+commentId+"\">"+commentAsString+"</blockquote>";
 comment.appendChild(metaP);

 textarea = document.getElementById('comment');
 textarea.value += commentAsString;
 textarea.focus();
}

function addReplyLinks(){
 var comments,theLink;
 comments = document.getElementById('comments');
 comments = comments.nextSibling;
 if(comments.nodeType != 1)
  comments = comments.nextSibling;
 comments = comments.getElementsByTagName('p');
 for(i=0;i<comments.length;i++){
  if(comments[i].className.indexOf('meta') != -1){
   theLink = document.createElement("a");
   theLink.appendChild(document.createTextNode('↓'));
   theLink.title = "Reply to " + comments[i].childNodes[0].title.substring(19);
   theLink.href = "#comment";
   theLink.onclick = new Function("", "replyToComment("+comments[i].firstChild.href.match(/comment-(\d+)/)[1]+")");
   comments[i].appendChild(document.createTextNode(' · '));
   comments[i].appendChild(theLink);
  }
 }
}

Obviously, this is Beta Production Work™ and still needs some cleanup. For even more thoroughness I might add that addReplyLinks is loaded in screen by Scott Andrew’s addEvent handler. Albeit slightly modified.

The reason behind all this is Lars Kasper, who puts a lot of effort in his replies and this is my small ‘javascript enhancement experiment’ to help him with that. If you still haven’t figured out what I changed; don’t you care. These are the days of unobtrusive javascript. Hip, cool and low profile.

Comments

  1. I test.

    Posted by Anne at

  2. Sounds a lot like every experience I've had with JS/DOM—frustrating.

    I truly wish I knew JS better, but every time I need to do something with it I end up never wanting to touch it again.

    Posted by Daniel Morrison at

  3. I test.

    Would be cool if it also added two new lines after the closing tag of <blockquote>.

    Posted by Patrys at

  4. Attribute values are also converted to lowercase, and you should actually use a charset parameter for script files aswell (and why not style sheets?), especially when you use non-ASCII characters.

    Talking about unobtrusive javascript, alerting ugh! to old browsers doesn't seem very unobtrusive to me... ;)

    Posted by zcorpan at

  5. You now need to add some more scripting to disable the use of e.g. GreaseMonkey and all other kinds of user scripts that may mess around with your DOM ;)

    Posted by Tino Zijdel at

  6. By the way: the most prefered way to attach eventhandlers is ofcourse by using the appropiate method for that: addEventListener (and for IE it's counterpart of the broken IE5 event model attachEvent). setAttribute is not meant to attach scripting data.
    But because of the broken propriety event model in IE indeed using the DOM 1 method is the best alternative.

    Posted by Tino Zijdel at

  7. I wrote one of these auto-reply things for my blog a while back but I never got around to implementing it. I quite like your unobtrusive (visually) implementation. Better than the clunky buttons I was using.

    Posted by Dean Edwards at

  8. Tino is correct- while setting the onclick property “works,” the DOM Event Model is the correct way to do this sort of thing.

    IE's JavaScript implementation is just as frustrating as it's implementation of other standards, but you knew that already. Watch out for it's bizarre behavior when you want to stop event propagation when using DOM Events.

    If you want to do JavaScript, go get the big O'Reilly book, it's a must-have for anyone starting out with this stuff.

    And you're right, JavaScript has come into it's own this year, due in large part (I think) to Google's extensive use of it for it's new applications.

    Posted by Ian Eure at

  9. I test.

    Welcome to the dark side :)

    Posted by Dimitri Glazkov at

  10. zcorpan, I should not and theoretically Apache should do it as I have AddDefaultCharset utf-8 specified, but as it doesn’t and I’m a UTF-8 fanatic I added AddCharset utf-8 .js .css to be sure and apparently that did the trick. Ugh! I also fixed the alerting thing, which was there mainly for debugging. About attribute values, you are right. Any suggestions?

    Tino, that was actually the first thing I tried, but it didn’t work out.

    Posted by Anne at

  11. Really cool functionality!

    This together with something like Derek's or Jonathan's new commenting functionality would be great!

    Posted by Robert Nyman at

  12. So shoot me. Apparently AddDefaultCharset only applies to text/plain and text/html.

    Posted by Anne at

  13. Anne> Did you try it using the addEvent macro? And by the way, although everyone probably knows about it already, one of the must reads beside the O'Reilly books is the wonderful Quirksmode, I don't think I've seen any other website with that much quality javascript packed at the same place

    Posted by masklinn at

  14. theLink.addEventListener('click', new Function('replyToComment(\'' + somevalues + '\')'), false);

    …should work, but you may want to do this:

    theLink.addEventListener('click', replyToComment, false);
    function replyToComment()
    {
      var commentId = this.parentNode.firstChild.href.match(/comment-(\d+)/)[1];
      // etcetera
    }

    Posted by Tino Zijdel at

  15. In regards to IEs broken getAttribute. IE treats element.getAttribute(sAttrName) as element[sAttrName] and therefore element.getAttribute("class") will not work because there is no such property. The end conclusion is to use DOM properties wherever possible.

    Javascript as a language is really nice. Just see Sjoerd's Loell. Too bad that there are no standard libraries that does anything useful, besides DOM that is - when it works as supposed which isn't often enough.

    Posted by Erik Arvidsson at

  16. To add IE support for addEventListener I came up with this:

    /* IE addEventListener
     *
     * Adds a wrapper around IE's attachEvent and lets you use addEventListener
     * It also works around the issue where the normal scope of the event triggered
     * is the window object instead of the event's target. Unfortunately in bubbling
     * fase (IE does not support capturing fase) this means that the 'this'-keyword
     * will always point to the element that triggered the event instead of the current
     * element in the bubbling fase. That is a shortcoming of IE's event-model, and you
     * may want to consider to fall back to the DOM 1 model (but then you cannot
     * dynamically attach multiple functions to the same event on one element, although
     * you could probably write a wrapper for that too).
    */
    if (window.attachEvent && document.getElementsByTagName)
    {
    	var el = document.getElementsByTagName('*'), i = el.length;
    	while (i--)
    	{
    		el[i].addEventListener = function(type, funcref, onlyBubbles)
    		{
    			this.attachEvent('on'+type, new Function('attachEventWrapper('+funcref+')'));
    			/* or if you like (works around the 'this'-keyword issue, but
                               doesn't allow multiple functions to the same event on the element):
    			this['on'+type] = funcref;
    			*/
    		}
    	}
    
    	function attachEventWrapper(funcref)
    	{
    		funcref.apply(event.srcElement);
    	}
    }

    Posted by Tino Zijdel at

  17. Your function is far too complicated Tino.

    Here is the little event handling registerer from Matt Kruse's website.

    // Utility function to add an event listener
    function addEvent(o,e,f){
    	if (o.addEventListener){ o.addEventListener(e,f,true); return true; }
    	else if (o.attachEvent){ return o.attachEvent("on"+e,f); }
    	else { return false; }
    }
    

    To use it, just do

    // Automatically attach a listener to the window onload, to convert the trees
    addEvent(object,"event",function_name);

    Example:

    addEvent(window,"load",processNodes);

    Posted by masklinn at

  18. masklinn, that function was written once by Scott Andrew, as mentioned in my post.

    Posted by Anne at

  19. masklinn: you're missing the point; first of all it is much more 'natural' to use a method on an object than a functioncall. Secondly Scott's method doesn't account for the difference between addEventListener and attachEvent with respect to scope. He even uses capturing on the addEventListener wereas IE's method only supports bubbling - that's also something you should be aware of.
    Basically using object.onevent=function_name is better that using this addEvent function because than at least you will have the same behavior in IE.

    Also I'm not too font on using the onload handler on the window object. Especially on pages that contain images the onload is triggered only after all the images have loaded, which in many cases is too late and will cause noticable 'jumpy' effects on the page. Don't shy to use some inline scripting here and there; the DOM is already available during page rendering, and some functionality you may want to add to elements as soon as they are available in the DOM. Yes, it pollutes markup, but that doesn't make it obtrusive. (There should actually be some kind of onrenderend event...).

    Posted by Tino Zijdel at

  20. Anne > damn, missed it, shame on me

    Tino > About bubbling Vs capturing, the only thing I can tell you is that... nothing stopped me from editing the function to use bubbling in both cases (yes, I was aware of that). About the jumpy effect, I see what you mean and I think I'd agree with you, but I have yet to be annoyed by this specific problem.

    Side note: Anne, have you ever thought of putting access keys on your preview/post buttons? Say S on the preview button, for example (IM style)

    Posted by Masklinn at

  21. I test.

    Thank you very much! :-)

    (A good example for useful JavaScript.)

    Posted by Lars Kasper at

  22. So shoot me. Apparently AddDefaultCharset only applies to text/plain and text/html.

    You can use the following to force charsets on other content types:

    AddType text/javascript;charset=utf-8 .js
    AddType text/css;charset=utf-8 .css

    Posted by Louis Bennett at

  23. By the way Anne; your javascript generates a strict warning:

    Warning: function addReplyLinks does not always return a value
    Source File: http://annevankesteren.nl/js/comments
    Line: 49

    Posted by Tino Zijdel at

  24. Using new Function(...) is a slow way to do it; it has the same performance penalty as eval(). A better idea would be to simply pass a function — e.g. replyToComment — and have it deduce whatever information it needs to, as Tino suggested.

    Posted by Aankhen at

  25. Do you need the first empty string parameter in the Function() constructor? Can't you just write it like this:

    theLink.onclick = new Function("replyToComment(" + comments[i].firstChild.href.match(/comment-(\d+)/)[1] + ")");

    I at least think you can.

    Posted by Asbjørn Ulsberg at

  26. masklin: this is not a good idea:
    if (o.addEventListener){ o.addEventListener(e,f,true); return true; }
    Do not use capturing event handlers, make the third argument to addEventListener a "false". See http://my.opera.com/hallvors/journal/47

    Posted by Hallvord R. M. Steen at

  27. I agree with Aankhen; avoid the Function constructor if at all possible. (Hint: having been writing JavaScript since it was born in Netscape 2, I've never found a situation where it was necesssary.)

    Remember that in an event handler, this holds a reference to the element on which the event is firing. Therefore you can set the event handler:

    theLink.onclick = handleLinkClick;
    

    and then just do the stuff to get the information out of the link using this; for example:

    function handleLinkClick() {
       alert(this.getAttribute("href"));
    }
    

    will pop up an alert with the value of the href attribute of the clicked link.

    It is a real pain that IE always tacks the protocol and domain on the front of the href attribute. You might want to be warned that it does the same thing with the img element's src attribute.

    Good luck with your experiments; as a long-term programmer, I've found JavaScript to be a great language to work with.

    Posted by Nick Fitzsimons at

  28. Nick, this sounds useful. (Pun intended for a bit.) If I find some time next weekend (not this) I’ll try it out. Or maybe a little bit earlier. Throughout the week or so.

    Posted by Anne at

  29. I'm trying to get this working on my site, but I don't quite seem to manage. I wish I knew more JavaScript. Anyway, here's how my comments list is structured:

    <ol id="comment-list">
    <li id="comment-1">
    <dl>
    <dt class="gravatar"><img
    src="/some/gravatar" width="20" height="20"
    alt="This commenter’s Gravatar" /></dt>
    <dt><a href="http://domain.ext/" title="Go to
    domain.ext">Someone</a>:</dt>
    <dd><p>Blah blah blah.</p></dd>
    <dd class="comment-posted"><cite><a
    href="#comment-1" title="Permanent link for comment 1 on
    This Post" rel="bookmark">Comment posted on May 30th,
    2005 @ 6:01 pm</a></cite></dd>
    </dl>
    </li>
    </ol>

    I made the following changes to the JavaScript:

    1. Changed url = "http://annevankesteren.nl"; // without slash into url = "http://mathibus.com";, obviously;
    2. Changed comments = comments.getElementsByTagName('p'); into comments = comments.getElementsByTagName('dd'); (see my comments list structure above);
    3. Changed if(comments[i].className.indexOf('meta') != -1){ into if(comments[i].className.indexOf('comment-posted') != -1){ (see my comments list structure above).

    Any ideas?

    Posted by Mathias Bynens at

  30. Never mind, I got it working now. Thanks for the email-wise help, Anne!

    Posted by Mathias Bynens at

  31. Never mind, I got it working now. Thanks for the email-wise help, Anne!

    Mathias, thanks for sharing this resource, this is one nice snippet of code! I'd love to use this on my site. I'll try to integrate this on the weekend. Thanks Anne.

    Posted by markku at