Twitter: raymondcamden


Address: Lafayette, LA, USA

Using jQuery to mimic the NYT's new paragraph linking

12-03-2010 10,020 views jQuery, JavaScript 33 Comments

Yesterday I read about a new paragraph linking system the New York Times has added to their site. The article linked to in the previous sentence describes it, but the gist is that you can add hash marks to focus in to a specific paragraph or sentence and do highlighting as well. This is really cool as it lets people linking to a story specifically call out a certain section. During lunch I decided to see if I could build something similar in jQuery. I specifically wanted to build a completely client side solution that did not - in any way - impact the textual data being served up from the CMS. I assume the NYT did the same. Here is what I came up with. It currently only supports paragraph selecting and highlighting, but I've got an idea on how to handle sentences as well. I wrote this very quickly so I'm sure it could be done much better.

view plain print about
1<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
2<script>
3$(document).ready(function() {
4
5    //get anchor
6    console.log('loaded');
7    if(document.location.hash != '') {
8        var hash = document.location.hash.substr(1,document.location.hash.length);
9        var type = hash.substr(0,1).toLowerCase();
10        
11        //ok, so if hash is PN, go to paragraph N
12        if(type == "p" || type == "h" ) {
13            var pindex = hash.substr(1, hash.length);
14            pindex = parseInt(pindex);
15            if(isNaN(pindex)) return;
16            console.log('going to load paragraph '+pindex);
17            //now find it
18            var para = $("p:nth-child("+pindex+")");
19            var top = para.position().top;
20            console.log(top);
21            window.scrollTo(0, top);
22            if(type == "h") para.addClass("highlight");
23            console.log('done');
24        }
25
26    }
27})
28</script>
29<style>
30.highlight {
31    background-color: yellow;
32    font-weight: bold;
33    color: red;
34}
35</style>

The code begins by checking to see if we even have a hash in the URL. If we do, we get the value. Notice that I remove the first character since document.location.hash includes the # within it. (Seems a bit silly, but oh well.) Next I get the first character. A "p" implies we are just going to scroll to a particular paragraph while "h" implies scrolling and highlighting as well. I do a bit of parsing to get the value after the first letter and call it my pindex. This is the Nth paragraph I want to use.

Once we have that - we can then easily use the nth-child selector in jQuery to find that particular paragraph. I don't have logic to check to see if the paragraph requested is more than the actual number of paragraphs, but I figure that's ok. I grab it's Y value by calling position() on the item and getting the top value. Then I simply use the scrollTo function of the window. The final piece to this is to use an addClass() if we were doing a highlight and not just a scroll. So how well does it work? Here are a few examples.

Paragraph 2

Paragraph 4

Highlight Paragraph 3

As I said - I wrote this very quickly - and I'm sure the NYT's system is much better, but it was pretty fun to create this myself in jQuery. Tomorrow I'm going to try attacking the sentence specific selecting/highlighting.

