Creating Super Printer-Friendly Pages

Don't you just hate it? You find a good article somewhere on the Internet and you decide to print it so you can read it later. But when you read it, you find out that all the links in the article are missing. All you see is something like "Click here to read this other very interesting document". On the web site, the word here links to some kind of other document, but on paper that is pretty useless. The only option you have left is to revisit the original article, find the link and follow it.

Fortunately, with a little bit of JavaScript and some DOM modification, it's easy to improve your user's experience, even long after they have visited your site and printed parts of the content.

Creating a List of Links for Your Document

This short article shows you how to create a list of links in the document that is only visible when the page gets printed. It extends the ideas from the article "How Do I Hide Screen Elements When a Page Is Printed?" that shows you how to apply different style sheets when a page is printed.

To see what I mean, request a Print Preview of this page (no need to waste trees on this by actually printing the page) and scroll down to the end of the of the last page. You'll see something like this:

A List of Links at the end of the Article in Print Preview
Figure 1 - A List of Links at the end of the Article

If you look at the content of the article in the print preview, you'll see a few reference numbers in parentheses after each piece of text that is a link in the original article. Each number refers to one of the text links at the bottom of the article.

To create this functionality, I wrote a little JavaScript function that finds all links in a certain part of the document, adds the reference number between parentheses behind the link, and then adds the full link text to the list of links at the end of the document. The JavaScript function looks like this:

function CreatePrinterFriendlyLinks()
{
  try
  {
    var elementToGetLinksFrom = document.getElementById("ctl00_MainContent");
    var elementToAddLinksTo = document.getElementById("LinksHere");
    
    var allLinks = elementToGetLinksFrom.getElementsByTagName("a");
    var previousTocElement = null;

    for (var i = allLinks.length - 1; i >= 0; i--)
    {
      var myLink = allLinks[i];
      if (myLink.href != null && myLink.href != "")
      {
        if (myLink.href != myLink.innerHTML)
        {
          // Found a link with a description different from its href
          // First, insert a footnote reference number.
          var newElement = document.createElement("span");
          newElement.innerHTML = " (" + (i + 1) + ")";
          newElement.className = "PrintOnlySpan";
          myLink.parentNode.insertBefore(newElement, myLink.nextSibling);

          // Now add the link to the Links TOC at the end of the article
          var newTocLink = document.createElement("span");
          newTocLink.innerHTML = "(" + (i + 1) + ") " + myLink.href;

          elementToAddLinksTo.insertBefore(newTocLink, previousTocElement);
          newTocLink.parentNode.insertBefore(
              document.createElement("br"), newTocLink.nextSibling);
          previousTocElement = newTocLink;
        }
      }
    }
  }
  catch(ex)
  {}
}		  

The code starts by getting a reference to two elements: one that is used to get all the links from (the main content area in my site), the other is used to add the links to as plain text. Then an array called allLinks is filled with all the <a> tags found in the elementToGetLinksFrom element. If you want all your links in the page to be listed, replace elementToGetLinksFrom.getElementsByTagName("a"); with document.getElementsByTagName("a");

The code then starts looping in reversed order through the collection of links. Since we're modifying the DOM as we go, it's important not to change indexes and order of elements that still need to be processed. By looping in reversed order, new elements are added after the current element, leaving the DOM structure for the remaining elements in good order.

