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 <OPTION VALUE="<%$_ |h%>" <% $curr_values->[$i] eq $_ ? 'SELECTED' : ''%>>
64 <% $options->{$_} |h%></OPTION>
72 <SCRIPT type="text/javascript">
73 var <% $pre %>tiers = <% $json->encode($tiers_by_key) %>;
74 var <% $pre %>curr_values = <% $json->encode($curr_values) %>;
75 function <% $pre %>select_change(select_this, i) {
77 i++; // operate on the next tier selector
78 var next_options = new Object; // use like a hash
79 // slight hack here: empty_label implies not multiple, so if the 'all'
80 // option is selected, it will be the "value" property of the select.
81 var all = (select_this.value == '');
82 // combine all of the options of this one
83 for (var j = 0; j < select_this.options.length; j++) {
84 var this_opt = select_this.options[j];
85 if ( this_opt.selected || all ) {
86 for (var next_key in <% $pre %>tiers[i][this_opt.value]) {
87 next_options[next_key] = <% $pre %>tiers[i][this_opt.value][next_key];
92 var select_next = document.getElementById('<% $pre."select_" %>' + i);
93 select_next.options.length = 0; // clear it
94 for (var next_key in next_options) {
95 var o = document.createElement('OPTION');
97 o.text = next_options[next_key];
99 if ( next_key == '' ) {
100 select_next.add(o, select_next.options[0]); //insert at top
102 select_next.add(o, null); //append
104 // then select it if we're selecting them all, or if it's the only one,
105 // or if it's the current value at that tier
106 o.selected = select_next.multiple
107 || (next_options.length == 1)
108 || (next_key == <% $pre %>curr_values[i])
111 if ( i < <% scalar(@$tiers) - 1 %> ) {
112 <% $pre %>select_change(select_next, i);
116 <% $pre %>select_change(document.getElementById('<% $pre %>select_0'), 0);
120 my $pre = $opt{prefix} || '';
121 my $tiers = $opt{tiers} or die "no tiers defined";
123 #my $json = JSON->new()->canonical(); #sort
124 # something super weird and broken going on with JSON's auto-loading, just
125 # using JSON alone errors out with
126 # Can't locate object method "new" via package "null" (perhaps you forgot to
128 # yes, "null", not "JSON". so instead, using JSON::XS explicity...
130 my $json = JSON::XS->new();
134 for( $i = 0; $i < @$tiers; $i++ ) {
135 my $tier = $tiers->[$i];
136 my $key = $tier->{value_col};
137 my $name_col = $tier->{name_col};
138 if ( !exists($tier->{records}) ) {
139 # minor false laziness w/ select-table
140 my $dbdef_table = dbdef->table($tier->{table})
141 or die "can't find dbdef for ".$tier->{table}." table\n";
142 $key ||= $dbdef_table->primary_key;
143 my $hashref = $tier->{hashref} || {};
144 my $select = $tier->{select} || '*';
145 # we don't yet support agent_virt
146 $tier->{records} = [ qsearch({
147 'select' => $select, # the real magic
148 'table' => $tier->{table},
149 'addl_from' => $tier->{addl_from},
150 'hashref' => $hashref,
151 'extra_sql' => $tier->{extra_sql},
159 map { $_->$key => $_->$name_col } @{ $tier->{records} }
163 my $link_col = $tier->{link_col}
164 or die "no link_col in '".$tier->{table}."' tier\n";
165 # %children_of maps the option values in the previous tier
166 # to hashes of their linked options in this tier.
167 foreach my $rec (@{ $tier->{records} }) {
168 $children_of{ $rec->$link_col } ||= {};
169 $children_of{ $rec->$link_col }->{ $rec->$key } = $rec->$name_col;
173 if ( defined $tier->{empty_label} ) {
174 foreach my $key (keys %children_of) {
175 # only create "all" options if there are multiple choices
176 if ( scalar(keys %{ $children_of{$key} }) > 1 ) {
177 $children_of{$key}->{''} = $tier->{empty_label};
181 $tier->{by_key} = \%children_of;
184 $i = scalar(@$tiers) - 1;
185 $tiers->[$i]->{curr_value} ||= $opt{curr_value};
186 $tiers->[$i]->{field} ||= $opt{field};
188 # We expect the usual case to be $opt{curr_value}, i.e.
189 # current value in the last tier. So trace it backward.
191 my $curr_value = $tiers->[$i]->{curr_value};
192 last if !defined($curr_value);
194 my $tier = $tiers->[$i];
195 foreach my $key ( %{ $tier->{by_key} } ) {
196 my $options = $tier->{by_key}->{$key};
197 if ( exists( $options->{$curr_value} ) ) {
198 $tiers->[$i-1]->{curr_value} = $key;
205 my $tiers_by_key = [ map { $_->{by_key} } @$tiers ];
206 my $curr_values = [ map { $_->{curr_value} || '' } @$tiers ];