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->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
132 # yes, "null", not "JSON". so instead, using JSON::XS explicity...
134 my $json = JSON::XS->new();
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},
163 map { $_->$key => $_->$name_col } @{ $tier->{records} }
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;
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};
185 $tier->{by_key} = \%children_of;
188 $i = scalar(@$tiers) - 1;
189 $tiers->[$i]->{curr_value} ||= $opt{curr_value};
190 $tiers->[$i]->{field} ||= $opt{field};
192 # We expect the usual case to be $opt{curr_value}, i.e.
193 # current value in the last tier. So trace it backward.
195 my $curr_value = $tiers->[$i]->{curr_value};
196 last if !defined($curr_value);
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;
209 my $tiers_by_key = [ map { $_->{by_key} } @$tiers ];
210 my $curr_values = [ map { $_->{curr_value} || '' } @$tiers ];