svc_hardware revision number, #16266
[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
39 For convenience, "curr_value" and "field" can be passed as part of the 
40 main argument list, and will be applied to the last tier.
41
42 </%doc>
43 % $i = 0;
44 % foreach my $tier (@$tiers) {
45 %   my $onchange;
46 %   $onchange="onchange='${pre}select_change(this, $i)'"
47 %     if $i < scalar(@$tiers) - 1;
48 <SELECT 
49   NAME="<% $tier->{field} %>"
50   ID="<% $pre."select_".$i %>"
51   <%$onchange%>
52   <% $tier->{multiple} ? 'MULTIPLE' : '' %>
53   >
54 %   if ( $i == 0 ) {
55 %     my $options = $tiers_by_key->[0]->{''};
56 %     foreach ( sort keys %$options ) {
57   <OPTION VALUE="<%$_ |h%>"><% $options->{$_} |h%></OPTION>
58 %     }
59 %   }
60 %   $i++;
61 </SELECT>
62 <% $tier->{after} %>
63 % } #foreach $tier
64 <SCRIPT type="text/javascript">
65 % my $json = JSON->new->canonical; #sort
66 var <% $pre %>tiers = <% $json->encode($tiers_by_key) %>;
67 var <% $pre %>curr_values = <% $json->encode($curr_values) %>;
68 function <% $pre %>select_change(select_this, i) {
69
70   i++; // operate on the next tier selector
71   var next_options = new Object; // use like a hash
72   // slight hack here: empty_label implies not multiple, so if the 'all'
73   // option is selected, it will be the "value" property of the select.
74   var all = (select_this.value == '');
75   // combine all of the options of this one
76   for (var j = 0; j < select_this.options.length; j++) {
77     var this_opt = select_this.options[j];
78     if ( this_opt.selected || all ) {
79       for (var next_key in <% $pre %>tiers[i][this_opt.value]) {
80         next_options[next_key] = <% $pre %>tiers[i][this_opt.value][next_key];
81       } // for next_key
82     } // if selected
83   } // for this_opt
84
85   var select_next = document.getElementById('<% $pre."select_" %>' + i);
86   select_next.options.length = 0; // clear it
87   for (var next_key in next_options) {
88     var o = document.createElement('OPTION');
89     o.value = next_key;
90     o.text = next_options[next_key];
91
92     if ( next_key == '' ) {
93       select_next.add(o, select_next.options[0]); //insert at top
94     } else {
95       select_next.add(o, null); //append
96     }
97     // then select it if we're selecting them all, or if it's the only one,
98     // or if it's the current value at that tier
99     o.selected = select_next.multiple
100               || (next_options.length == 1)
101               || (next_key == <% $pre %>curr_values[i])
102               ;
103   }
104   if ( i < <% scalar(@$tiers) - 1 %> ) {
105     <% $pre %>select_change(select_next, i);
106   }
107   return;
108 }
109 <% $pre %>select_change(document.getElementById('<% $pre %>select_0'), 0);
110 </SCRIPT>
111 <%init>
112 my %opt = @_;
113 my $pre = $opt{prefix} || '';
114 my $tiers = $opt{tiers} or die "no tiers defined";
115
116 my $i;
117 for( $i = 0; $i < @$tiers; $i++ ) {
118   my $tier = $tiers->[$i];
119   my $key = $tier->{value_col};
120   my $name_col = $tier->{name_col};
121   if ( !exists($tier->{records}) ) {
122     # minor false laziness w/ select-table
123     my $dbdef_table = dbdef->table($tier->{table})
124       or die "can't find dbdef for ".$tier->{table}." table\n";
125     $key ||= $dbdef_table->primary_key;
126     my $hashref = $tier->{hashref} || {};
127     my $select = $tier->{select} || '*';
128     # we don't yet support agent_virt
129     $tier->{records} = [ qsearch({
130         'select'    => $select, # the real magic
131         'table'     => $tier->{table},
132         'addl_from' => $tier->{addl_from},
133         'hashref'   => $hashref,
134         'extra_sql' => $tier->{extra_sql},
135     }) ];
136   }
137
138   # set up options
139   my %children_of;
140   if ( $i == 0 ) {
141     $children_of{''} = {
142       map { $_->$key => $_->$name_col } @{ $tier->{records} }
143     };
144   }
145   else {
146     my $link_col = $tier->{link_col} 
147       or die "no link_col in '".$tier->{table}."' tier\n";
148     # %children_of maps the option values in the previous tier 
149     # to hashes of their linked options in this tier. 
150     foreach my $rec (@{ $tier->{records} }) {
151       $children_of{ $rec->$link_col } ||= {};
152       $children_of{ $rec->$link_col }->{ $rec->$key } = $rec->$name_col;
153     }
154   }
155
156   if ( defined $tier->{empty_label} ) {
157     foreach my $key (keys %children_of) {
158       # only create "all" options if there are multiple choices
159       if ( scalar(keys %{ $children_of{$key} }) > 1 ) {
160         $children_of{$key}->{''} = $tier->{empty_label};
161       }
162     }
163   }
164   $tier->{by_key} = \%children_of;
165 }
166
167 $i = scalar(@$tiers) - 1;
168 $tiers->[$i]->{curr_value} ||= $opt{curr_value};
169 $tiers->[$i]->{field} ||= $opt{field};
170
171 # We expect the usual case to be $opt{curr_value}, i.e.
172 # current value in the last tier.  So trace it backward.
173 while($i >= 1) {
174   my $curr_value = $tiers->[$i]->{curr_value};
175   last if !defined($curr_value);
176
177   my $tier = $tiers->[$i];
178   foreach my $key ( %{ $tier->{by_key} } ) {
179     my $options = $tier->{by_key}->{$key};
180     if ( exists( $options->{$curr_value} ) ) {
181       warn "tier $i curr_value ($curr_value) found under key $key\n";
182       $tiers->[$i-1]->{curr_value} = $key;
183       last;
184     }
185   }
186   $i--;
187 }
188
189 my $tiers_by_key = [ map { $_->{by_key} } @$tiers ];
190 my $curr_values = [ map { $_->{curr_value} || '' } @$tiers ];
191 </%init>