The code then checks if the innerHTML of the link is not the same as its href. This is optional, but it helps in removing obvious links from the list of links. There's no point in listing a link like http://imar.spaanjaars.com/ because it's already clear what the URL of the link is. (The only downside is that the numbered list with links has a gap for each link that is skipped, as you can see in figure 1. To fix this problem, you shouldn't add the links to the list directly in the loop, but store them in a local array. Once all links have been processed, iterate through the array and write out the links and reference numbers.)

Once a valid link is found, the code creates a new <span> element and sets its innerHTML to the current loop index plus 1 (since the loop is zero based) wrapped in parentheses. It also sets the CSS className property to PrintOnlySpan to ensure that the span isn't visible on screen, but only when the page gets printed. Refer to the article on printer friendly pages to see how this works.

The <span> tag is then added after the current link. Since the DOM is missing the insertAfter method, I had to resort to a trick using insertBefore to emulate insertAfter. The trick, and its caveats, is documented in the article "DOM insertAfter()".

The final part of the code inserts the reference number and the full text of the link as a <span> to the element called elementToAddLinksTo. Each <span> is follow by a simple <br /> tag so each link ends up on its own line. Since I am adding the <span> and <br /> tags in a loop, I keep a reference to the element I inserted in the variable previousTocElement and use that on each iteration to insert the new link before the previous link.

The <span> and <br /> tags are added to the element called elementToAddLinksTo. In my site, that element looks like this:

<div id="TocLinks" class="PrintOnly">
  <h2>Links in this Document</h2>
  <div id="LinksHere"></div>
</div>		  

I also wrapped the LinksHere <div> in another <div> that is also visible in print only.

To make this work on screen and on print, I have two different style sheets linked in the head section of my pages:

<link rel="stylesheet" type="text/css" 
      href="Css/Styles.css" />
<link rel="stylesheet" type="text/css" 
      href="Css/PrintStyles.css" media="print" />		  

The second link has a media="print" attribute so it's only applied when printing the page. The files contain the following definition for the PrintOnlyspan class:

[Styles.css]
.PrintOnlySpan
{
	display: none;
}

[PrintStyles.css]
.PrintOnlySpan
{
	display: inline;
}		  

With these style sheets, all elements with a class of PrintOnlySpan are hidden on screen, and visible when the page is printed.

The final thing to do is call this JavaScript function on pages that require it. In most pages, you can simply add it to the onload attribute of the <body> tag:

<body onload="CreatePrinterFriendlyLinks();">		  

However, since I am using Master Pages in my web site, I had to add the following code in the code behind of the pages that require printer friendly links:

this.ClientScript.RegisterStartupScript(this.GetType(), "printerFriendly", 
    "<script type=\"text/javascript\">" + 
    "CreatePrinterFriendlyLinks();</script>");

This adds a JavaScript block at the end of the page to call the CreatePrinterFriendlyLinks function.

Summary

With a little bit of JavaScript and CSS, it's easy to improve the quality of your content in print. The function from this article can easily be reused across multiple pages and sites. All you need to do is include the script somewhere in an external JavaScript file and call CreatePrinterFriendlyLinks. To make the code a bit more reusable, you can change the function so it accepts the client IDs of the element with the links and the element where the text links should be added to.

I tested this code on IE6 and IE7 and the latest version of FireFox. Please let me know if you find issues with this script on other browsers and I'll try to fix them.


Where to Next?

Wonder where to go next? You can read existing comments below or you can post a comment yourself on this article .


Consider making a donation
Please consider making a donation using PayPal. Your donation helps me to pay the bills so I can keep running Imar.Spaanjaars.Com, providing fresh content as often as possible.



Feedback by Other Visitors of Imar.Spaanjaars.Com

On Friday, May 11, 2007 1:22:49 PM Grant said:
Imar,

Cool article. I read somewhere though that MS changed how you registered client JavaScripts in ASP.NET 2.0. Is the method you're using here to do this the current wayto register client side JavaScripts?
On Saturday, May 12, 2007 11:51:01 AM Imar Spaanjaars said:
Hi Grant,

Actually, no it isn't.

    this.RegisterStartupScript

is now considered obsolete. You should use

    this.ClientScript.RegisterStartupScript

instead.

I've updated the article.

Cheers,

Imar

Talk Back! Comment on Imar.Spaanjaars.Com

I am interested in what you have to say about this article. Feel free to post any comments, remarks or questions you may have about this article. The Talk Back feature is not meant for technical questions that are not directly related to this article. So, a post like "Hey, can you tell me how I can upload files to a MySQL database in PHP?" is likely to be removed. Also spam and unrealistic job offers will be deleted immediately.

When you post a comment, you have to provide your name and the comment. Your e-mail address is optional and you only need to provide it if you want me to contact you. It will not be displayed along with your comment. I got sick and tired of the comment spam I was receiving, so I have protected this page with a simple calculation exercise. This means that if you want to leave a comment, you'll need to complete the calculation before you hit the Post Comment button.

If you want to object to a comment made by another visitor, be sure to contact me and I'll look into it ASAP. Don't forget to mention the page link, or the QuickDocId of the document.

For more information about the Talk Back feature, check out this news item.