Implementing Sequential Navigation

Our docbase records now provide one answer to “Where can I go from here?”—you can go to any of the tabbed indexes. But they don’t say anything about “Where am I?” beyond the plain facts evident in the records themselves. There’s no notion of sequence. It’s true that you can get to the next or previous record in the company index by way of its tabbed-index page. That is, you can go to the company tabbed-index page for the current record, then select its predecessor or successor. But that’s far more cumbersome than the sequential controls we saw in Figure 7.1. There, another answer to “Where am I?” is “In the company (or analyst, or product...) index.” And another answer to “Where can I go from here?” is “To the next (or previous) company (or analyst, or product...).”

What’s needed, then, is a way to map from a record number to its predecessor or successor, relative to any of the indexes. The first step was to save the sequences of record numbers that were only implicit in the structures we built for the tabbed-index controls. We accomplished that in _enumerateRecords( ), which, as we saw in Example 7.8, walks the tab structures and builds a new structure with ordered lists of record numbers for each index.

These ordered lists are necessary, but not sufficient, for the sequential controls. For each record, we need the record number of its predecessor (if any) and its successor (if any), relative to each index. While we’re at it, let’s capture the record’s position in each index and the count of records in each index; we’ll need these items to display the counters shown in Figure 7.1.

Conceptually, we need a structure built on the pattern INDEX,POSITION,MAX, PREV,NEXT. For record 13, the report on Calendar Server, it might look as shown in Example 7.11.

Example 7-11. Sequence Information for a Docbase Record

company,10,14,000014,000005
product,2,14,000009,000003
analyst,2,14,000014,000002
duedate,14,14,000014,

This structure says, for example, that record 13 is 10th in the company index, where it’s preceded by record 14 and followed by record 5. In the duedate index, it’s also preceded by record 14 but has no successor—it’s last in that index.

Suppose each record has a small companion file that stores just this information. Then, links in a dynamic navigation system can use a simple CGI component to look up their destination records in this structure. That’s just how Docbase::Navigate works, as we’ll see shortly. A static navigation system that relies only on precomputed web pages is also possible, though more complicated. We’ll see how that’s done later. First, let’s capture the per-record sequence structures. The buildSequenceStructures( ) routine, shown in Example 7.12, does the job.

Example 7-12. Capturing Per-Record Sequence Info

sub buildSequenceStructures
  {
  my $self = shift;
  my $app = $self->{app};
  my @indexed_fields = @{$self->{indexed_fields}};
  foreach my $index (@indexed_fields)
    {
    my @idx = @{$self->{idxHoL}->{$index}};           # grab index saved earlier
    my $max = $#idx;                              
    my $lim = $max + 1;
    foreach my $i ( 0 .. $max )                       # for each record
      {
      my $prev = ($i == 0)    ? ''   : $idx[$i-1];    # look up predecessor
      my $next = ($i == $max) ? ''   : $idx[$i+1];    # and successor
      $self->{seqHoH}->{$index}->{$idx[$i]} =         # save sequence tuple
        $i+1 . ",$lim,$prev,$next";
      }
    }

  foreach my $doc (@{$self->{idxHoL}->{$indexed_fields[0]}})
    {
    my $seqinfo = "$self->{docbase_cgi_absolute}/$app/seqinfo/$doc";
    open (F, ">$seqinfo") or die "cannot create seqinfo $seqinfo $!";
    foreach my $index (@indexed_fields)
      {
      print F                                         # write sequence tuple
        "$index,$self->{seqHoH}->{$index}->{$doc}
";
      }
    close F;
    }

  }

This routine iterates, once per index, over the ordered lists of record numbers we saved during the _enumerateRecords( ) step. It builds another hashtable, stored in the instance variable $self->{seqHoH}, that holds per-index POS,MAX,PREV, NEXT tuples, like this:

{
'company' => 
  {
  '000013 => '10,14,000014,000015',
  '000012 => '....',
  }
'product' =>
  {...},
}

