File Coverage

blib/lib/HTML/DataTable.pm
Criterion Covered Total %
statement 15 235 6.3
branch 0 120 0.0
condition 0 114 0.0
subroutine 5 29 17.2
pod 7 17 41.1
total 27 515 5.2


); ); ); ); );
line stmt bran cond sub pod time code
1             package HTML::DataTable;
2              
3 1     1   20638 use 5.006;
  1         4  
  1         30  
4 1     1   5 use strict;
  1         1  
  1         25  
5 1     1   4 use warnings;
  1         6  
  1         38  
6              
7 1     1   759 use HTML::FromArrayref;
  1         7745  
  1         70  
8              
9 1     1   1509 use overload q{""} => \&list_HTML;
  1         880  
  1         7  
10              
11             =head1 NAME
12              
13             HTML::DataTable - Print HTML tables from Perl data
14              
15             =head1 VERSION
16              
17             Version 0.54
18              
19             =cut
20              
21             our $VERSION = 0.54;
22              
23             =head1 SYNOPSIS
24              
25             use HTML::DataTable
26             my $list = HTML::DataTable->new(
27             data => $cgi_data,
28             columns => [
29             # hashrefs describing column formats
30             ],
31             rows => [
32             # arrayrefs listing data to show in table
33             ],
34             );
35             print $list;
36              
37             =head1 METHODS
38              
39             =head2 new()
40              
41             Creates a new HTML::DataTable object.
42              
43             =head3 ATTRIBUTES
44              
45             =head3 header_bg
46              
47             The HTML color code for the background of the first row of the list.
48              
49             =head3 header_bar
50              
51             If this is defined, there will be a 1px black line under the header.
52              
53             =head3 shade_alternate_rows
54              
55             If this is defined, then alternating rows will be colored differently.
56              
57             =head3 light_bg, dark_bg
58              
59             The HTML color codes for the alternating backgrounds of the list rows.
60              
61             =head3 sections
62              
63             If this evaluates to a reference to a subroutine, then that subroutine will be called with each row's values, and when the returned values changes, the table will be divided and the value printed as a section title. (Since there's no value initially, there will be a section title before the first row.)
64              
65             =head3 section_headers
66              
67             If this is defined, the table header will be reprinted after each section title.
68              
69             =head3 alphabet
70              
71             If this is defined, a linked alphabet index will be printed above the table header. Subclasses of this class are responsible for using the "letter" CGI parameter to show the appropriate rows.
72              
73             =head3 search
74              
75             If this is defined, then a field called "search" will be shown in the table header (after the alphabet, if that attribute is also defined). If it evaluates to a reference to a hash, then the keys of the hash will be shown in a SELECT control called "search_columns" after the field. Subclasses of this class are responsible for using these CGI parameters to show the appropriate rows.
76              
77             =head3 data
78              
79             Should be assigned a hashref representing the CGI parameters.
80              
81             =head3 rows
82              
83             One of:
84              
85             * An arrayref listing one arrayref holding the values to appear in each row of the list.
86              
87             * An arrayref listing one hashref holding the values to appear in each row of the list.
88              
89             * A hashref mapping each row's ID to a hashref holding the values to appear in that row of the list, in which case the "sort" attribute should name the hash key by which to sort the hashref entries.
90              
91             =head3 columns
92              
93             An arrayref listing one hashref defining each column of the table. These hashrefs can have these attributes:
94              
95             =head4 header
96              
97             The text to print at the top of this column.
98              
99             =head4 category
100              
101             A second-level header to be printed above the column header; adjacent column's category headers will be merged if they are the same.
102              
103             =head4 format
104              
105             This can be either
106              
107             * A scalar, which will be used as an index in the current row's data array
108              
109             * An arrayref, which will list an index in the current row's data array and singular and plural nouns to append to the value found there
110              
111             * A reference to a subroutine, which will be passed the current row's data array
112              
113             * A hashref, which will map a predefined format name to an index in the current row's data array
114              
115             =head4 none
116              
117             A string to show if the value in that column is undefined. Defaults to "None".
118              
119             =head4 style, class, align
120              
121             If any of these attributes evaluate to a string, they will become the corresponding attributes of each table cell in the column.
122              
123             =head4 action
124              
125             The path or URL of a CGI program to which each entry will linked.
126              
127             =head4 data
128              
129             A reference to a hash listing the CGI parameters to be included in the "action" link. Each value is either a scalar or a reference, as for the "format" attribute.
130              
131             =head4 link_empty
132              
133             If defined, then if the value in the column is undefined the "None" shown will be linked to the "action" URL.
134              
135             =head4 norepeat
136              
137             If this is defined, then the column will be left blank if the value printed would be the same the that for the previous row.
138              
139             =head4 nobr
140              
141             If this is defined, then the column's content will be surrounded by tags so it isn't formatted into multiple lines.
142              
143             =cut
144              
145             sub new {
146 0     0 1   my $pkg = shift;
147              
148 0 0         my $attribs = ref $_[0] ? $_[0] : { @_ };
149 0           return bless $attribs, $pkg;
150             }
151              
152             =head3 list_HTML()
153              
154             Returns the HTML that renders the list.
155              
156             =cut
157              
158             sub list_HTML {
159 0     0 1   my $me = shift;
160              
161 0           my ($html, $title, @cols, $bgcolor);
162              
163 0 0         if ( ! $me->{columns} ) {
164 0           print "

Invalid list definiton.

";
165 0           return;
166             }
167              
168 0   0       $me->{cellpadding} ||= 1;
169 0   0       $me->{cellspacing} ||= 0;
170 0   0       $me->{light_bg} ||= '#ffffff';
171 0   0       $me->{_bg_color} = $me->{dark_bg} ||= '#eeeeee';
172 0   0       $me->{header_bg} ||= $me->{dark_bg};
173 0   0       $me->{none} ||= 'None';
174 0 0         $me->{shade_alternate_rows} = 'no' if grep $_->{shade_alternate_vals}, @{$me->{columns}};
  0            
175              
176 0           $me->{columns}->[0]->{class} = 'first_col';
177              
178 0 0 0       if ( $me->{alphabet} and ! $me->{data}->{search} ) {
179 0   0       $me->set_letter( $me->{data}->{letter} ||= 'a' );
180             }
181              
182 0 0         if ( $me->{data}->{search} ) {
183 0           $me->set_search();
184             }
185              
186             # SET ORDER BY IF SORTING ##############
187              
188 0 0 0       delete $me->{data}->{sort} if defined $me->{data}->{sort} and $me->{data}->{sort} eq $me->{sort}->[0];
189 0 0         unshift @{$me->{sort}}, $me->{data}->{sort} if defined $me->{data}->{sort};
  0            
190              
191 0 0 0       delete $me->{data}->{sort_dir} if defined $me->{data}->{sort_dir} and $me->{data}->{sort_dir} eq 'asc';
192 0 0         $me->{sort_dir} = $me->{data}->{sort_dir} if defined $me->{data}->{sort_dir};
193 0 0         if ( $me->{sort} ) { $me->set_sort_order }
  0            
194              
195             # PRINT LIST #############
196              
197 0           $me->{n_cols} = scalar( @{$me->{columns}} );
  0            
198              
199 0 0         $html .= qq(

$me->{hed}

) if $me->{hed};
200 0   0       $me->{cellpadding} ||= 1;
201 0   0       $me->{cellspacing} ||= 0;
202 0           my $table_attribs = join ' ', map qq($_="$me->{$_}"), grep exists $me->{$_},
203             qw( border width cellspacing cellpadding class id style );
204 0           $html .= qq(); )
205 0 0         $html .= qq(
) if $me->{search};
206              
207             # List header ###########
208              
209 0 0         my $alphabet = $me->alphabet if $me->{alphabet};
210 0 0 0       my $search = $me->search_form if $me->{search} and ! $me->{hide_search_form};
211              
212             # Alphabet
213 0 0 0       $html .= qq(
$alphabet$search
214             if $alphabet or $search;
215              
216             # Table header
217 0 0 0       $html .= $me->header unless
      0        
      0        
218             defined $me->{header} and $me->{header} eq 'no'
219             or ( $me->{sections} and $me->{header} ne 'yes' );
220              
221             # Print rows ###################
222              
223 0           my $rows;
224 0           while ( my $d = $me->next_row ) {
225 0           $rows .= $me->table_row( $d );
226             }
227              
228 0 0 0       if ( ! $rows and $me->{hide_if_empty} ) { return '' }
  0            
229              
230 0           $html .= $rows;
231              
232 0 0         $html .= qq() if $me->{search};
233 0           $html .= '
';
234              
235 0 0         $html .= << "" if $me->{javascript};
236            
239              
240 0           return $html;
241             }
242              
243             sub header {
244 0     0 1   my $me = shift;
245              
246 0           my $html;
247              
248 0 0         if ( grep $_->{category}, @{$me->{columns}} ) {
  0            
249 0           $html = qq(
250 0           my ($prev_col, $i_col, $colspan);
251 0           for my $col ( @{$me->{columns}}, { category => '_END_OF_COLUMNS' } ) {
  0            
252 0 0         $colspan++ if $i_col;
253 0 0 0       if ( $i_col++ and $col->{category} ne $prev_col->{category} ) {
254 0   0       $html .= HTML
255             [ th => { colspan => $colspan, class => $col->{class}, style => 'text-align: center; font-weight: bold' },
256             [ $col->{nobr} && 'nobr', $prev_col->{category} ]
257             ];
258 0           $colspan = 0;
259             }
260 0           $prev_col = $col;
261             }
262 0           $html .= qq(
263             }
264              
265 0           $html .= qq(
266 0           for my $col ( @{$me->{columns}} ) {
  0            
267 0           $html .= $me->column_header( $col );
268             }
269 0           $html .= qq(
270              
271 0 0         $html .= $me->header_bar if $me->{header_bar};
272              
273 0           return $html;
274             }
275              
276             sub column_header {
277 0     0 0   my ($me, $col) = @_;
278              
279 0           my $content;
280              
281 0 0         if ( $col->{sort} ) {
282 0 0         if ( $col->{sort} eq $me->{sort}->[0] ) {
283 0 0         my ($other_dir, $dir_link) = $me->{sort_dir} eq 'desc' ? ('asc', '/') : ('desc', '\\');
284 0           $content = [ a => { href => $me->query_string( sort_dir => $other_dir ) }, [ b => $col->{header} ] ];
285             } else {
286 0           $content = [ a => { href => $me->query_string( sort => $col->{sort} ) }, $col->{header} ];
287             }
288             } else {
289 0           $content = $col->{header};
290             }
291              
292 0   0       return HTML [ th => { class => $col->{class}, style => $col->{header_style} || $col->{style} || undef },
      0        
      0        
293             [ $col->{nobr} && 'nobr',
294             $content,
295             defined $col->{data} && $col->{add} && ( ' (', [[ $me->link( $col, 'Add' ) ]], ')' )
296             ]
297             ];
298             }
299              
300             sub header_bar {
301 0     0 1   my $me = shift;
302              
303 0           return HTML [ tr => { height => 1, valign => 'bottom', class => 'nodrag nodrop' },
304             [ td => { height => 1, colspan => $me->{n_cols}, id => 'header_bar' },
305             [ table => { cellspacing => 0, cellpadding => 0, height => 1, width => '100%' },
306             [ tr => { height => 1, bgcolor => '#000000' }, [ 'td' ] ]
307             ]
308             ]
309             ];
310             }
311              
312 0     0 0   sub set_letter { }
313              
314             sub search_form {
315 0     0 0   my $me = shift;
316              
317             return HTML
318             [ input => { name => 'search', value => $me->{data}->{search} } ],
319             ref $me->{search} eq 'HASH' && [ select => { name => 'search_columns' },
320 0   0       map [ option => $_ ], keys %{$me->{search}}
321             ];
322             }
323              
324 0     0 0   sub set_sort_order { }
325              
326             sub next_row {
327 0     0 0   my $me = shift;
328              
329 0 0         if ( ref $me->{rows} eq 'ARRAY' ) {
    0          
330 0           return shift @{$me->{rows}};
  0            
331             } elsif ( ref $me->{rows} eq 'HASH' ) {
332 0   0 0     my $sorter = sub{ $me->{rows}->{ $_[0] }->{ $me->{sort} || 'name' } };
  0            
333             return $me->{rows}->{
334 0   0       shift @{ $me->{_row_hash_ids} ||= [ sort { $sorter->($a) cmp $sorter->($b) } keys %{$me->{rows}} ] }
  0            
  0            
  0            
335             };
336             }
337             }
338              
339             sub table_row {
340 0     0 0   my ($me, $d) = @_;
341              
342 0           my $html;
343              
344             # Start a new section if section has changed
345 0 0 0       if ( $me->{sections} and $me->{_section} ne ( my $section = $me->{sections}->( @$d ) ) ) {
346 0           $html .= qq(

$section

347 0 0         $html .= $me->header if $me->{section_headers} ne 'no';
348 0           $me->{_section} = $section;
349 0           $me->{_bgcolor} = $me->{dark_bg};
350             }
351              
352 0           my $cells;
353 0           for my $col ( @{$me->{columns}} ) {
  0            
354 0           $cells .= $me->table_cell( $col, $d );
355             }
356              
357             # Change row bgcolor
358 0 0 0       $me->switch_bgs unless defined $me->{shade_alternate_rows} and $me->{shade_alternate_rows} eq 'no';
359              
360 0   0       $html .= HTML [ tr =>
361             {
362             valign => 'top',
363             bgcolor => $me->{_bgcolor},
364             id => defined $me->{row_id_col} && join '-', 'row', $d->[$me->{row_id_col} ]
365             },
366             [[ $cells ]]
367             ];
368              
369 0           return $html;
370              
371             }
372              
373             sub switch_bgs {
374 0     0 0   my $me = shift;
375              
376 0 0 0       $me->{_bgcolor} = ( defined $me->{_bgcolor} and $me->{_bgcolor} eq $me->{light_bg} ) ?
377             $me->{dark_bg} : $me->{light_bg};
378             }
379              
380             sub table_cell {
381 0     0 0   my ($me, $col, $d) = @_;
382              
383 0   0       $col->{none} ||= $me->{none};
384              
385 0           my $content;
386              
387 0           $content = $me->format( $col, $d );
388              
389 0   0       return HTML [ td => { style => $col->{style}, class => $col->{class}, align => $col->{align} },
390             [ $col->{nobr} && 'nobr',
391             [[ $content ]]
392             ]
393             ];
394             }
395              
396             sub format {
397 0     0 1   my ($me, $col, $d) = @_;
398              
399 0           my @data;
400              
401             # Upgrade $col->{format} to a subroutine ref
402 0 0         if ( ! ref $col->{format} ) {
    0          
    0          
403             # the "format" attribute can be either an index into an array or a key into a hash
404 0 0         if ( $col->{format} =~ /\D/ ) {
405 0   0 0     $col->{formatter} ||= sub { my %d = @_; $d{ $col->{format} } };
  0            
  0            
406 0           @data = ( %$d );
407             } else {
408 0   0 0     $col->{formatter} ||= sub { $_[ $col->{format} ] };
  0            
409 0           @data = ( @$d );
410             }
411             } elsif ( ref $col->{format} eq 'ARRAY' ) {
412             # or it can be a reference to an array mapping the index to a noun and optional plural
413             $col->{formatter} ||= sub {
414 0     0     my ($index, $noun, $plural) = @{$col->{format}};
  0            
415 0           my $value = $_[ $index ];
416 0 0         return 0 if $value == 0;
417 0 0         $noun .= 's' if $value > 1;
418 0 0 0       $noun = $plural if $value > 1 and $plural;
419 0           return "$value $noun";
420 0   0       };
421 0           @data = ( @$d );
422             } elsif ( ref $col->{format} eq 'HASH' ) {
423             # or it can be a reference to a hash mapping a predefined format to an index into the data
424 0 0         if ( ! $col->{formatter} ) {
425 0           my ($format, $index) = each %{$col->{format}};
  0            
426             $col->{formatter} = {
427 0     0     date => sub { my @parts = split /\D/, $_[$index]; join '/', grep $_, @parts[1,2,0]; },
  0            
428             datetime => sub {
429 0     0     my @parts = split /\D/, $_[$index];
430 0           join ' ', join( '/', grep $_, @parts[1,2,0] ), join( ':', grep $_, @parts[3,4,5] );
431             },
432 0   0 0     }->{$format} || sub { "UNKNOWN FORMAT NAME $format" };
  0            
433             }
434 0           @data = ( @$d );
435             } else {
436             # or it can already be a reference to a subroutine
437 0   0       $col->{formatter} ||= $col->{format};
438 0           @data = ( @$d );
439             }
440              
441 0           my $test = $col->{test};
442              
443 0           my $formatted;
444              
445 0 0 0       if (
    0 0        
    0 0        
446 0 0         ! ( ref $test eq 'CODE' and ! $test->(@data) )
447             and $formatted = $col->{formatter}->(
448             map { $col->{contains_html} ? $_ : HTML $_ } @data
449             )
450             ) {
451              
452 0 0 0       if ( $col->{norepeat} and $formatted eq $col->{_prev_value} ) {
453 0           $formatted = '';
454             } else {
455 0 0         $me->switch_bgs if $col->{shade_alternate_vals};
456 0           $col->{_prev_value} = $formatted;
457 0 0         $formatted = $me->link( $col, $formatted, $d ) if $col->{action};
458             }
459              
460             } elsif ( $col->{action} and $col->{link_empty} ) {
461              
462 0           $formatted = $me->link( $col, $col->{none}, $d );
463              
464             } elsif ( $col->{none_not_dimmed} ) {
465              
466 0           $formatted = $col->{none};
467              
468             } else {
469              
470 0           $formatted = qq($col->{none});
471              
472             }
473              
474 0           return $formatted;
475              
476             }
477              
478             sub link {
479 0     0 0   my ($me, $col, $formatted, $d) = @_;
480              
481 0   0       my $action = $col->{action} || $me->{action}; # . ( $col->{action} =~ /\?/ ? '&' : '?' );
482 0 0         if ( ref $action eq 'CODE' ) { $action = $action->(@$d) }
  0            
483 0 0         return $formatted unless $action;
484              
485 0           my %query_data;
486              
487 0           while ( my ($q_key, $q_format) = each %{$col->{data}} ) {
  0            
488             # The datum's 'format' attribute can be a reference to a subroutine,
489             # in which case we execute it; the word 'format', in which case we use
490             # the column's formatted value; or any other value, which we use unaltered.
491 0 0         next unless $q_format;
492 0 0         if ( ref $q_format eq 'CODE' ) {
    0          
493 0           $query_data{$q_key} = $q_format->(@$d);
494             } elsif ( $q_format eq 'format' ) {
495 0           $query_data{$q_key} = $formatted;
496             } else {
497 0           $query_data{$q_key} = $q_format;
498             }
499 0           $query_data{$q_key} =~ s|(\W)|sprintf("%%%x", ord($1))|eg;
  0            
500             }
501              
502 0   0       $me->{first_row_data} ||= \%query_data;
503              
504 0 0         my $query_string = join '&', ( map { join '=', $_, $query_data{$_} } grep defined $query_data{$_}, keys %query_data ) if %query_data;
  0            
505 0 0         $action .= ( $action =~ /\?/ ? '&' : '?' ) if $query_string;
    0          
506 0           return HTML [ a => { href => "$action$query_string" }, [[ $formatted ]] ];
507             }
508              
509             sub alphabet {
510 0     0 1   my $me = shift;
511              
512 0           my @alphabet;
513              
514 0   0       my $letter = $me->{data}->{letter} || 'a';
515 0 0         undef $letter if $me->{data}->{search};
516 0           for my $l ( 'A' .. 'Z' ) {
517 0 0         push @alphabet,
518             lc $l eq lc $letter ?
519             "$l " :
520             q($l );
521             }
522 0           return join ' ', @alphabet;
523             }
524              
525             sub query_string {
526 0     0 0   my $me = shift;
527 0           my %replace = @_;
528              
529 0           my %data = %{$me->{data}};
  0            
530 0           @data{ keys %replace } = values %replace;
531 0 0         return ( %data ? '?' : '' ) . join '&', map "$_=$data{$_}", grep $data{$_}, keys %data;
532             }
533              
534             =head3 xls
535              
536             Returns the list as an Excel spreadsheet.
537              
538             =cut
539              
540             sub xls {
541 0     0 1   my $me = shift;
542              
543 0           require Spreadsheet::WriteExcel;
544              
545 0 0         open my $fh, '>', \my $str or die "Failed to open filehandle: $!";
546 0           my $workbook = Spreadsheet::WriteExcel->new( $fh );
547              
548 0           my $worksheet = $workbook->addworksheet("Data");
549 0           $worksheet->set_column('A:AZ', 40);
550              
551 0           my $i_col = 0;
552 0           for ( @{$me->{columns}} ) {
  0            
553 0           $worksheet->write_string( 0, $i_col++, $_->{header} );
554             }
555 0           $worksheet->freeze_panes(1, 0);
556              
557 0 0         if ( $me->{sort} ) {
558 0           $me->set_sort_order;
559             }
560              
561 0           my $i_row = 1;
562 0           while ( my $data = $me->next_row ) {
563 0           $i_col = 0;
564 0           for my $col ( @{$me->{columns}} ) {
  0            
565 0           $col->{none_not_dimmed} = 'y';
566 0           delete $col->{action};
567 0           $col->{contains_html} = 'y';
568 0           $worksheet->write_string( $i_row, $i_col++, $me->format( $col, $data ) );
569             }
570 0           $i_row++;
571             }
572              
573 0           $workbook->close;
574              
575 0           $str;
576              
577             }
578              
579             1;
580              
581             =head1 SEE ALSO
582              
583             HTML::DataTable::DBI, HTML::FromArrayref
584              
585             =head1 AUTHORS
586              
587             Nic Wolff
588             Jason Barden
589              
590             =cut