Using jQuery to mimic the NYT's new paragraph linking

This post is more than 2 years old.

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.

<script type="text/javascript" src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script> <script> $(document).ready(function() {

//get anchor
console.log('loaded');
if(document.location.hash != '') {
	var hash = document.location.hash.substr(1,document.location.hash.length);
	var type = hash.substr(0,1).toLowerCase();
	
	//ok, so if hash is PN, go to paragraph N
	if(type == "p" || type == "h" ) {
		var pindex = hash.substr(1, hash.length);
		pindex = parseInt(pindex);
		if(isNaN(pindex)) return;
		console.log('going to load paragraph '+pindex);
		//now find it
		var para = $("p:nth-child("+pindex+")");
		var top = para.position().top;
		console.log(top);
		window.scrollTo(0, top);
		if(type == "h") para.addClass("highlight");
		console.log('done');
	}

}

}) </script> <style> .highlight { background-color: yellow; font-weight: bold; color: red; } </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.

Raymond Camden's Picture

About Raymond Camden

Raymond is a senior developer evangelist for Adobe. He focuses on document services, JavaScript, and enterprise cat demos. If you like this article, please consider visiting my Amazon Wishlist or donating via PayPal to show your support. You can even buy me a coffee!

Lafayette, LA https://www.raymondcamden.com

Archived Comments

Comment 1 by Todd Rafferty posted on 12/3/2010 at 6:08 PM

Using FF3.6 / WinXP at work, it's jumping to the appropriate paragraph, but that's it. No highlights, etc.

Comment 2 by Raymond Camden posted on 12/3/2010 at 6:11 PM

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.

Comment 3 by Todd Rafferty posted on 12/3/2010 at 6:17 PM

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. :|

Comment 4 by Todd Rafferty posted on 12/3/2010 at 6:23 PM

The highlighting is working now, whatever you fixed. :)

Comment 5 by Todd Rafferty posted on 12/3/2010 at 6:24 PM

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.

Comment 6 by Zephirnl posted on 12/3/2010 at 6:39 PM

For me same, nothing happens in FF unless I activate Firebug. Chrome works flawlessly (winXP).

Comment 7 by Rodney Rehm posted on 12/3/2010 at 6:56 PM

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

Comment 8 by xibris posted on 12/3/2010 at 6:58 PM

does not work in firefox

Comment 9 by Ahmed Almonajed posted on 12/3/2010 at 6:59 PM

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 :)

Comment 10 by David Young posted on 12/3/2010 at 7:12 PM

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.

Comment 11 by Sid Wing posted on 12/3/2010 at 7:41 PM

Hi Ray -

Yes - I concur with Ahmed - I get the "console undefined" issue - running IE8 on XP

Comment 12 by Raymond Camden posted on 12/3/2010 at 7:42 PM

Guys - sorry - I thought all web developers used a 'good' browser with console support. ;) I've removed the console messages.

Comment 13 by Raymond Camden posted on 12/3/2010 at 7:45 PM

@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. ;)

Comment 14 by Todd Rafferty posted on 12/3/2010 at 7:51 PM

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/#f... ). 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.

Comment 15 by David Young posted on 12/3/2010 at 7:52 PM

@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.

Comment 16 by Todd Rafferty posted on 12/3/2010 at 7:53 PM

Another example, I can link directly to a id on your own site:

http://www.coldfusionjedi.c...

Comment 17 by Raymond Camden posted on 12/3/2010 at 7:55 PM

@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. :)

Comment 18 by Todd Rafferty posted on 12/3/2010 at 7:59 PM

@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.

Comment 19 by Elliott Sprehn posted on 12/3/2010 at 8:22 PM

@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/...

Comment 20 by Raymond Camden posted on 12/3/2010 at 8:33 PM

Thanks @Todd, @Elliott. Every time I build a simple FAQ I end up using <a name> tags. No more!

Comment 21 by Joe Lillibridge posted on 12/3/2010 at 11: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

Comment 22 by John Clegg posted on 12/3/2010 at 11: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") ;

Comment 23 by Drew Wells posted on 12/3/2010 at 11: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/drewwell...

I used kir's idea for finding out the position of a sentence in a paragraph: https://github.com/kir/js_c...

Comment 24 by Drew Wells posted on 12/3/2010 at 11: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.o...

Comment 25 by Raymond Camden posted on 12/3/2010 at 11:50 PM

Well shoot, now I don' thave to write the sentence part. ;)

Comment 26 by Ian Parr posted on 12/5/2010 at 5:59 PM

I tried the NYT one and it didn't work. Yours did. So nice work. A good simple idea.

Comment 27 by Bryan Rice posted on 12/5/2010 at 9:54 PM

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.).

Comment 28 by Raymond Camden posted on 12/5/2010 at 11: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.

Comment 29 by Bryan Rice posted on 12/6/2010 at 12:11 AM

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!

Comment 30 by Phillip Senn posted on 12/30/2010 at 5:41 AM

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_.

Comment 31 by Raymond Camden posted on 12/30/2010 at 5:50 AM

@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.

Comment 32 by Phillip Senn posted on 12/30/2010 at 9:29 PM

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.

Comment 33 by Raymond Camden posted on 12/30/2010 at 9:35 PM

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.