This structure gives us random access to the per-index tuples for each record number. Now we can iterate over the record numbers, grab the per-index tuples for each, and write sequence information into each record’s companion file.

Dynamically Generating the Sequential Controls

To complete the dynamically generated solution, let’s double back and see how doc-view.pl, doc-nav.pl, and Docbase::Navigate exploit the sequential index information we’ve just created. Example 7.13 shows the complete template for the controls. Each docbase has an instance of this template named dynamic-navigation-template.htm.

Example 7-13. HTML/JavaScript Template for Navigational Controls

<script language=javascript>
function setIndex(index)
  {
  url = "DOCBASE_CGI/doc-view.pl?app=THISAPP&doc=THISDOC&index=" + index;
  location = url;
  }

function gotoTabbedIndex(index)
  {
  if (index != 'choose')
    {
    url = "DOCBASE_WEB/DOCBASE_APP/idxs/" + index + ".htm";
    location = url;
    }
  }
</script>

<center>
<table width=500>
<tr>
<td colspan=3 align=center><b>sequential navigation</t></td>
<td align=center><b>counters</b></td>
<td align=left><b>tabbed-index navigation</b></td>
</tr>
<tr>
<td valign=top align=right width=32>
PREVDOC
</td>
<td valign=top align=center>
<form name=Sequence>
<select name=seq onChange="setIndex(seq.options[seq.selectedIndex].text);">
<OPTION>company
<OPTION>product
<OPTION>duedate
<OPTION>analyst
</select>
</form>
</td>
<td valign=top align=left width=32>
NEXTDOC
</td>
<td align=center valign=top><b>THISPOS</b> of <b>MAXPOS</b></td>
<td align=left valign=top>
<form name=Index>
<select name=idx onChange="gotoTabbedIndex(idx.options[idx.selectedIndex].value);">
<option value=choose>choose tabbed index</option>
<OPTION VALUE=company>company tabs</option>
<OPTION VALUE=product>product tabs</option>
<OPTION VALUE=duedate>duedate tabs</option>
<OPTION VALUE=analyst>analyst tabs</option>
</select>
</form>
</td>
</tr>
</table>
</center>

Names in all caps are the ones that the template processor (see the fillNavigationTemplate( ) method in Example 7.16) looks for. Here’s a rundown of what gets replaced, why, and how.

THISAPP and THISDOC

These are markers for the current Docbase application and the current record. Replacements might be “ProductAnalysis” and “000127.” These are CGI parameters used in a call to doc-view.pl, the same script that we saw _enumerateRecords( ) encode in the addresses of the links it generates.

Here, doc-view.pl appears in the JavaScript function setIndex( ), which is wired to the sequential-index selector. When you switch from the company index to the product index, the dropdown list’s onChange handler fires. It redirects the browser to a page produced by doc-view.pl. That page displays the same record as the current page but in the new navigational context determined by the index name passed to setIndex( ).

As shown in Example 7.14, doc-view.pl uses two services provided by the Docbase::Navigate module. The getSeqInfo( ) method looks up the sequence information for the current record and index. The fillNavigationTemplate( ) method interpolates that information into the template shown in Example 7.13 to create a cluster of navigational controls. It then interpolates the controls into the docbase record, to generate an HTML page that combines the raw record with the controls.

Example 7-14. Sequential Navigation: the doc-view.pl Script

#!/usr/bin/perl -w

use strict;
use TinyCGI;
my $tc = TinyCGI->new;
print $tc->printHeader;
my $vars = $tc->readParse();                   # acquire CGI vars

my $app = $vars->{app};                        # which docbase
my $doc = $vars->{doc};                        # which record
my $idx = $vars->{index};                      # which index

use Docbase::Docbase;                          # initialize docbase
my $db = Docbase::Docbase->new($app);
my $docbase_cgi_relative = $db->{docbase_cgi_relative};

