4 <& /elements/select-tiered.html,
6 { table => 'table1', ... }, # most select-table options are supported
7 { table => 'table2', ..., link_col = 't2num' }, # foreign key in table1
9 prefix => '', # to avoid name conflicts
10 curr_value => 42, # in the last table
11 field => 'fieldname', # NAME attribute of the last element
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.
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
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
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
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.
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.
45 % foreach my $tier (@$tiers) {
47 % $onchange="${pre}select_change(this, $i)"
48 % if $i < scalar(@$tiers) - 1;
50 % $onchange .= ';'.$tier->{onchange}."(this, $i);"
51 % if $tier->{onchange};
53 % $onchange = "onchange='$onchange'" if $onchange;
55 NAME="<% $tier->{field} %>"
56 ID="<% $pre."select_".$i %>"
58 <% $tier->{multiple} ? 'MULTIPLE' : '' %>
61 % my $options = $tiers_by_key->[0]->{''};
62 % #foreach ( sort keys %$options ) {
63 % foreach ( sort { lc($options->{$a}) cmp lc($options->{$b}) }
67 <OPTION VALUE="<%$_ |h%>" <% $curr_values->[$i] eq $_ ? 'SELECTED' : ''%>>
68 <% $options->{$_} |h%></OPTION>
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) {
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];
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');
101 o.text = next_options[next_key];
103 if ( next_key == '' ) {
104 select_next.add(o, select_next.options[0]); //insert at top
106 select_next.add(o, null); //append
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])
115 if ( i < <% scalar(@$tiers) - 1 %> ) {
116 <% $pre %>select_change(select_next, i);
120 <% $pre %>select_change(document.getElementById('<% $pre %>select_0'), 0);
124 my $pre = $opt{prefix} || '';
125 my $tiers = $opt{tiers} or die "no tiers defined";
127 my $json = JSON::XS->new();
131 for( $i = 0; $i < @$tiers; $i++ ) {
132 my $tier = $tiers->[$i];
133 my $key = $tier->{value_col};
134 my $name_col = $tier->{name_col};
135 if ( !exists($tier->{records}) ) {
136 # minor false laziness w/ select-table
137 my $dbdef_table = dbdef->table($tier->{table})
138 or die "can't find dbdef for ".$tier->{table}." table\n";
139 $key ||= $dbdef_table->primary_key;
140 my $hashref = $tier->{hashref} || {};
141 my $select = $tier->{select} || '*';
142 # we don't yet support agent_virt
143 $tier->{records} = [ qsearch({
144 'select' => $select, # the real magic
145 'table' => $tier->{table},
146 'addl_from' => $tier->{addl_from},
147 'hashref' => $hashref,
148 'extra_sql' => $tier->{extra_sql},
156 map { $_->$key => $_->$name_col } @{ $tier->{records} }
160 my $link_col = $tier->{link_col}
161 or die "no link_col in '".$tier->{table}."' tier\n";
162 # %children_of maps the option values in the previous tier
163 # to hashes of their linked options in this tier.
164 foreach my $rec (@{ $tier->{records} }) {
165 $children_of{ $rec->$link_col } ||= {};
166 $children_of{ $rec->$link_col }->{ $rec->$key } = $rec->$name_col;
170 if ( defined $tier->{empty_label} ) {
171 foreach my $key (keys %children_of) {
172 # only create "all" options if there are multiple choices
173 if ( scalar(keys %{ $children_of{$key} }) > 1 ) {
174 $children_of{$key}->{''} = $tier->{empty_label};
177 # ensure that there's always at least one empty label
178 $children_of{''}->{''} = $tier->{empty_label};
180 $tier->{by_key} = \%children_of;
183 $i = scalar(@$tiers) - 1;
184 $tiers->[$i]->{curr_value} ||= $opt{curr_value};
185 $tiers->[$i]->{field} ||= $opt{field};
187 # We expect the usual case to be $opt{curr_value}, i.e.
188 # current value in the last tier. So trace it backward.
190 my $curr_value = $tiers->[$i]->{curr_value};
191 last if !defined($curr_value);
193 my $tier = $tiers->[$i];
194 foreach my $key ( %{ $tier->{by_key} } ) {
195 my $options = $tier->{by_key}->{$key};
196 if ( exists( $options->{$curr_value} ) ) {
197 $tiers->[$i-1]->{curr_value} = $key;
204 my $tiers_by_key = [ map { $_->{by_key} } @$tiers ];
205 my $curr_values = [ map { $_->{curr_value} || '' } @$tiers ];