package HTML::Paginator; use strict; # Preloaded methods go here. sub new { my $pkg = shift or return undef; my $obj = { html=>{ pre_cap => '', end_cap => '', previous_icon => '<< ', next_icon => ' >>', singular => '', plural => 's', item_name => 'result', seperator => ' | ', href_link => $ENV{REQUEST_URI} }}; bless $obj, ref $pkg || $pkg; $obj->_Prep_HTML_Text; if ($obj->{html}->{href_link} =~ /&page\=\d+&/) { $obj->{html}->{href_link} =~ s/&page\=\d+&/&/g; $obj->{html}->{href_link} .= '&page='; } elsif ($obj->{html}->{href_link} =~ /\?page\=\d+&/) { $obj->{html}->{href_link} =~ s/\?page\=\d+&//g; $obj->{html}->{href_link} .= '&page='; } elsif ($obj->{html}->{href_link} =~ /&page\=\d+$/) { $obj->{html}->{href_link} =~ s/&page\=\d+$//g; $obj->{html}->{href_link} .= '&page='; } elsif ($obj->{html}->{href_link} =~ /\?page\=\d+$/) { $obj->{html}->{href_link} =~ s/\?page\=\d+$//g; $obj->{html}->{href_link} .= '?page='; } elsif ($obj->{html}->{href_link} =~ /\?/) { $obj->{html}{href_link} .= '&page='; } else { $obj->{html}{href_link} .= '?page='; } $obj->{num_per_page} = shift and $obj->{num_per_page} > 0 or return undef; # must have at least one item per page $obj->{book} = \@_; $obj->_Ginsoo; return $obj; } sub _Ginsoo { my ($obj, @array); $obj = shift and ref $obj and @array = @{$obj->{book}} or return undef; $obj->{total} = scalar @array; # store total in array my $i = 0; while (my @slice = splice @array, 0, $obj->{num_per_page}) { push @{$obj->{page}}, \@slice; # one slice per page $i++ } $obj->{num_pages} = $i; } sub _Set_HTML_Text { my $obj; $obj = shift and ref $obj or return undef; my $what = shift or return undef; my $new_name = shift; defined $new_name or return undef; $obj->{html}->{$what} = $new_name; return $obj->_Prep_HTML_Text; } sub _Prep_HTML_Text { my $obj; $obj = shift and ref $obj or return undef; $obj->{html}->{previous_text} = "Last %d $obj->{html}->{item_name}%s"; $obj->{html}->{next_text} = "Next %d $obj->{html}->{item_name}%s"; return 1; } sub Name_Item { my $obj; $obj = shift and ref $obj or return undef; return $obj->_Set_HTML_Text('item_name', shift); } sub Set_Plural { my $obj; $obj = shift and ref $obj or return undef; return $obj->_Set_HTML_Text('plural', shift); } sub Set_Singular { my $obj; $obj = shift and ref $obj or return undef; return $obj->_Set_HTML_Text('singular', shift); } sub Contents { my ($obj, $page); $obj = shift and ref $obj and $page = shift and $page > 0 or return undef; $page--; return exists $obj->{page}->[$page]? @{$obj->{page}->[$page]}: (); } sub Next { # next page index is current page number my $obj; $obj = shift and ref $obj or return undef; return $obj->_Item_Count(shift); } sub Previous { # previous page index is two less than page number my $obj; $obj = shift and ref $obj or return undef; return $obj->_Item_Count(shift(@_) - 2); } sub Item_Count { # current page is one less than page number my $obj; $obj = shift and ref $obj or return undef; return $obj->_Item_Count(shift(@_) - 1); } sub _Item_Count { my $obj; $obj = shift and ref $obj or return undef; my $page = shift; $page > -1 or return undef; # no negative page numbers return exists $obj->{page}->[$page]? scalar @{$obj->{page}->[$page]}: 0; } sub Page_Count { my $obj; $obj = shift and ref $obj or return undef; return scalar @{$obj->{page}}; } sub First_Item { my $obj; $obj = shift and ref $obj or return undef; my $page = shift; $page > 0 or return undef; $page--; return undef unless $obj->_Item_Count($page); return ($obj->{num_per_page} * $page) + 1; } sub Last_Item { my $obj; $obj = shift and ref $obj or return undef; my $page = shift; $page > 0 or return undef; $page--; return undef unless $obj->_Item_Count($page); return ($obj->{num_per_page} * $page) + scalar $obj->_Item_Count($page); } sub Page_Header_HTML { my ($obj, $page); $obj = shift and ref $obj and $page = shift and $page > 0 or return "[An error occurred while forming the page header]"; $obj->{html}->{previous_text} = "Last %d $obj->{html}->{item_name}%s"; $obj->{html}->{next_text} = "Next %d $obj->{html}->{item_name}%s"; if ($obj->Page_Count > 1) { # Case more than one page return sprintf <<"eohtml", %s%s %d to %d of %d (Page $page of %d) eohtml ucfirst $obj->{html}->{item_name}, $obj->{html}->{plural}, $obj->First_Item($page), $obj->Last_Item($page), $obj->{num_per_page} * ($obj->Page_Count - 1) + $obj->_Item_Count($obj->Page_Count-1), $obj->Page_Count; } elsif ($obj->_Item_Count($page-1) == 1) { # Case one page one item return sprintf <<"eohtml", The only %s%s eohtml $obj->{html}->{item_name}, $obj->{html}->{singular}; } elsif ($obj->_Item_Count($page-1) == 0) { # Case no items return sprintf <<"eohtml", No %s%s eohtml $obj->{html}->{item_name}, $obj->{html}->{plural}; } else { # Case one page multiple items return sprintf <<"eohtml", All %d %s%s eohtml $obj->_Item_Count($page-1), $obj->{html}->{item_name}, $obj->{html}->{plural}; } } sub Page_Nav_HTML { my ($obj, $page); $obj = shift and ref $obj and $page = shift and $page > 0 or return "[An error occured while forming the page count]"; $obj->{html}->{previous_text} = "Last %d $obj->{html}->{item_name}%s"; $obj->{html}->{next_text} = "Next %d $obj->{html}->{item_name}%s"; my $p_cap = $obj->{html}->{pre_cap}; my $n_cap = $obj->{html}->{end_cap}; my $p_icon = $obj->{html}->{previous_icon}; my $n_icon = $obj->{html}->{next_icon}; my $p_count = $obj->Previous($page); my $n_count = $obj->Next($page); my $p_page = $page - 1; my $n_page = $page + 1; my $p_text = sprintf $obj->{html}->{previous_text}, $p_count, $p_count == 1? $obj->{html}->{singular}: $obj->{html}->{plural}; my $n_text = sprintf $obj->{html}->{next_text}, $n_count, $n_count == 1? $obj->{html}->{singular}: $obj->{html}->{plural}; my $link = $obj->{html}->{href_link}; my $sep = $obj->{html}->{seperator}; my $nav_html = sprintf <<"eohtml", $p_cap%s%s%s$n_cap eohtml $p_count? "$p_icon$p_text": '', $p_count && $n_count? $sep: '', $n_count? "$n_text$n_icon": ''; return $nav_html; } 1; __END__ =head1 NAME HTML::Paginator - Object-Oriented Pagination for Web Applications =head1 SYNOPSIS use CGI; use HTML::Paginator; my $cgi = new CGI; my $page = $cgi->param('page') || 1; my @items = (1..67); my $book = HTML::Paginator->new(25,@items); $book->Name_Item('random item'); @items = $book->Contents($page); print "\n \n Sample Script\n ", "\n"; # it bugs me that people use CGI methods for stuff like that # above. Gaaah. print "\n", $cgi->h2($book->Page_Header_HTML($page)), "\n", $book->Page_Nav_HTML($page), "\n\n"; =head1 DESCRIPTION HTML::Paginator is an Object-Oriented module intended to make pagination of large lists easy. Using an amazing (or amazingly simple) internal method, it takes your favourite array and it slices, it dices, and it makes Julien Fries out of your array. It's an HTML module because that's where it's most useful. However, a small amount of finagling can make it useful for any interface, really. =head1 Instantiation You create a Paginator object, which I'm calling a 'book' for lack of a better term, by calling the new($@) method, like is done with most OO modules. new takes two or more arguments: the first is the number of items you want per page. The second and all following are the items you want sliced up into seperate pages. For instance, you could pull the results of a SQL query in to be sliced up, and display 10 per page: my @stuff; while (my $row = $my_query->fetchrow_hashref) { push @stuff, $row; } my $book = HTML::Paginator->new(10, @stuff); for my $row ($book->Contents($page) { print $row->{column_to_print}; } It's that easy. Of course, if your database is slow, or you have a huge number of results, you don't want to pull down all of them first. I recommend getting a count, using paginator to slice *that* up, and then working a little programmer magic to get back only the slice of the table you want (Oracle would let you use rownum, while with MySQL you might have to work harder, doing a few small queries to whittle things to where you want them). Then again, who says you're using a database? You could even use this to paginate a huge text document in an external file, with a while(<>) and a counter scalar, maybe. Ahh, this is all your job. I did the slicing. As a note, HTML::Paginator acts like it thinks in terms of 1-indexed arrays. It doesn't, really. It just pretends to with its public methods. This is because while we all know that arrays should be zero-indexed, the user doesn't, and seeing page=0 in their URL looks goofy to them. So we're nice to them. They won't thank you because the web is full of ungrateful bastards, but you can feel nicer about yourself for knowing you were nice to a bastard. Or something. =head1 Public Methods Several nice convenience methods are supplied, so you can make the module do the thinking and you can back to drinking... or whatever. Hey, it rhymed. =over 4 =item new($@) (constructor) As stated above, this creates a new Paginator object and slices it up into little pieces (or big pieces, as you prefer). It takes 2 or more arguments: the number of items per page, and the array or list (not an arrayref, BTW. Dereference any arrays you intend to hand to this first (or en passe)). For instance: my $book = HTML::Paginator(25, @array); or perhaps my $book = HTML::Paginator->new(100, @{$object->{arrayref}}); =item Name_Item($) Takes any string and makes that the internal name for whatever you're chopping up a list of. The default is 'result'. For instance, if you have a list of kittens for sale, you would call: $book->Name_Item('kitten'); =item Set_Plural($) Takes any string and makes that the plural-iser for the item name. The default is 's'. For instance, if you'd set the item name to 'child' with Name_Item, you would want to set the plural correctly, so that it didn't consider more than one to be 'childs': $book->Set_Plural('ren'); =item Set_Singular($) Like Set_Plural above, sets the singular. The default is '' (empty string). You may want to set the singular when the word changes form based on plurality. For instance, if you were strange enough to list octopi (that's the plural for 'octopus'), you would want to set the name to the least common denominator, and set the plural and singular forms: $book->Name_Item('octop'); $book->Set_Singular('us'); $book->Set_Plural('i'); While you can, theoretically, set the name to '' and the Singular and Plural forms to be the entire words: $book->Name_Item(''); $book->Set_Singular('mouse'); $book->Set_Plural('mice'); I don't recommend it, because there is, in one of the two convenience methods, a case where the item name is ucfirst-ed. This will miss this case. =item Contents($) This method takes the current page number as an argument. It returns the slice of the array or list you handed it corresponding to the page number. This is the method that is most important and useful to this whole thing, really. =item Next($) Next takes a page number as an argument and returns the number of items in the *following* page. So if you slice up 67 items, page 6 will have items 51-60, and page 7 will have items 61-67. Thus, if you call: my $page7_items = $book->next(6); Your $page7_items will be set to 7. Of course, you can feel free to use this for it's boolean value as well. I do. =item Previous($) Similar to Next above, this method returns the number of items in the page BEFORE the page number given in the argument. This is really only useful for its boolean value, however, as the only way to get a different number than the number of pages you set is to ask for page 1 (which will return 0), or a page outside of the page list (i.e. number of pages+1 will return the number of items in the last page, while number of pages +2 will return 0). =item Item_Count($) Quite similar to the Next and Previous methods above, Item_Count returns the numbe of items in the current page. This is useful for finding out if there are any items at all (a boolean use) or just any old use you feel like putting it to. Of course, its argument is the page being referred to. =item Page_Count() This method returns the total number of pages in the 'book'. It takes no arguments at all. All it does is return the scalar value of @{$book->{page}}, the internal arrayref. =item First_Item($) This useful litle method returns the 1-indexed number of the first item in the current page as it equates to all of the items in the original list. It takes a page number as an argument. Assuming you haven't changed the default array index or gotten rid of the original array, ($book->Content($page))[0] will always match @original_array[$book->First($page)-1]. Using whatever names. =item Last_Item($) Like First_Item above, this returns the 1-indexed number of the *last* item in the current page slice as it associates with the original array. Again, it takes a page number as an argument. =item Page_Header_HTML($) This super cool method takes a page number as an argument, and returns a nicely formatted sentence telling you where you are in the 'book'. The results look like: Results 51 to 60 of 67 (Page 6 of 7) Isn't that nice? Of course, if you have set Name_Item to 'g' and Set_Plural to 'eese', it will say: Geese 51 to 60 of 67 (Page 6 of 7) And if you did the thing with mouse/mice that I said not to, it will say: mice 51 to 60 of 67 (Page 6 of 7) With 'mice' in lowercase, which is why I said 'don't do that.' If it's called wrong it tells you (via its return) in a square-bracketed SSI-esque sort of way. =item Page_Nav_HTML($) Another really cool method, this also takes a page number as an argument (is that part getting redundant or is it just me?) and returns a spiffy-cool formatted HTML link for each page forward or back (with 'page' as the CGI parameter name that looks like so: Last 10 items | Next 6 items or whatever. Again, setting the item name and plurality stuff changes the appearance. There is also some stuff below for property setting. Um, sorry, you can't change the cgi parametre name yet -- look for a later version. I just thought of that as I was writing this POD. One of the cool things about this is that it actually preserves any other arguments in the query string, but replaces its page number (and sticks it at the end). This way you can use it with other arguments at the same time. As long as they aren't 'page' (hey, I can't think of everything off the bat!) If it fails it returns a square bracketed message telling you it failed. This means you called it wrong (i.e. you gave it a silly page number like -1 or 0, or you didn't give it a page number, or you tried to call it as a sub instead of a method, or whatever.) =back =head1 Private Methods =over 4 =item _Ginsoo This method does all the work, chopping and slicing like one of those crazy chefs at Benihana. =item _Set_HTML_Text This does the work of setting your plurals and stuff. =item _Prep_HTML_Text This resets things based on html text you may have changed =item _Item_Count This does what the public method Item_Count does, but it takes a 0-indexed subarray index rather than a 'page number'. Thus for page 1, you ask for 0. Actually, you don't -- use the public methods. =back =head1 Properties =over 4 =item html properties The object has a hashref of properties keyed by 'html' inside it. This is set by the Name_Item and Set_Plural/Singular methods. You get to these with: $book->{html}->{} You can, if you really need to, mess around with these. They are as follows: pre_cap: The thing that begins the HTML navigation tag. Default empty string. end_cap: The thingy on the other end. Default empty string. previous_icon: a thingy pointing left. You can replace this with an image tag or something if you want. Default '<<' (<<) next_icon: As above with previous icn, but pointing right. Default '>>' (>>) singular: what you set with Set_Singular. Default '' plural: what you set with Set_Plural. Default 's' item_name: what you set with Name_Item. Default 'result' seperator: a thingy between direction links if there are two. Default ' | ' href_link: the URL to link to. Defaults to $ENV{REQUEST_URI} whatever that is. If you have to change this, remember that the CGI parametre name for the page rests here, not in the place where the number is filled in. Actually, you can always s/page/your_param_name/ here to change the parametre name the hard way. previous_text: A sprintf template to point at the prior page. Don't break the template if you feel you must change it. Default "Last %d $obj->{html}->{item_name}%s" next_text: As above. Default "Next %d $obj->{html}->{item_name}%s" =item num_per_page You can change the num_per_page if you want, though you will have to call the private method _Ginsoo to reslice. The num_per_page property is: $book->{num_per_page} =item book This is where the original array is stored. Note that is IS kept around. After the object is created you can remove it if you have to, and you can use this property if you suddenly change your mind in a fit of boredom. For instance, if you decide you want to slice up different stuff, you can. Just set this property to an arrayref of your choice: $book->{book} = \@new_array If you have to do this, remember to call $book->_Ginsoo before you expect it to do anything, really. =back =head1 GOOD INTENTIONS - Add an easy way to set the CGI parametre name. - Add the option of giving it a negative number as the first argument, and use that to slice for a given number of pages with whatever number per page, instead. - Methods to set words like 'Last' and 'Next' for easy locale changing. - A 'goooooooogle' style Page Navigation bar, maybe. =head1 AUTHOR Dodger - dodger@dodger.org =head1 WEBSITE http://www.perl5cgi.com =head1 SEE ALSO perl(1). =cut