use Docbase::Navigate;                         
my $dn = Docbase::Navigate->new($db);          # initialize navigation module

my ($pos,$maxpos,$prev,$next) =                # get sequence info for record
  $dn->getSeqInfo($doc,$idx);


$dn->fillNavigationTemplate($doc,$idx,$pos,$maxpos,$prev,$next);

NEXTDOC and PREVDOC

These markers are replaced by links that encode CGI calls to doc-view’s companion script, doc-nav.pl. For record 13, NEXTDOC might turn into:

/cgi-bin/Docbase/doc-nav.
pl?app=ProductAnalysis&direction=next&doc=13&index=company

The doc-nav.pl script, shown in Example 7.15, uses getSeqInfo( ) twice. First, like doc-view.pl, it looks up the predecessor and successor of the current record. Then, if the next record was requested, it calls getSeqInfo( ) again, passing the successor’s record number and retrieving the successor’s navigational context. That context enables fillNavigationTemplate( ) to build a successor page that combines the next record with its correct navigational controls.

Example 7-15. Sequential Navigation: the doc-nav.pl Script

#!/usr/bin/perl -w

use strict;
use TinyCGI;
my $tc = TinyCGI->new;
print $tc->printHeader;
my $vars = $tc->readParse();                   

my $app = $vars->{app};
my $doc = $vars->{doc};
my $idx = $vars->{index};
my $dir = $vars->{direction};

use Docbase::Docbase;
my $db = Docbase::Docbase->new($app);

use Docbase::Navigate;
my $dn = Docbase::Navigate->new($db);

my ($pos,$maxpos,$prev,$next) = $dn->getSeqInfo($doc,$idx);

if ($dir eq 'next')
  { $doc = $next; }
else
  { $doc = $prev; }

($pos,$maxpos,$prev,$next) = $dn->getSeqInfo($doc,$idx); 

$dn->fillNavigationTemplate($doc,$idx,$pos,$maxpos,$prev,$next);

Note that doc-view.pl and doc-nav.pl work in a complementary manner. The former, wired to the onChange event of the sequential-index selector, changes the index while holding the record number constant. The latter, wired to the next and prev arrows, changes the record number while holding the index constant.

<OPTION>indexname

These aren’t special markers, they’re the normal HTML <OPTION> tags that comprise the sequential-index picklist. But in this context we choose to interpret them as template markers too. Why? We want index selection to be sticky. When you move from a record in the product index to its successor, you want the new page’s selector to default to the product index, not the first option in the list. That stickiness is another way of preserving context. It’s also what tells the user which sequential index is current. As shown in the Docbase::Navigate module (Example 7.16), we can achieve this effect with a simple transformation:

if ($self->{tabstyle} eq 'multi')
  { s/<OPTION VALUE=(w+)/<option value=$1-$tabs->{$1}/; }

<OPTION VALUE=indexname>

Here’s the part of the template that governs the tabbed-index selector. We saw, at the end of the last section, how that works. Note that the template processor is relying on two different patterns—<OPTION>indexname and <OPTION VALUE= indexname>—to distinguish between the sequential-index selector and the tabbed-index selector. What if we needed to match a third selector too? Have we exhausted the patterns available without inventing new, non-HTML syntax? Nope. There’s still <Option>indexname or <Option VALUE=indexname> or <OPTION>indexname. The template and its processor are a matched pair of components. When you control both, you can widen the interface between them at will.

The Docbase::Navigate Module

The last piece of the dynamically generated solution is Docbase::Navigate, the module shared by doc-view.pl and doc-nav.pl. We’ve already seen how its methods—getSeqInfo( ), makeContextTabs( ), and fillNavigationTemplate( )—are used. Example 7.16, which presents the complete module, shows these methods.

Example 7-16. Docbase::Navigate

use strict;
package Docbase::Navigate;

use TinyCGI;
my $tc = TinyCGI->new();

