blib/lib/HTML/TreeStructured.pm | |||
---|---|---|---|
Criterion | Covered | Total | % |
statement | 13 | 15 | 86.6 |
branch | n/a | ||
condition | n/a | ||
subroutine | 5 | 5 | 100.0 |
pod | n/a | ||
total | 18 | 20 | 90.0 |
line | stmt | bran | cond | sub | pod | time | code |
---|---|---|---|---|---|---|---|
1 | package HTML::TreeStructured; | ||||||
2 | 1 | 1 | 21833 | use 5.006; | |||
1 | 4 | ||||||
1 | 62 | ||||||
3 | 1 | 1 | 6 | use strict; | |||
1 | 9 | ||||||
1 | 43 | ||||||
4 | 1 | 1 | 5 | use warnings; | |||
1 | 7 | ||||||
1 | 92 | ||||||
5 | |||||||
6 | 1 | 1 | 7 | use Carp qw(croak); | |||
1 | 2 | ||||||
1 | 98 | ||||||
7 | 1 | 1 | 410 | use HTML::Template 2.6; | |||
0 | |||||||
0 | |||||||
8 | |||||||
9 | =head1 NAME | ||||||
10 | |||||||
11 | HTML::TreeStructured - Perl extension for generating tree structured HTML | ||||||
12 | |||||||
13 | =head1 SYNOPSIS | ||||||
14 | |||||||
15 | use HTML::TreeStructured; | ||||||
16 | |||||||
17 | ### Describe tree via collection of Node and its properties | ||||||
18 | |||||||
19 | ### Method 1: Via ArrayRef | ||||||
20 | ### | ||||||
21 | ### Node can be a string or '/' concatenated strings to show ancestry | ||||||
22 | ### Properties are name/value pairs | ||||||
23 | |||||||
24 | my $tree1 = [ | ||||||
25 | ['/aaa', color => 'green'], | ||||||
26 | ['/aaa/bbb' mouseover => 'This is addl info'], | ||||||
27 | ['/aaa/ccc', color => 'red', active => 0] | ||||||
28 | ]; | ||||||
29 | |||||||
30 | ### Method 2: Via Hashref | ||||||
31 | |||||||
32 | my $tree2 = { | ||||||
33 | aaa => { | ||||||
34 | color => 'green', | ||||||
35 | bbb => { | ||||||
36 | mouseover => 'This is addl info', | ||||||
37 | }, | ||||||
38 | ccc => { | ||||||
39 | color => 'red', | ||||||
40 | active => 0, | ||||||
41 | }, | ||||||
42 | }; | ||||||
43 | |||||||
44 | Interpreted Node Properties: | ||||||
45 | |||||||
46 | color = Color of the node name | ||||||
47 | mouseover = Mouse Over text for the node (Info image is displayed next to node) | ||||||
48 | active = 0 would cause strike thru on node | ||||||
49 | highlight = color code used for marker highlight of node | ||||||
50 | url = URL to hyperlink the node | ||||||
51 | tooltip = popup when mouse is over the link (together with url) (See HTML::Tooltip::Javascript) | ||||||
52 | closed = 1 if node be closed on default display (default, all nodes are open) | ||||||
53 | comment = Text to display next to node in bold | ||||||
54 | weight = A numeric value on node which will be used for sorting node position in at sibling level | ||||||
55 | (Default, nodes are sorted in ascending order per dictionary order) | ||||||
56 | |||||||
57 | |||||||
58 | ### Now get HTML equivalent for the tree | ||||||
59 | ### The associated JavaScript for nodes close/open and ExpandAll/CollapseAll is generated alongside | ||||||
60 | |||||||
61 | $tree_html = HTML::TreeStructured->new( | ||||||
62 | name => 'tree_name', | ||||||
63 | image_path => '/images/', | ||||||
64 | data => $tree1, | ||||||
65 | title => "My Tree", | ||||||
66 | title_width => 300, | ||||||
67 | level => {}, ### If scalar, close BEYOND this depth. Depth start at 0. | ||||||
68 | ### If Hash, close for depths specified in keys | ||||||
69 | )->output; | ||||||
70 | |||||||
71 | ### The same module can be used to generate FAQ - see "examples/faq.cgi" | ||||||
72 | |||||||
73 | =cut | ||||||
74 | |||||||
75 | our $VERSION = "1.01"; | ||||||
76 | |||||||
77 | sub new | ||||||
78 | { | ||||||
79 | my $pkg = shift; | ||||||
80 | |||||||
81 | # setup defaults and get parameters | ||||||
82 | my $self = bless({ | ||||||
83 | |||||||
84 | title_width => 300, | ||||||
85 | child_indent => 20, | ||||||
86 | image_path => ".", | ||||||
87 | level => {}, ### Default open up the full tree - This would contain | ||||||
88 | ### Scalar => depth BEYOND which nodes are closed for tree display | ||||||
89 | ### Hash => depths matching keys are closed for tree display | ||||||
90 | ### NB: Depth starts at ZERO | ||||||
91 | ### E.g. Value of | ||||||
92 | ### level=2 means Tree nodes are closed at depth 3 and more (all others are open) | ||||||
93 | ### level={2=>1} means Tree node at depth 2 are closed (all others are open) | ||||||
94 | @_, | ||||||
95 | }, $pkg); | ||||||
96 | |||||||
97 | # fix up image_path to always end in a / | ||||||
98 | $self->{image_path} .= "/" unless $self->{image_path} =~ m!/$!; | ||||||
99 | |||||||
100 | # check required params | ||||||
101 | foreach my $req (qw(name data title)) { | ||||||
102 | croak("Missing required parameter '$req'") unless exists $self->{$req}; | ||||||
103 | } | ||||||
104 | |||||||
105 | if (ref($self->{data}) eq 'ARRAY') { | ||||||
106 | $self->{data} = process_arrayref($self->{data}); | ||||||
107 | } else { | ||||||
108 | my $res; | ||||||
109 | my $data = $self->{data}; | ||||||
110 | my @kkk = grep { ref($data->{$_}) eq 'HASH' } keys %$data; | ||||||
111 | if (1 == @kkk) { | ||||||
112 | $res = process_hashref($kkk[0], $data->{$kkk[0]}); | ||||||
113 | } else { | ||||||
114 | $res = process_hashref('ROOT', $data); | ||||||
115 | } | ||||||
116 | $self->{data} = $res; | ||||||
117 | } | ||||||
118 | |||||||
119 | #use Data::Dumper; | ||||||
120 | #print '', Dumper($self->{data}), ''; |
||||||
121 | |||||||
122 | return $self; | ||||||
123 | } | ||||||
124 | |||||||
125 | sub output | ||||||
126 | { | ||||||
127 | my $self = shift; | ||||||
128 | our $TEMPLATE_SRC; | ||||||
129 | my $template = HTML::Template->new(scalarref => \$TEMPLATE_SRC, | ||||||
130 | die_on_bad_params => 0, | ||||||
131 | global_vars => 1, | ||||||
132 | ); | ||||||
133 | |||||||
134 | # build node loop | ||||||
135 | my @loop; | ||||||
136 | $self->_output_node(node => $self->{data}, | ||||||
137 | loop => \@loop, | ||||||
138 | depth => 1, | ||||||
139 | level => $self->{level}, | ||||||
140 | ); | ||||||
141 | my @parents; ### Collect all nodes with children - for use in ExpandAll/CollapseAll | ||||||
142 | map { push(@parents, {id => $_->{id}}) if ($_->{has_children}) } @loop; | ||||||
143 | # setup template parameters | ||||||
144 | $template->param(loop => \@loop); | ||||||
145 | $template->param(parents => \@parents); | ||||||
146 | $template->param(map { ($_, $self->{$_}) } qw(name title title_width child_indent image_path)); | ||||||
147 | # get output for the widget | ||||||
148 | my $output = $template->output; | ||||||
149 | |||||||
150 | return $output; | ||||||
151 | } | ||||||
152 | |||||||
153 | # recursively add nodes to the output loop | ||||||
154 | sub _output_node | ||||||
155 | { | ||||||
156 | my ($self, %arg) = @_; | ||||||
157 | my $node = $arg{node}; | ||||||
158 | my $depth = $arg{depth}; | ||||||
159 | my $level = $arg{level}; | ||||||
160 | |||||||
161 | #use Data::Dumper; | ||||||
162 | #print "", Dumper($node), ""; |
||||||
163 | |||||||
164 | my $id = next_id(); | ||||||
165 | push @{$arg{loop}}, { label => $node->{label}, ### Label to appear in tree | ||||||
166 | value => $node->{value}, ### Hidden Value (whats the use?, but good to have) | ||||||
167 | id => $id, ### Unique Id (no immediate use, but good to have) | ||||||
168 | open => display_closed_node($depth, $level, $node->{closed}) ? 0 : 1, | ||||||
169 | ### During Display, whether to close/open | ||||||
170 | url => $node->{url}, ### Url to link | ||||||
171 | mouseover => $node->{mouseover}, ### mouseover message | ||||||
172 | tooltip => $node->{tooltip}, ### Tooltip popup box (See HTML::Tooltip::Javascript) | ||||||
173 | active => ((defined($node->{active}) and $node->{active} == 0) ? 0 : 1), | ||||||
174 | ### Is this node active? If not, strike thru during display | ||||||
175 | color => $node->{color} || 'black', ### Color of the label | ||||||
176 | highlight => $node->{highlight}, ### Color to use for marker highlight | ||||||
177 | comment => $node->{comment}, ### Comment in bold within parentheses next to label | ||||||
178 | }; | ||||||
179 | |||||||
180 | if ($node->{children} and @{$node->{children}}) { | ||||||
181 | $arg{loop}[-1]{has_children} = 1; | ||||||
182 | for my $child (@{$node->{children}}) { | ||||||
183 | $self->_output_node(node => $child, | ||||||
184 | loop => $arg{loop}, | ||||||
185 | depth => $depth + 1, | ||||||
186 | level => $level, | ||||||
187 | ); | ||||||
188 | } | ||||||
189 | push @{$arg{loop}}, { end_block => 1 }; | ||||||
190 | } | ||||||
191 | |||||||
192 | } | ||||||
193 | |||||||
194 | sub display_closed_node | ||||||
195 | { | ||||||
196 | my $ddd = shift; # Depth Info | ||||||
197 | my $lll = shift; # Level Info | ||||||
198 | # scalar ==> Close all nodes BEYOND this depth | ||||||
199 | # hash ==> Close all nodes for specified keys | ||||||
200 | my $closed = shift; | ||||||
201 | |||||||
202 | if (defined($closed)) { | ||||||
203 | return $closed; | ||||||
204 | } | ||||||
205 | |||||||
206 | if (ref($lll) eq 'HASH') { | ||||||
207 | return ($lll->{$ddd} ? 1 : 0); | ||||||
208 | } else { | ||||||
209 | return ($ddd > $lll ? 1 : 0); | ||||||
210 | } | ||||||
211 | } | ||||||
212 | |||||||
213 | { | ||||||
214 | my $id = 1; | ||||||
215 | sub next_id { $id++ } | ||||||
216 | } | ||||||
217 | |||||||
218 | our $TEMPLATE_SRC = < | ||||||
219 | |||||||
252 | |||||||
253 | |||||||
305 | |||||||
306 | |||||||
307 | |
||||||
308 | |
||||||
309 | ExpandAll | CollapseAll | ||||||
310 | |
||||||
311 | |
||||||
312 | |
||||||
313 | |
||||||
314 | |
||||||
315 | |||||||
316 | |
||||||
317 | |||||||
318 | |||||||
319 | "Double click to toggle (Open/Close)" |
||||||
320 | |||||||
321 | |
||||||
322 | |
||||||
323 | |||||||
324 | |||||||
325 | |||||||
326 | |||||||
327 | |||||||
328 | |
||||||
329 | |
||||||
330 | |||||||
331 | |||||||
332 | |||||||
333 | |||||||
334 | |||||||
335 | |
||||||
336 | |
||||||
337 | |||||||
338 | |
||||||
339 | |
||||||
340 | |
||||||
341 | |||||||
342 | |||||||
343 | |
||||||
344 | style="display: block" |
||||||
345 | |||||||
346 | |
||||||
347 | |||||||
348 | |||||||
349 | |||||||
350 | |||||||
351 | |||||||
352 | |||||||
353 | |||||||
354 | END | ||||||
355 | |||||||
356 | ### Process file having tree specification | ||||||
357 | sub process_tree_file | ||||||
358 | { | ||||||
359 | my $in = shift; | ||||||
360 | my $separator = shift || "\t"; | ||||||
361 | |||||||
362 | open(FFF, $in); | ||||||
363 | my @lines = |
||||||
364 | close(FFF); | ||||||
365 | chomp(@lines); | ||||||
366 | |||||||
367 | return process_arrayref([ map { [split($separator, $_)] } grep { ! /^\s*#/ } @lines ]); | ||||||
368 | } | ||||||
369 | |||||||
370 | ### Recursively process path and create XML-structure | ||||||
371 | sub process_arrayref | ||||||
372 | { | ||||||
373 | my $in = shift; | ||||||
374 | |||||||
375 | my $data = {}; | ||||||
376 | |||||||
377 | for my $i (@$in) { | ||||||
378 | my @iii = @$i; | ||||||
379 | my $head = shift(@iii); | ||||||
380 | my @a = grep {$_} split('/', $head); | ||||||
381 | my $a = join('', map { "{'" . $_ . "'}" } @a); | ||||||
382 | my %prop = @iii; | ||||||
383 | if (%prop) { | ||||||
384 | while (my ($k,$v) = each %prop) { | ||||||
385 | my $x = $v; | ||||||
386 | $x =~ s/\\/\\\\/g; | ||||||
387 | $x =~ s/'/\\'/g; | ||||||
388 | #print "v=$v x=$x \n"; |
||||||
389 | eval "\$data->$a" . "{$k} = '$x'"; | ||||||
390 | } | ||||||
391 | } else { | ||||||
392 | eval "\$data->$a = {}"; | ||||||
393 | } | ||||||
394 | } | ||||||
395 | |||||||
396 | my $res; | ||||||
397 | my @kkk = keys %$data; | ||||||
398 | if (1 == @kkk) { | ||||||
399 | $res = process_hashref($kkk[0], $data->{$kkk[0]}); | ||||||
400 | } else { | ||||||
401 | $res = process_hashref('ROOT', $data); | ||||||
402 | } | ||||||
403 | return $res; | ||||||
404 | } | ||||||
405 | |||||||
406 | ## Recursively take hashref and construct XML-structure (merely transforms the raw structure created in process_path_arrayref) | ||||||
407 | sub process_hashref | ||||||
408 | { | ||||||
409 | my $k = shift; | ||||||
410 | my $v = shift; | ||||||
411 | |||||||
412 | my $res = { label => $k, children => [] }; | ||||||
413 | map { | ||||||
414 | if (ref($v->{$_}) eq 'HASH') { | ||||||
415 | push(@{$res->{children}}, process_hashref($_, $v->{$_})); | ||||||
416 | } else { | ||||||
417 | $res->{$_} = $v->{$_}; | ||||||
418 | } | ||||||
419 | # } sort {$a <=> $b} keys %$v; | ||||||
420 | } sort { (ref($v->{$a}) eq 'HASH' and defined($v->{$a}{weight}) and ref($v->{$b}) eq 'HASH' and defined($v->{$b}{weight})) ? $v->{$a}{weight} <=> $v->{$b}{weight} : $a cmp $b } keys %$v; | ||||||
421 | return $res; | ||||||
422 | } | ||||||
423 | |||||||
424 | =head1 AUTHOR | ||||||
425 | |||||||
426 | Ramana Mokkapati, |
||||||
427 | |||||||
428 | I have been using HTML tables for structuring HTML presentation. | ||||||
429 | After seeing HTML::PopupTreeSelect from Sam Tregar |
||||||
430 | I liked the idea of stylesheets to indent HTML and adapted the same. | ||||||
431 | |||||||
432 | This library is free software; you can redistribute it and/or modify | ||||||
433 | it under the same terms as Perl itself, either Perl version 5.8.2 or, | ||||||
434 | at your option, any later version of Perl 5 you may have available. | ||||||
435 | |||||||
436 | =cut | ||||||
437 | |||||||
438 | 1; |