Paginator

Paginator is a script for hiding anything that isn't in the current section that has been navigated to within a webpage, effectively making isolated subpages within a webpage. When you use a link to the get and use section on this page, for example, only that section will be shown.

Get and use

1.

Get jQuery and Paginator:

jquery.min.js
jquery.paginator.auto.min.js

  • Both of these files are minified
  • Put them where you can link them
  • You can rename them if you like
2.

Add them to a document with sections:

<title>Paginator example</title>
<script src="jquery.js"></script>
<script src="jquery.paginator.js"></script>

<p>Show only the <a href="#section">next section</a>.
<p id="section">This is a section.

How it works

Figure 01

The script works by moving the entire <body> contents into a content cache when a #page is requested. Each #page requested thereafter is then selectively cloned from the content cache and placed into the <body>. When the user requests the entire body again, the content cache is moved back out of the cache to be rendered again.

Note These diagrams use svg-in-img.

Figure 02a

There are three different paths of interaction that the script has to handle: going from body to a #page, from one #page to another, and from a #page back to the body. Consult the details section for further explanation.

Animate this:

Details

These annotations of the most important components of the code are in order of best explanation, which is not necessarily the same order as in the source code.(a) The language used here is CoffeeScript, for increased readability, though the master source uses JavaScript.

The source is hosted on github at @sbp/paginator.

Three hashchange possibilities

  $(window).hashchange ->
    source = (if content then "page" else "body")
    dest = (if location.hash then "page" else "body")

    switch [source, dest].toString()
      when ["body", "page"].toString()
        cache()
        add_page()

      when ["page", "page"].toString()
        delete_page()
        add_page()

      when ["page", "body"].toString()
        delete_page()
        uncache()

The source and dest variables store the type of locations before and after the hashchange polyfill event. The content cache is only created when a hash page (called "page") is navigated to, and is destroyed again when navigating back to the whole body (called "body"), so it can be used to detect the source. The location.hash obviously only exists when there's a hash page being navigated to.

You have to use toString on the lists in the switch/case statement because lists in JavaScript don't compare equal with the same members. The stringified version of lists are comma separated with quoted strings, so they're unambiguous, though that isn't a factor here.

Since the hash doesn't change when the body is reloaded, we won't get a hashchange event for it. Even if we could intercept that, we wouldn't want to do anything anyway since it doesn't involve any hash pages.

When a page is loaded with a hash straight away, the browser will actually invoke the body to page path. This means that we don't have to deal with a separate condition for loading straight into path, which would require initialising the content cache separately.

Cache and display

  cache = ->
    children = body.children().detach()
    content = $("<div>").append(children)

  uncache = ->
    body.append content.children()
    content = false

  add_page = ->
    page = $("[data-id=" + location.hash + "]", content)
    return uncache() unless page.size()
    body.append page.clone(true)
    $(window).scrollTop 0

  delete_page = ->
    body.empty()

These functions adapt the logic that we use into terms that work with the jQuery API. All references to content in these functions are to a variable scoped outside of the functions.

The uncache function appends the children from the content cache, but it isn't clear whether this is a deep or shallow clone of them, or counted as some kind of copy operation with different semantics. Probably it's using the same pointer, and then we're just overwriting it in content. That would make it an exact reference.

The add_page function here uses the data-id attribute to select pages. We change all id attributes to data-id elsewhere in the code, to prevent the browser from skipping down when we put them in the document again. What happens is that the browser stores the vertical offset of elements with id attributes when the body is first loaded, so that even when you remove the id attributes, the browser will scroll down to where it thinks the id you're accessing used to be. That could be half way down the hash page.

The true boolean argument to clone in add_page means to copy element data and events. A second true would make it a deep copy. The line about calling to uncache unless there is a page size means that if we don't actually find the page, or the page is empty, then we just abort what we were doing and act as though we were just showing the body.

The clone and unclone functions are inverses of one another. Similarly, add_page and delete_page are complementary.

The delete_page function just clears the currently displayed page out of the body. This function shows why it's important to name things logically in context, not just limited to what they do technically. This clears the body, but the context is that the content of the body will always be a particular page. So calling this delete page makes it clear that the body.empty call is actually removing a single page from the body.

Setup

  $("[id]").each ->
    e = $(this)
    e.attr("data-id", "#" + e.attr("id")).removeAttr "id"

  $(window).hashchange()

This is the code that we use to break the IDs so that the browser doesn't skip down to them upon loading. This could easily be taken out, and the add_page function changed, to make the script work nicely with other scripts that depend on IDs. It doesn't appear that the problem can be worked around without removing the IDs, or if it can be then I suspect it would require very complicated code and would probably be very fragile between different browsers given the aggressive ID caching that is taking place.

The hashchange call at the bottom to the polyfill is so that when you load a document for the first time, the code can detect whether there's a hash location or not. The initial load of a document will be in the body state, in other words, and we have to detect whether we're moving to the page state. It's a bit odd that calling the event with a function sets a handler, but that's the jQuery style.

Other

Ideas

Credits

Anything using jQuery wouldn't be possible without John Resig. Paginator also uses the excellent jQuery hashchange polyfill, by Ben Alman. The polyfill is included in the packed version of Paginator, under MIT and GPL licenses. The SVG diagrams were made in Inkscape, and the code syntax highlighting was done by hand but with the theme and assistance of ace. The styles were compiled from LESS using lessc.

Footnotes

(a) There are two main reasons for the different orders. One is that abstract functions depend on more particular and detailed functions, and so the latter usually come before the former in programs because the computer reads the file as though it were reading a dictionary before reading a book. When explaining to a person, on the other hand, you don't have to define the particular and detailed parts, because you mainly want to explain the motivations, the effects that you want to achieve, and not the method by which you're achieving it. So the abstract part is the most important for explaining to a person.

The other reason for the different order is that programs often need administrative tasks at the top and bottom of their files, things like setting up global variables and importing modules, which aren't very interesting to a person reading code. So we can put those at the bottom of an explanation of the code, or leave them out altogether.

sbp.so @sbp Email Paypal