sub new
  {
  my ($pkg,$db) = @_;
  my $self = 
    {
    'db'        => $db,
    'app'       => $db->{app},
    'tabstyle'  => $db->getDefaultValue('tabstyle'),
    };
  bless $self,$pkg;
  return $self;
  }

sub getSeqInfo
  {
  my ($self,$doc,$index) = @_;
  my $app = $self->{app};
  my $db = $self->{db};
  my $seqinfo = "$db->{docbase_cgi_absolute}/$app/seqinfo/$doc";
  open(F,"$seqinfo") or print "cannot open seq $seqinfo";
  while (<F>)
    {
    chomp;
    my ($thisindex,$pos,$maxpos,$prev,$next) = split(/,/);
    if ($thisindex eq $index)
      {
      return ($pos,$maxpos,$prev,$next);
      }
    }
  close F;
  }

sub makeContextTabs
  {
  my ($self,$doc) = @_;
  my $db = $self->{db};
  my $metadata = 
    $self->{db}->getMetadata("$db->{docbase_web_absolute}/$db->{app}/docs/
         $doc.htm");

  my $tab_fns = $db->getDefaultValue('tab_functions'),

  my $tabs = {};

  foreach my $key (keys %$metadata)
    {
    my $idx_fn = $tab_fns->{$key};
    $tabs->{$key} = $tc->escape(&{$idx_fn}($metadata->{$key}));
    }

  return $tabs;
  }


sub fillNavigationTemplate
  {
  my ($self,$doc,$index,$pos,$maxpos,$prev,$next) = @_;
  my $app = $self->{app};
  my $db = $self->{db};

  my $tabs = $self->makeContextTabs($doc);

  my ($prevlink,$nextlink);

  if ($prev eq '')
    { $prevlink = ''; }
  else
    { 
    $prevlink = "<a href="$db->{docbase_cgi_relative}/doc-nav.pl?
         app=$app&doc=$doc&direction=prev&index=$index">
         <img src="$db->{docbase_web_relative}/img/prev.gif" 
         border=0 alt="Prev record by $index"></a>"; 
    }

  if ($next eq '')
    { $nextlink = ''; }
  else
    { 
    $nextlink = "<a href="$db->{docbase_cgi_relative}/doc-nav.pl?
         app=$app&doc=$doc&direction=next&index=$index">
         <img src="$db->{docbase_web_relative}/img/next.gif" 
         border=0 alt="Next record by $index"></a>"; 
    }

  open (DOC, "$db->{docbase_web_absolute}/$app/docs/$doc.htm") or 
    print "cannot open doc $db->{docbase_web_absolute}/$app/docs/$doc.htm";

  while (<DOC>)                       # emit docbase record
    {
    if ( m#<!-- navcontrols --># )    # emit nav controls
      {
      open (NAV, "$db->{docbase_cgi_absolute}/$app/dynamic-navigation-
           template.htm") or 
        print "cannot open nav template";
      while (<NAV>)                   # interpolate into nav template
        {
        my $thispos = $pos;           # sequential controls
        s/DOCBASE_CGI/$db->{docbase_cgi_relative}/;
        s/DOCBASE_WEB/$db->{docbase_web_relative}/;
        s/DOCBASE_APP/$self->{app}/;
        s/THISPOS/<b>$thispos</b>/;
        s/MAXPOS/<b>$maxpos</b>/;
        s/THISAPP/$app/;
        s/THISDOC/$doc/;
        s/NEXTDOC/$nextlink/;
        s/PREVDOC/$prevlink/;
        s/<OPTION>$index/<option selected>$index/;

                                      # tabbed-index controls
        if ($self->{tabstyle} eq 'multi')
          { s/<OPTION VALUE=(w+)/<option value=$1-$tabs->{$1}/;  }
        else
          { s/<OPTION VALUE=(w+)/<option value=$1/; }

        print $_;
        }
      close NAV;
      }
    else                              # emit normal line of docbase record
      {  print $_;  }
    }
  close DOC;
  }

1;
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.108.241