A Reusable, Fixed-Width Page Index Template

May 07, 2011

As soon as I announced that I had restored content from my old blog, a response followed about a bug. The page index at the bottom of the page overflowed, as previous applications did not have to deal with more pages than would fit on the breadth of the page. A quick fix was to set the overflow property of the content to hide so that the page index or any other content for that matter would no longer stick out of the content box. But that was not a real fix of course.

High time to generalize the pageIndex definition in the library.

Old Definition

The old pageIndex template rendered a list of links to all pages in a series. Thus with a total of 17 pages and the current page at 4, it would display the following list, where each element is a link, except for the current page:

Previous 1 2 3 4 5 6 7 8 9 [10] 11 12 13 14 15 16 17 Next

In WebDSL this expressed with the following template, which is parameterized with the index of the current page, a count of the total number of pages, and the number of items perpage. The template is parameterized with a call to pageIndexLink(i,s), which should produce a link (navigate) to the i-th page with anchor string s.

define pageIndex(index: Int, count: Int, perpage: Int) {
  var pages : Int := 1 + (count - 1)/perpage
  var idx := min(max(1,index), pages)
  if(pages > 1) { 
    pageIndexLink(idx-1, "Previous") 

    for(i : Int from 1 to pages+1) {  
      if(i == idx) {
        "[" output(i) "]"
      } else {
        pageIndexLink(i, i + "")
      }
    }

    pageIndexLink(idx+1,"Next")
  }
}

Note that the index is normalized to be within the range 1 to pages. We’ll see below that the template can be enhanced by including CSS classes for the different cases to associate appropriate style with the list elements. There is no point in displaying the page index if there is only one page.

New Definition

If we have room for a page index with at most 10 pages, the index above will overflow. Thus, we can only show a selection of the pages. Typically, an index shows the first and last page(s), and some pages around the current page. For example, in the situation above we could show.

Previous 1 ... 7 8 9 [10] 11 12 ... 17 Next

In general, we want to divide the index into several intervals of pages that we want to include. The parameters for the index are the maximum number of entries in the index (max) and the number of pages at the start and end of the index (end). The following definition of pageIndex iterates over a list of intervals based on these parameters:

define pageIndex(index : Int, count : Int, perpage : Int, max: Int, end: Int) {
  var pages : Int := 1 + (count - 1)/perpage
  var idx := min(max(1,index), pages)
  var intervals : List<List<Int>> := idxIntervals(idx, count, perpage, max, end)
  if(pages > 1) { 
    pageIndexLink(idx-1, "Previous")

    for(iv : List<Int> in intervals) {
      for(i : Int from iv.get(0) to iv.get(1) + 1) { 
        if(i == idx) {
          "[" output(i) "]"
        } else { 
          pageIndexLink(i, i + "")
        }
      }
    } separated-by { "..." }

    pageIndexLink(idx+1,"Next") 
  }
}

An interval is represented by a list of (two) integers. Thus, the list of intervals is represented by a list of lists of integers. The body of the template is an iteration over intervals with a nested iteration over each interval displaying links to the corresponding pages. The intervals are separated by an ellipsis "...".

Intervals

Remains to compute the intervals. The typical case is where idx is somewhere in the middle, e.g.

Previous 1 ... 7 8 9 [10] 11 12 ... 17 Next

Thus, we take some pages at the start, a bunch of pages around idx, and some pages at the end. Given that middle is (max - (2 * (end + 1)))/2, i.e. half the number of slots that remain after accounting for the slots at the beginning and end, including the slots taken by the ellipses, this generalizes to the following intervals:

[[1, end], [idx - middle, idx + middle - 1], [pages - end + 1, pages]]

But if idx is close to one of the edges, it doesn’t make sense to include the left or right gap. For example,

Previous 1 2 3 [4] 5 6 7 8 ... 17 Next

For the left edge the index is too close to the edge if idx is smaller than end + 2 + middle. In that case, we only get the intervals:

[[1, end + 1 + 2 * middle], [pages - end + 1, pages]]

On the right edge we have the same situation

Previous 1 ... 10 11 12 [13] 14 15 16 17 Next

with the pattern

[[1,end], [pages - end - 2 * middle, pages]]

Putting this together, the function idxIntervals computes the appropriate interval:

function idxIntervals(idx: Int, count: Int, perpage: Int, max: Int, end: Int)
       : List<List<Int>> 
{
  var pages : Int := 1 + (count - 1)/perpage;
  var middle := (max - (2 * (end + 1)))/2;  
  var intervals : List<List<Int>>;
  if(pages <= max) {
    intervals := [[1,pages]];
  } else { if(idx <= end + 2 + middle) {
    intervals := [[1,end + 1 + 2 * middle],[pages - end + 1, pages]];
  } else { if(idx >= pages - end - middle) {
    intervals := [[1,end],[pages - end - 2 * middle, pages]];
  } else {
    intervals := [[1,end],[idx - middle, idx + middle - 1],[pages - end + 1, pages]];
  }}}
  return intervals;
}

With Class

The full definition of pageIndex is a bit more involved, since it applies CSS classes to the entries in the list and avoids making Previous and Next active links when there are previous or next pages.

define pageIndex(index : Int, count : Int, perpage : Int, max: Int, end: Int) {
  var pages : Int := 1 + (count - 1)/perpage
  var idx := min(max(1,index), pages)
  var intervals : List<List<Int>> := pageIndexIntervals(idx, count, perpage, max, end)
  if(pages > 1) { 
    container[class="pageIndex"] {
      if(idx > 1) { 
        container[class="indexEntryActive"]{ pageIndexLink(idx-1, "Previous") }
      } else { 
        container[class="indexEntryInactive"]{ "Previous" }
      }

      for(iv : List<Int> in intervals) {
        for(i : Int from iv.get(0) to iv.get(1) + 1) { 
          if(i == idx) {
            container[class="indexEntryCurrent"]{ output(i) }
          } else { 
            container[class="indexEntryActive"]{ pageIndexLink(i, i + "") }
          }
        }
      } separated-by {
        container[class="indexEntryGap"]{ "..." }
      }

      if(idx < pages) { 
        container[class="indexEntryActive"]{ pageIndexLink(idx+1,"Next") }
      } else { 
        container[class="indexEntryInactive"]{ "Next" }
      }
    }
  }
}