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>
71 <SCRIPT type="text/javascript">
72 % my $json = JSON->new->canonical; #sort
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";
124 for( $i = 0; $i < @$tiers; $i++ ) {
125 my $tier = $tiers->[$i];
126 my $key = $tier->{value_col};
127 my $name_col = $tier->{name_col};
128 if ( !exists($tier->{records}) ) {
129 # minor false laziness w/ select-table
130 my $dbdef_table = dbdef->table($tier->{table})
131 or die "can't find dbdef for ".$tier->{table}." table\n";
132 $key ||= $dbdef_table->primary_key;
133 my $hashref = $tier->{hashref} || {};
134 my $select = $tier->{select} || '*';
135 # we don't yet support agent_virt
136 $tier->{records} = [ qsearch({
137 'select' => $select, # the real magic
138 'table' => $tier->{table},
139 'addl_from' => $tier->{addl_from},
140 'hashref' => $hashref,
141 'extra_sql' => $tier->{extra_sql},
149 map { $_->$key => $_->$name_col } @{ $tier->{records} }
153 my $link_col = $tier->{link_col}
154 or die "no link_col in '".$tier->{table}."' tier\n";
155 # %children_of maps the option values in the previous tier
156 # to hashes of their linked options in this tier.
157 foreach my $rec (@{ $tier->{records} }) {
158 $children_of{ $rec->$link_col } ||= {};
159 $children_of{ $rec->$link_col }->{ $rec->$key } = $rec->$name_col;
163 if ( defined $tier->{empty_label} ) {
164 foreach my $key (keys %children_of) {
165 # only create "all" options if there are multiple choices
166 if ( scalar(keys %{ $children_of{$key} }) > 1 ) {
167 $children_of{$key}->{''} = $tier->{empty_label};
171 $tier->{by_key} = \%children_of;
174 $i = scalar(@$tiers) - 1;
175 $tiers->[$i]->{curr_value} ||= $opt{curr_value};
176 $tiers->[$i]->{field} ||= $opt{field};
178 # We expect the usual case to be $opt{curr_value}, i.e.
179 # current value in the last tier. So trace it backward.
181 my $curr_value = $tiers->[$i]->{curr_value};
182 last if !defined($curr_value);
184 my $tier = $tiers->[$i];
185 foreach my $key ( %{ $tier->{by_key} } ) {
186 my $options = $tier->{by_key}->{$key};
187 if ( exists( $options->{$curr_value} ) ) {
188 $tiers->[$i-1]->{curr_value} = $key;
195 my $tiers_by_key = [ map { $_->{by_key} } @$tiers ];
196 my $curr_values = [ map { $_->{curr_value} || '' } @$tiers ];