File Coverage

blib/lib/Dancer2/Plugin/Menu.pm
Criterion Covered Total %
statement 73 73 100.0
branch 13 16 81.2
condition 8 15 53.3
subroutine 13 13 100.0
pod 1 2 50.0
total 108 119 90.7


line stmt bran cond sub pod time code
1             package Dancer2::Plugin::Menu ;
2             $Dancer2::Plugin::Menu::VERSION = '0.006';
3 2     2   932396 use 5.010; use strict; use warnings;
  2     2   15  
  2     2   11  
  2         3  
  2         39  
  2         19  
  2         4  
  2         79  
4              
5             # ABSTRACT: Automatically generate an HTML menu for your Dancer2 app
6              
7 2     2   612 use Storable 'dclone';
  2         3155  
  2         155  
8 2     2   15 use List::Util 'first';
  2         3  
  2         149  
9 2     2   583 use Data::Dumper 'Dumper';
  2         6500  
  2         106  
10 2     2   1789 use HTML::Element;
  2         39097  
  2         11  
11 2     2   1176 use Dancer2::Plugin;
  2         228678  
  2         24  
12 2     2   45072 use Dancer2::Core::Hook;
  2         6  
  2         1688  
13              
14             plugin_keywords qw ( menu_item );
15              
16             ### ATTRIBUTES ###
17             # tree is the current active tree with "active" tags
18             # clean_tree is tree without "active" tags, used to easily reset tree
19             # html contains HTML generated from the tree
20              
21             # Separting the HTML from a logical data structure is probably slightly more
22             # expensive but makes the code cleaner and easier to follow.
23              
24             has 'tree' => ( is => 'rw', default => sub { { '/' => { children => {} } } } );
25             has 'clean_tree' => ( is => 'rw', predicate => 1,);
26             has 'html' => ( is => 'rw');
27             ###################
28              
29             # set up before_template hook to make the menu dynamic using "active" property
30             sub BUILD {
31 1     1 0 72 my $s = shift;
32              
33             $s->app->add_hook (Dancer2::Core::Hook->new (
34             name => 'before_template',
35             code => sub {
36              
37             # reset or init the trees
38 2 100   2   44176 $s->has_clean_tree ? $s->tree(dclone $s->clean_tree)
39             : $s->clean_tree(dclone $s->tree);
40              
41             # set active menu items
42 2         6 my $tokens = shift;
43 2         10 my @segments = split /\//, $tokens->{request}->route->spec_route;
44 2         18 shift @segments; # get rid of blank segment
45 2         8 my $tree = $s->tree->{'/'};
46 2         5 foreach my $segment (@segments) {
47 6         14 $tree->{children}{$segment}{active} = 1;
48 6         19 $tree = $tree->{children}{$segment};
49             }
50              
51             # tear down and regenerate html and send to template
52 2         17 $s->html( HTML::Element->new('ul') );
53 2         92 _get_menu($s->tree->{'/'}, $s->html);
54 2         12 $tokens->{menu} = $s->html->as_HTML('', "\t", {});
55             }
56 1         28 ));
57             }
58              
59             # init the tree; called for each route wrapped in the menu_item keyword
60             sub menu_item {
61 4     4 1 7932 my ($s, $xt_data, $route) = @_;
62 4         31 my @segments = split /\//, $route->spec_route;
63 4         12 my $tree = $s->tree;
64 4         8 $segments[0] = '/'; # replace blank segment with root segment
65              
66             # add the path segments and associated data to our tree
67 4         13 while (my $segment = shift @segments) {
68 14         31 my $title = ucfirst($segment);
69 14         19 my $weight = 5;
70 14   33     29 $xt_data->{title} //= $title;
71 14         38 print Dumper $xt_data->{title};
72 14   33     901 $xt_data->{weight} //= $weight;
73              
74             # add xt_data to existig terminal segments, grow the tree otherwise
75 14 100 33     120 if (!@segments && ($s->tree->{$segment} || !$tree->{$segment}{children})) {
    100 66        
      100        
76 4         9 $tree->{$segment} = $xt_data;
77 4         9 $tree->{$segment}{protected} = 1; # cannot be changed by a different route
78             } elsif (!$s->tree->{$segment} && !$tree->{$segment}{children}) {
79 4         10 $tree->{$segment}{children} = {};
80             }
81              
82             # add menu item data to non-protected items
83 14 100       33 if (!$tree->{$segment}{protected}) {
84 6 50       14 ($title, $weight) = ($xt_data->{title}, $xt_data->{weight}) if !@segments;
85 6         13 $tree->{$segment}{title} = $title;
86 6         12 $tree->{$segment}{weight} = $weight;
87 6         11 $tree->{$segment}{protected} = !@segments;
88             }
89 14         59 $tree = $tree->{$segment}{children};
90             }
91             }
92              
93             # generate the HTML based on the contents of the tree
94             sub _get_menu {
95 10     10   18 my ($tree, $element) = @_;
96              
97             # sort sibling children menu items by weight and then by name
98 10         16 foreach my $child (
99             sort { ( $tree->{children}{$a}{weight} <=> $tree->{children}{$b}{weight} )
100             || ( $tree->{children}{$a}{title} cmp $tree->{children}{$b}{title} )
101 2 0       12 } keys %{$tree->{children}} ) {
  10         37  
102              
103             # create menu item list element with classes for css styling
104 12         27 my $li_this = HTML::Element->new('li');
105 12 100       269 $li_this->attr(class => $tree->{children}{$child}{active} ? 'active' : '');
106              
107             # add HTML elements for menu item; recurse if menu item has children itself
108 12         219 $li_this->push_content($tree->{children}{$child}{title});
109 12 100       168 if ($tree->{children}{$child}{children}) {
110 8         20 my $ul = HTML::Element->new('ul');
111 8         163 $li_this->push_content($ul);
112 8         121 $element->push_content($li_this);
113 8         118 _get_menu($tree->{children}{$child}, $ul)
114             } else {
115 4         8 $element->push_content($li_this);
116             }
117             }
118 10         68 return $element;
119             }
120              
121             1; # Magic true value
122              
123             __END__