33 Comments

  • Commented on 12-03-2010 at 7:08 AM
    Using FF3.6 / WinXP at work, it's jumping to the appropriate paragraph, but that's it. No highlights, etc.
  • Commented on 12-03-2010 at 7:11 AM
    So all 3 work, but the 3rd one doesn't highlight? If so - could you do a quick test of just addClass? That's a fairly simple jQuery call. I'd be surprised if it didn't work right.
  • Commented on 12-03-2010 at 7:17 AM
    What I meant to say is none of your demos are highlighting anything. There's no scrolling, but it does jump to the appropriate paragraph (because that's already built into the browser, if you specify a #id and it's the same name as an id on the page, the browser will already jump to that section). Let me see if I can debug what's going on. :|
  • Commented on 12-03-2010 at 7:23 AM
    The highlighting is working now, whatever you fixed. :)
  • Commented on 12-03-2010 at 7:24 AM
    Alright, if I leave firebug open, your code works. If I close firebug, your code doesn't work. I guess all the console.logs() are screwing things up.
  • Commented on 12-03-2010 at 7:39 AM
    For me same, nothing happens in FF unless I activate Firebug. Chrome works flawlessly (winXP).
  • Commented on 12-03-2010 at 7:56 AM
    Before Firefox 4 there was no native console. You only got that via Firebug. In general haveing console calls in production or publication code is a bad idea.

    What happens if the document has an element with id="p1"? An easy check if the given hash is already known to the document could be: $(':target').length
  • xibris #
    Commented on 12-03-2010 at 7:58 AM
    does not work in firefox
  • Ahmed Almonajed #
    Commented on 12-03-2010 at 7:59 AM
    Hi Raymond,
    The demos doesn't work totally and when i tried to trace your demos code, the following error message appeared "console is undefined".
    I don't know if this is the main problem that prevents the code to be executed.

    Thanks for your important and helpful posts, I'm using them as my tutorials :)
  • David Young #
    Commented on 12-03-2010 at 8:12 AM
    jQuery(function($){
             var a=location.hash.length>0?location.hash.replace(/\#/g,''):null,index=0,$p=$('p'),y=0;
             if(a!==null){
                index=((a.replace(/\D/g,''))-1);
                y=$p.eq(index).addClass('highlight').offset().top;
                $('html, body').animate({"scrollTop":y}, 250);
             }      
          })

    it's probably better to use the .eq() method rather than the nth-child selector, since the nth-child selector will return ALL paragraphs that are the nth-child of their parent. In addition, I did not do all the checking you had for highlight vs. scrollto...I just simply set it to scroll the window to the paragraph and highlight it by adding the class.
  • Commented on 12-03-2010 at 8:41 AM
    Hi Ray -

    Yes - I concur with Ahmed - I get the "console undefined" issue - running IE8 on XP
  • Commented on 12-03-2010 at 8:42 AM
    Guys - sorry - I thought all web developers used a 'good' browser with console support. ;) I've removed the console messages.
  • Commented on 12-03-2010 at 8:45 AM
    @rodney - hashes don't work with IDs, they only work with <a name=""></a> pairs, so an existing ID of p1 wouldn't matter (afaik).

    @David Young: My thought was that there would normally be a greater div around article content. Like <div id="article"> and that you would simply add that to your main selector. I had thought about doing the animation - but I wanted it more immediate. Personal preference I guess. ;)
  • Commented on 12-03-2010 at 8:51 AM
    Ray, hashes do work with ids. You can anchor directly to an id on the page. Case in point, I have an id called 'footer' on my about page ( http://web-rat.com/about/#footer ). The browser will attempt to scroll down to footer.

    If it doesn't work, don't maximize the browser to the fullest, shrink it a little bit and make sure the scrollbar is active and try it.
  • David Young #
    Commented on 12-03-2010 at 8:52 AM
    @Raymond

    I agree there would likely be a containing parent element, but in cases where something like this might be implemented on a complex CMS where there are multiple people making edits, you would never want to take anything for granted. Even though there might be a containing div that you use as your base, nth-child will still return all p elements that are the nth-child of their container, so if your containing div #article, then had subsequent elements containing p's (such as generic divs, blockquote elements, etc), nth-child would still return all of those. You would have to assume that your containing element would be scoped only to it's immediate children #article > p:nth-child.
  • Commented on 12-03-2010 at 8:53 AM
    Another example, I can link directly to a id on your own site:

    http://www.coldfusionjedi.com/index.cfm/2010/12/3/...
  • Commented on 12-03-2010 at 8:55 AM
    @Todd: Very cool - I didn't know that. Has it always worked or only more recently?

    @David: My thinking was that in the CMS, your output would be something like this (this is ColdFusion,but would apply anywhere): <cfoutput><div id="article">#fromdb#</div></cfoutput> So in other words, only the text of the article is in the main div. No other layout, etc.

    But yeah - I get your point. Now I'm just being difficult. :)
  • Commented on 12-03-2010 at 8:59 AM
    @Ray: Honestly, I don't know. I do know that it's not a new addition, but I don't know when about this came to be a feature. I do know that majority of all the modern browsers support it. I stopped using <a name=""> ages ago.
  • Commented on 12-03-2010 at 9:22 AM
    @Ray

    Using the hash to link to an id has worked for years and years. Neither XHTML or HTML5 have the name attribute on the anchor element. Almost all modern websites use Id, even your blog does for the direct comment linking. ;)

    HTML4 specified that id should work this way (in 1999):
    http://www.w3.org/TR/html4/struct/links.html#h-12....
  • Commented on 12-03-2010 at 9:33 AM
    Thanks @Todd, @Elliott. Every time I build a simple FAQ I end up using <a name> tags. No more!
  • Commented on 12-03-2010 at 12:19 PM
    I threw together a script that highlights paragraphs or sentences and jumps to a specified paragraph. Parts of it are ugly, but it works. It's at https://gist.github.com/727261
  • John Clegg #
    Commented on 12-03-2010 at 12:44 PM
    Nice work! How about the following to highlight sentences?

    // add span tags around all sentances in all paragraphs ( . ! <end of line> are the delimiters )
       $("p").each(function() {
       var s = $(this).html();
       s = s.replace(/(.*?)(\.|!|$)/gm,"<span>$1$2</span>") ;
          $(this).html(s);
       });
       // highlight para2, sentence3
       $("span:nth-child(3)",$("p").eq(2-1)).addClass("highlight") ;
  • Commented on 12-03-2010 at 12:45 PM
    This is very cool. I went a couple steps further and made it scroll to individual sentences and highlight them like NYT. https://github.com/drewwells/sentence-highlighting...

    I used kir's idea for finding out the position of a sentence in a paragraph: https://github.com/kir/jscursorposition
  • Commented on 12-03-2010 at 12:46 PM
    Hope you don't mind, I'll probably blog about this (not that mine gets any traffic). I found a really cool feature of regexp.replace via the Mozilla docs https://developer.mozilla.org/en/JavaScript/Refere...Objects/String/replace#Specifyingafunctionasaparameter
  • Commented on 12-03-2010 at 12:50 PM
    Well shoot, now I don' thave to write the sentence part. ;)
  • Commented on 12-05-2010 at 6:59 AM
    I tried the NYT one and it didn't work. Yours did. So nice work. A good simple idea.
  • Commented on 12-05-2010 at 10:54 AM
    This is really cool. How would you go about getting the value of "document.location.hash" in ColdFusion (if you wanted to do something similar on the server side using CFML before returning the .cfm page)? I can't seem to find this value anywhere (URL, CGI, etc.).
  • Commented on 12-05-2010 at 12:04 PM
    Unfortunately the hash value is not available. It is only available client side. You could - of course - do a 2 step process. Step one is request foo.cfm#moo and return HTML that includes JS to get the hash. You then do an Ajax request to pass the hash to CF. I can't imagine what you would use it for server side though.
  • Commented on 12-05-2010 at 1:11 PM
    Thanks for the reply! That's odd to me. I wonder why we don't have access to it. One case where you might want to know this during the request on the CF server side is if you wanted to perform the same kind of functionality for browsers that can't support javascript and where you just return html styled with the highlights in place.

    In other cases, you might have long list of items that have named anchors and you might want to know as you are rendering the .cfm page during the request if the incoming link contained an anchor so you could highlight the section the users is trying to see (and not just have the browser scroll to it-without javascript).

    Anyway, nice work on this!
  • Commented on 12-29-2010 at 6:41 PM
    I wonder if Adobe could come up with a way to enforce access="public" or access="package" for AJAX requests.
    I know that AJAX is stateless, but still...

    Maybe they could come up with a standard that says "if access=package, then there's an extra parameter that has to match a something found in the same folder", like a checksum of the calling routine or something.
  • Commented on 12-29-2010 at 6:50 PM
    @PSenn: I don't understand your request. What do you mean by 'enforce access=public'? You want access=remote for your remote (ajax/flash remoting) methods.
  • Commented on 12-30-2010 at 10:29 AM
    That's just it - I want access=package for my ajax methods.
    For security purposes of course.
    I suppose I can roll my own and require one of the parameters to contain a password of sorts.
    Something that only the calling function knows.
  • Commented on 12-30-2010 at 10:35 AM
    I must not get what you mean. If you call method FOO on a CFC via Ajax, then FOO an call a package-access level method in another CFC in the same folder. The initial method must be REMOTE. That's a good thing. But after that normal access rules apply.

Post Reply

Please refrain from posting large blocks of code as a comment. Use Pastebin or Gists instead. Text wrapped in asterisks (*) will be bold and text wrapped in underscores (_) will be italicized.

Leave this field empty