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.
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.
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);
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.
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}/; }
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 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;
52.14.84.29