Merge branch 'master' of git.freeside.biz:/home/git/freeside
[freeside.git] / httemplate / elements / select-tiered.html
1 <%doc>
2 Usage:
3
4 <& /elements/select-tiered.html,
5   tiers       => [
6     { table => 'table1', ... }, # most select-table options are supported
7     { table => 'table2', ..., link_col = 't2num' }, # foreign key in table1
8   ],
9   prefix      => '', # to avoid name conflicts
10   curr_value  => 42, # in the last table
11   field       => 'fieldname', # NAME attribute of the last element
12 &>
13
14 This creates a group of SELECT elements (similar to select-table.html) for
15 drill-down navigation of data with one-to-many relationships.
16
17 'tiers' is required, and must be an arrayref of hashes, each describing one
18 tier of selection (from most general to most specific).  Each tier can 
19 contain the following:
20 - table, select, addl_from, hashref, extra_sql: as in FS::Record::qsearch.
21 - records, an arrayref of exact records.  Either this or "table" must be 
22   provided.
23 - field: the NAME attribute of the select element.  Optional.
24 - name_col: the column/method name to obtain the record's text label in the 
25   select element.
26 - value_col: the column/method name to obtain the record's value, which is
27   sent on form submission.  Defaults to the primary key.
28 - link_col: the column/method name to associate the record to the value_col
29   of a record in the previous table's value_col.  (That is, the foreign key.)
30 - empty_label: the label to use for an option with the logical meaning of 
31   "all of these" and a value of ''.
32 - curr_value: the currently selected value.  This will constrain the current
33   values of preceding tiers.
34 - multiple: set to true for a multiple-style selector.  This should work but 
35   isn't fully tested.
36 - after: an HTML string to be inserted after the select element, before 
37   the next one.  By default there's nothing between them.
38 - onchange: an additional javascript function to be called on change.
39
40 For convenience, "curr_value" and "field" can be passed as part of the 
41 main argument list, and will be applied to the last tier.
42
43 </%doc>
44 % $i = 0;
45 % foreach my $tier (@$tiers) {
46 %   my $onchange;
47 %   $onchange="${pre}select_change(this, $i)"
48 %     if $i < scalar(@$tiers) - 1;
49 %
50 %   $onchange .= ';'.$tier->{onchange}."(this, $i);"
51 %     if $tier->{onchange};
52 %
53 %   $onchange = "onchange='$onchange'" if $onchange;
54 <SELECT 
55   NAME="<% $tier->{field} %>"
56   ID="<% $pre."select_".$i %>"
57   <%$onchange%>
58   <% $tier->{multiple} ? 'MULTIPLE' : '' %>
59   >
60 %   if ( $i == 0 ) {
61 %     my $options = $tiers_by_key->[0]->{''};
62 %     #foreach ( sort keys %$options ) {
63 %     foreach ( sort { lc($options->{$a}) cmp lc($options->{$b}) }
64 %                 keys %$options
65 %             )
66 %     {
67   <OPTION VALUE="<%$_ |h%>" <% $curr_values->[$i] eq $_ ? 'SELECTED' : ''%>>
68   <% $options->{$_} |h%></OPTION>
69 %     }
70 %   }
71 %   $i++;
72 </SELECT>
73 <% $tier->{after} %>
74 % } #foreach $tier
75
76 <SCRIPT type="text/javascript">
77 var <% $pre %>tiers = <% $json->encode($tiers_by_key) %>;
78 var <% $pre %>curr_values = <% $json->encode($curr_values) %>;
79 function <% $pre %>select_change(select_this, i) {
80
81   i++; // operate on the next tier selector
82   var next_options = new Object; // use like a hash
83   // slight hack here: empty_label implies not multiple, so if the 'all'
84   // option is selected, it will be the "value" property of the select.
85   var all = (select_this.value == '');
86   // combine all of the options of this one
87   for (var j = 0; j < select_this.options.length; j++) {
88     var this_opt = select_this.options[j];
89     if ( this_opt.selected || all ) {
90       for (var next_key in <% $pre %>tiers[i][this_opt.value]) {
91         next_options[next_key] = <% $pre %>tiers[i][this_opt.value][next_key];
92       } // for next_key
93     } // if selected
94   } // for this_opt
95
96   var select_next = document.getElementById('<% $pre."select_" %>' + i);
97   select_next.options.length = 0; // clear it
98   for (var next_key in next_options) {
99     var o = document.createElement('OPTION');
100     o.value = next_key;
101     o.text = next_options[next_key];
102
103     if ( next_key == '' ) {
104       select_next.add(o, select_next.options[0]); //insert at top
105     } else {
106       select_next.add(o, null); //append
107     }
108     // then select it if we're selecting them all, or if it's the only one,
109     // or if it's the current value at that tier
110     o.selected = select_next.multiple
111               || (next_options.length == 1)
112               || (next_key == <% $pre %>curr_values[i])
113               ;
114   }
115   if ( i < <% scalar(@$tiers) - 1 %> ) {
116     <% $pre %>select_change(select_next, i);
117   }
118   return;
119 }
120 <% $pre %>select_change(document.getElementById('<% $pre %>select_0'), 0);
121 </SCRIPT>
122 <%init>
123 my %opt = @_;
124 my $pre = $opt{prefix} || '';
125 my $tiers = $opt{tiers} or die "no tiers defined";
126
127 #my $json = JSON->new()->canonical(); #sort
128 # something super weird and broken going on with JSON's auto-loading, just
129 # using JSON alone errors out with
130 #   Can't locate object method "new" via package "null" (perhaps you forgot to
131 #   load "null"?)
132 # yes, "null", not "JSON".  so instead, using JSON::XS explicity...
133 use JSON::XS;
134 my $json = JSON::XS->new();
135 $json->canonical;
136
137 my $i;
138 for( $i = 0; $i < @$tiers; $i++ ) {
139   my $tier = $tiers->[$i];
140   my $key = $tier->{value_col};
141   my $name_col = $tier->{name_col};
142   if ( !exists($tier->{records}) ) {
143     # minor false laziness w/ select-table
144     my $dbdef_table = dbdef->table($tier->{table})
145       or die "can't find dbdef for ".$tier->{table}." table\n";
146     $key ||= $dbdef_table->primary_key;
147     my $hashref = $tier->{hashref} || {};
148     my $select = $tier->{select} || '*';
149     # we don't yet support agent_virt
150     $tier->{records} = [ qsearch({
151         'select'    => $select, # the real magic
152         'table'     => $tier->{table},
153         'addl_from' => $tier->{addl_from},
154         'hashref'   => $hashref,
155         'extra_sql' => $tier->{extra_sql},
156     }) ];
157   }
158
159   # set up options
160   my %children_of;
161   if ( $i == 0 ) {
162     $children_of{''} = {
163       map { $_->$key => $_->$name_col } @{ $tier->{records} }
164     };
165   }
166   else {
167     my $link_col = $tier->{link_col} 
168       or die "no link_col in '".$tier->{table}."' tier\n";
169     # %children_of maps the option values in the previous tier 
170     # to hashes of their linked options in this tier. 
171     foreach my $rec (@{ $tier->{records} }) {
172       $children_of{ $rec->$link_col } ||= {};
173       $children_of{ $rec->$link_col }->{ $rec->$key } = $rec->$name_col;
174     }
175   }
176
177   if ( defined $tier->{empty_label} ) {
178     foreach my $key (keys %children_of) {
179       # only create "all" options if there are multiple choices
180       if ( scalar(keys %{ $children_of{$key} }) > 1 ) {
181         $children_of{$key}->{''} = $tier->{empty_label};
182       }
183     }
184   }
185   $tier->{by_key} = \%children_of;
186 }
187
188 $i = scalar(@$tiers) - 1;
189 $tiers->[$i]->{curr_value} ||= $opt{curr_value};
190 $tiers->[$i]->{field} ||= $opt{field};
191
192 # We expect the usual case to be $opt{curr_value}, i.e.
193 # current value in the last tier.  So trace it backward.
194 while($i >= 1) {
195   my $curr_value = $tiers->[$i]->{curr_value};
196   last if !defined($curr_value);
197
198   my $tier = $tiers->[$i];
199   foreach my $key ( %{ $tier->{by_key} } ) {
200     my $options = $tier->{by_key}->{$key};
201     if ( exists( $options->{$curr_value} ) ) {
202       $tiers->[$i-1]->{curr_value} = $key;
203       last;
204     }
205   }
206   $i--;
207 }
208
209 my $tiers_by_key = [ map { $_->{by_key} } @$tiers ];
210 my $curr_values = [ map { $_->{curr_value} || '' } @$tiers ];
211 </%init>