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.
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.
44 % foreach my $tier (@$tiers) {
46 % $onchange="onchange='${pre}select_change(this, $i)'"
47 % if $i < scalar(@$tiers) - 1;
49 NAME="<% $tier->{field} %>"
50 ID="<% $pre."select_".$i %>"
52 <% $tier->{multiple} ? 'MULTIPLE' : '' %>
55 % my $options = $tiers_by_key->[0]->{''};
56 % foreach ( sort keys %$options ) {
57 <OPTION VALUE="<%$_ |h%>"><% $options->{$_} |h%></OPTION>
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) {
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];
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');
90 o.text = next_options[next_key];
92 if ( next_key == '' ) {
93 select_next.add(o, select_next.options[0]); //insert at top
95 select_next.add(o, null); //append
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])
104 if ( i < <% scalar(@$tiers) - 1 %> ) {
105 <% $pre %>select_change(select_next, i);
109 <% $pre %>select_change(document.getElementById('<% $pre %>select_0'), 0);
113 my $pre = $opt{prefix} || '';
114 my $tiers = $opt{tiers} or die "no tiers defined";
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},
142 map { $_->$key => $_->$name_col } @{ $tier->{records} }
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;
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};
164 $tier->{by_key} = \%children_of;
167 $i = scalar(@$tiers) - 1;
168 $tiers->[$i]->{curr_value} ||= $opt{curr_value};
169 $tiers->[$i]->{field} ||= $opt{field};
171 # We expect the usual case to be $opt{curr_value}, i.e.
172 # current value in the last tier. So trace it backward.
174 my $curr_value = $tiers->[$i]->{curr_value};
175 last if !defined($curr_value);
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;
189 my $tiers_by_key = [ map { $_->{by_key} } @$tiers ];
190 my $curr_values = [ map { $_->{curr_value} || '' } @$tiers ];