Commit 80c6d150 authored by Max Kanat-Alexander's avatar Max Kanat-Alexander

Bug 636416: Use the standard value-controller javascript to control the

drop-down fields on the Advanced Search page. r=glob, a=mkanat
parent 93175c68
......@@ -517,44 +517,143 @@ function handleVisControllerValueChange(e, args) {
}
}
function showValueWhen(controlled_field_id, controlled_value_ids,
controller_field_id, controller_value_id)
/**
* This is a data structure representing the tree of controlled values.
* Let's call the "controller value" the "source" and the "controlled
* value" the "target". A target can have only one source, but a source
* can have an infinite number of targets.
*
* The data structure is a series of hash tables that go something
* like this:
*
* source_field -> target_field -> source_value_id -> target_value_ids
*
* We always know source_field when our event handler is called, since
* that's the field the event is being triggered on. We can then enumerate
* through every target field, check the status of each source field value,
* and act appropriately on each target value.
*/
var bz_value_controllers = {};
// This keeps track of whether or not we've added an onchange handler
// for the source field yet.
var bz_value_controller_has_handler = {};
function showValueWhen(target_field_id, target_value_ids,
source_field_id, source_value_id, empty_shows_all)
{
var controller_field = document.getElementById(controller_field_id);
// Note that we don't get an object for the controlled field here,
// because it might not yet exist in the DOM. We just pass along its id.
YAHOO.util.Event.addListener(controller_field, 'change',
handleValControllerChange, [controlled_field_id, controlled_value_ids,
controller_field, controller_value_id]);
if (!bz_value_controllers[source_field_id]) {
bz_value_controllers[source_field_id] = {};
}
if (!bz_value_controllers[source_field_id][target_field_id]) {
bz_value_controllers[source_field_id][target_field_id] = {};
}
var source_values = bz_value_controllers[source_field_id][target_field_id];
source_values[source_value_id] = target_value_ids;
if (!bz_value_controller_has_handler[source_field_id]) {
var source_field = document.getElementById(source_field_id);
YAHOO.util.Event.addListener(source_field, 'change',
handleValControllerChange, [source_field, empty_shows_all]);
bz_value_controller_has_handler[source_field_id] = true;
}
}
function handleValControllerChange(e, args) {
var controlled_field = document.getElementById(args[0]);
var controlled_value_ids = args[1];
var controller_field = args[2];
var controller_value_id = args[3];
var controller_item = document.getElementById(
_value_id(controller_field.id, controller_value_id));
for (var i = 0; i < controlled_value_ids.length; i++) {
var item = getPossiblyHiddenOption(controlled_field,
controlled_value_ids[i]);
if (item.disabled && controller_item && controller_item.selected) {
item = showOptionInIE(item, controlled_field);
YAHOO.util.Dom.removeClass(item, 'bz_hidden_option');
item.disabled = false;
var source = args[0];
var empty_shows_all = args[1];
for (var target_field_id in bz_value_controllers[source.id]) {
var target = document.getElementById(target_field_id);
if (!target) continue;
_update_displayed_values(source, target, empty_shows_all);
}
}
/* See the docs for bz_option_duplicate count lower down for an explanation
* of this data structure.
*/
var bz_option_hide_count = {};
function _update_displayed_values(source, target, empty_shows_all) {
var show_all = (empty_shows_all && source.selectedIndex == -1);
bz_option_hide_count[target.id] = {};
var source_values = bz_value_controllers[source.id][target.id];
for (source_value_id in source_values) {
var source_option = getPossiblyHiddenOption(source, source_value_id);
var target_values = source_values[source_value_id];
for (var i = 0; i < target_values.length; i++) {
var target_value_id = target_values[i];
_handle_source_target(source_option, target, target_value_id,
show_all);
}
else if (!item.disabled) {
YAHOO.util.Dom.addClass(item, 'bz_hidden_option');
if (item.selected) {
item.selected = false;
bz_fireEvent(controlled_field, 'change');
}
item.disabled = true;
hideOptionInIE(item, controlled_field);
}
// We may have updated which elements are selected or not selected
// in the target field, and it may have handlers associated with
// that, so we need to fire the change event on the target.
bz_fireEvent(target, 'change');
}
function _handle_source_target(source_option, target, target_value_id,
show_all)
{
var target_option = getPossiblyHiddenOption(target, target_value_id);
// We always call either _show_option or _hide_option on every single
// target value. Although this is not theoretically the most efficient
// thing we can do, it handles all possible edge cases, and there are
// a lot of those, particularly when this code is being used on the
// search form.
if (source_option.selected || (show_all && !source_option.disabled)) {
_show_option(target_option, target);
}
else {
_hide_option(target_option, target);
}
}
/* When an option has duplicates (see the docs for bz_option_duplicates
* lower down in this file), we only want to hide it if *all* the duplicates
* would be hidden. So we keep a counter of how many duplicates each option
* has. Then, when we run through a "change" call for a source field,
* we count how many times each value gets hidden, and only actually
* hide it if the counter hits a number higher than the duplicate count.
*/
var bz_option_duplicate_count = {};
function _show_option(option, field) {
if (!option.disabled) return;
option = showOptionInIE(option, field);
YAHOO.util.Dom.removeClass(option, 'bz_hidden_option');
option.disabled = false;
}
function _hide_option(option, field) {
if (option.disabled) return;
var value_id = option.bz_value_id;
if (field.id in bz_option_duplicate_count
&& value_id in bz_option_duplicate_count[field.id])
{
if (!bz_option_hide_count[field.id][value_id]) {
bz_option_hide_count[field.id][value_id] = 0;
}
bz_option_hide_count[field.id][value_id]++;
var current = bz_option_hide_count[field.id][value_id];
var dups = bz_option_duplicate_count[field.id][value_id];
// We check <= because the value in bz_option_duplicate_count is
// 1 less than the total number of duplicates (since the shown
// option is also a "duplicate" but not counted in
// bz_option_duplicate_count).
if (current <= dups) return;
}
YAHOO.util.Dom.addClass(option, 'bz_hidden_option');
option.selected = false;
option.disabled = true;
hideOptionInIE(option, field);
}
// A convenience function to generate the "id" tag of an <option>
......@@ -571,7 +670,7 @@ function _value_id(field_name, id) {
* on <option> tags. However, you *can* insert a Comment Node as a
* child of a <select> tag. So we just insert a Comment where the <option>
* used to be. */
var ie_hidden_options = new Array();
var ie_hidden_options = {};
function hideOptionInIE(anOption, aSelect) {
if (browserCanHideOptions(aSelect)) return;
......@@ -591,7 +690,7 @@ function hideOptionInIE(anOption, aSelect) {
// Store the comment node for quick access for getPossiblyHiddenOption
if (!ie_hidden_options[aSelect.id]) {
ie_hidden_options[aSelect.id] = new Array();
ie_hidden_options[aSelect.id] = {};
}
ie_hidden_options[aSelect.id][anOption.id] = commentNode;
}
......@@ -620,6 +719,7 @@ function showOptionInIE(aNode, aSelect) {
function initHidingOptionsForIE(select_name) {
var aSelect = document.getElementById(select_name);
if (browserCanHideOptions(aSelect)) return;
if (!aSelect) return;
for (var i = 0; ;i++) {
var item = aSelect.options[i];
......@@ -631,7 +731,27 @@ function initHidingOptionsForIE(select_name) {
}
}
/* Certain fields, like the Component field, have duplicate values in
* them (the same name, but different ids). We don't display these
* duplicate values in the UI, but the option hiding/showing code still
* uses the ids of these unshown duplicates. So, whenever we get the
* id of an unshown duplicate in getPossiblyHiddenOption, we have to
* return the actually-used <option> instead.
*
* The structure of the data looks like:
*
* field_name -> unshown_value_id -> shown_value_id_it_is_a_duplicate_of
*/
var bz_option_duplicates = {};
function getPossiblyHiddenOption(aSelect, optionId) {
if (bz_option_duplicates[aSelect.id]
&& bz_option_duplicates[aSelect.id][optionId])
{
optionId = bz_option_duplicates[aSelect.id][optionId];
}
// Works always for <option> tags, and works for commentNodes
// in IE (but not in Webkit).
var id = _value_id(aSelect.id, optionId);
......@@ -643,6 +763,10 @@ function getPossiblyHiddenOption(aSelect, optionId) {
val = ie_hidden_options[aSelect.id][id];
}
// We add this property for our own convenience, it's used in
// other places.
val.bz_value_id = optionId;
return val;
}
......
......@@ -220,6 +220,34 @@ function bz_valueSelected(aSelect, aValue) {
}
/**
* Returns all Option elements that are selected in a <select>,
* as an array. Returns an empty array if nothing is selected.
*
* @param aSelect The select you want the selected values of.
*/
function bz_selectedOptions(aSelect) {
// HTML 5
if (aSelect.selectedOptions) {
return aSelect.selectedOptions;
}
var start_at = aSelect.selectedIndex;
if (start_at == -1) return [];
var first_selected = aSelect.options[start_at];
if (!aSelect.multiple) return first_selected;
// selectedIndex is specified as being the "first selected item",
// so we can start from there.
var selected = [first_selected];
var options_length = aSelect.options.length;
// We start after first_selected
for (var i = start_at + 1; i < options_length; i++) {
var this_option = aSelect.options[i];
if (this_option.selected) selected.push(this_option);
}
return selected;
}
/**
* Tells you where (what index) in a <select> a particular option is.
* Returns -1 if the value is not in the <select>
*
......
......@@ -40,6 +40,46 @@ use Bugzilla::Keyword;
use Bugzilla::Field;
use Bugzilla::Install::Util qw(vers_cmp);
###############
# Subroutines #
###############
sub get_product_values {
my ($products, $field, $vars) = @_;
my @all_values = map { @{ $_->$field } } @$products;
my (@unique, %duplicates, %duplicate_count, %seen);
foreach my $value (@all_values) {
my $lc_name = lc($value->name);
if ($seen{$lc_name}) {
$duplicate_count{$seen{$lc_name}->id}++;
$duplicates{$value->id} = $seen{$lc_name};
next;
}
push(@unique, $value);
$seen{$lc_name} = $value;
}
if ($field eq 'version') {
@unique = sort { vers_cmp(lc($a->name), lc($b->name)) } @unique;
}
else {
@unique = sort { lc($a->name) cmp lc($b->name) } @unique;
}
$field =~ s/s$//;
$field = 'target_milestone' if $field eq 'milestone';
$vars->{duplicates}->{$field} = \%duplicates;
$vars->{duplicate_count}->{$field} = \%duplicate_count;
# "component" is a reserved word in Template Toolkit.
$field = 'component_' if $field eq 'component';
$vars->{$field} = \@unique;
}
###############
# Main Script #
###############
my $cgi = Bugzilla->cgi;
my $dbh = Bugzilla->dbh;
my $template = Bugzilla->template;
......@@ -133,38 +173,18 @@ if (!PrefillForm($buffer)) {
my @selectable_products = sort {lc($a->name) cmp lc($b->name)}
@{$user->get_selectable_products};
Bugzilla::Product::preload(\@selectable_products);
$vars->{'product'} = \@selectable_products;
# Create the component, version and milestone lists.
my %components;
my %versions;
my %milestones;
foreach my $product (@selectable_products) {
$components{$_->name} = 1 foreach (@{$product->components});
$versions{$_->name} = 1 foreach (@{$product->versions});
$milestones{$_->name} = 1 foreach (@{$product->milestones});
foreach my $field (qw(components versions milestones)) {
get_product_values(\@selectable_products, $field, $vars);
}
my @components = sort(keys %components);
my @versions = sort { vers_cmp (lc($a), lc($b)) } keys %versions;
my @milestones = sort(keys %milestones);
$vars->{'product'} = \@selectable_products;
# Create data structures representing each classification
if (Bugzilla->params->{'useclassification'}) {
$vars->{'classification'} = $user->get_selectable_classifications;
}
# We use 'component_' because 'component' is a Template Toolkit reserved word.
$vars->{'component_'} = \@components;
$vars->{'version'} = \@versions;
if (Bugzilla->params->{'usetargetmilestone'}) {
$vars->{'target_milestone'} = \@milestones;
}
my @chfields;
push @chfields, "[Bug creation]";
......
......@@ -121,22 +121,71 @@
[% legal_values = ${"component_"} %]
[% END %]
[% FOREACH current_value = legal_values %]
[% IF current_value.id %]
[%# current_value is a hash instead of a value which
only applies for Resolution really, everywhere else current_value
is just the value %]
[% v = current_value.name OR '---' -%]
<option value="[% v FILTER html %]"
[% ' selected="selected"' IF value.contains( v ) %]>
[% display_value(field.name, current_value.name) FILTER html %]
</option>
[% ELSE %]
<option value="[% current_value OR '---' FILTER html %]"
[% ' selected="selected"' IF value.contains( current_value ) %]>
[% display_value(field.name, current_value) FILTER html %]
</option>
[% END %]
[% SET v = current_value.name OR '---' -%]
[% SET display = display_value(field.name, current_value.name) %]
<option [% IF v != display %]value="[% v FILTER html %]"[% END ~%]
id="v[% current_value.id FILTER html %]_[% field.name FILTER html %]"
[% ' selected="selected"' IF value.contains( v ) %]>
[%~ display FILTER html ~%]
</option>
[% END %]
</select>
</div>
[% IF value_controllers.${field.name}.defined %]
<script type="text/javascript"><!--
[%+ FILTER collapse %]
[% FOREACH accessor = value_controllers.${field.name}.keys %]
[% PROCESS controller_js %]
[% END %]
[%~ END ~%]
// --></script>
[% END %]
[% IF duplicates.${field.name}.keys.size %]
[% SET field_dups = duplicates.${field.name} %]
[% SET dup_counts = duplicate_count.${field.name} %]
<script type="text/javascript">
[%+ FILTER collapse %]
bz_option_duplicates['[% field.name FILTER js %]'] = {
[% FOREACH dup = field_dups.keys %]
[% dup FILTER js %]:[% field_dups.$dup.id FILTER js %]
[%~ ',' UNLESS loop.last %]
[% END ~%]
};
bz_option_duplicate_count['[% field.name FILTER js %]'] = {
[% FOREACH dup_target = dup_counts.keys %]
[% dup_target FILTER js %]:[% dup_counts.$dup_target %]
[%~ ',' UNLESS loop.last %]
[% END %]
};
[% END %]
</script>
[% END %]
[% END %]
[%# END OF SWITCH %]
[% BLOCK controller_js %]
[%# If there are selected values already, we need to fire the
# "change" event once the page has loaded, so we can set all
# the values in all the other <select>s properly.
#%]
YAHOO.util.Event.onDOMReady(function() {
var field = document.getElementById('[% field.name FILTER js %]');
if (field.selectedIndex != -1) bz_fireEvent(field, 'change');
});
[% SET sub_field = value_controllers.${field.name}.$accessor %]
[% FOREACH legal_value = legal_values %]
[% SET controlled_ids = [] %]
[% FOREACH sub_value = legal_value.$accessor %]
[% controlled_ids.push(sub_value.id) %]
[% END %]
[% NEXT IF !controlled_ids.size %]
showValueWhen('[% sub_field.name FILTER js %]',
[[% controlled_ids.join(',') FILTER js %]],
'[% field.name FILTER js %]',
[% legal_value.id FILTER js %],
true);
[% END %]
[% END %]
......@@ -25,85 +25,6 @@
<script type="text/javascript">
var first_load = true; [%# is this the first time we load the page? %]
var last_sel = new Array(); [%# caches last selection %]
[% IF Param('useclassification') %]
var useclassification = true;
var prods = new Array();
[% ELSE %]
var useclassification = false;
[% END %]
var cpts = new Array();
var vers = new Array();
[% IF Param('usetargetmilestone') %]
var tms = new Array();
[% END %]
[%# Create an array of products, indexed by the classification #%]
[% nclass = 0 %]
[% FOREACH c = classification %]
prods[[% nclass FILTER js %]] = [
[% sep = '' %]
[%- FOREACH item = user.get_selectable_products(c.id) -%]
[%- IF item.components.size -%]
[%- sep FILTER js %]'[% item.name FILTER js %]'
[%- sep = ',' -%]
[%- END -%]
[%- END -%] ];
[% nclass = nclass+1 %]
[% END %]
[%# Create three arrays of components, versions and target milestones, indexed
# numerically according to the product they refer to. #%]
[% n = 0 %]
[% FOREACH p = product %]
[% NEXT IF NOT p.components.size %]
[% IF Param('useclassification') %]
prods['[% p.name FILTER js %]'] = [% n %]
[% END %]
cpts[[% n %]] = [
[%- FOREACH item = p.components %]'[% item.name FILTER js %]'[% ", " UNLESS loop.last %] [%- END -%] ];
vers[[% n %]] = [
[%- FOREACH item = p.versions -%]'[% item.name FILTER js %]'[% ", " UNLESS loop.last %] [%- END -%] ];
[% IF Param('usetargetmilestone') %]
tms[[% n %]] = [
[%- FOREACH item = p.milestones %]'[% item.name FILTER js %]'[% ", " UNLESS loop.last %] [%- END -%] ];
[% END %]
[% n = n+1 %]
[% END %]
/*
* doOnSelectProduct determines which selection should get updated
*
* - selectmode = 0 - init
* selectmode = 1 - classification selected
* selectmode = 2 - product selected
*
* globals:
* queryform - string holding the name of the selection form
*/
function doOnSelectProduct(selectmode) {
var f = document.forms[queryform];
var milestone = (typeof(f.target_milestone) == "undefined" ?
null : f.target_milestone);
if (selectmode == 0) {
// If there is no classification selected, give us a chance to fill
// the select fields with values from the possibly selected product.
if (useclassification && f.classification.selectedIndex > -1) {
selectClassification(f.classification, f.product, f.component, f.version, milestone);
} else {
selectProduct(f.product, f.component, f.version, milestone, null);
}
} else if (selectmode == 1) {
selectClassification(f.classification, f.product, f.component, f.version, milestone);
} else {
selectProduct(f.product, f.component, f.version, milestone, null);
}
}
// Hide the Advanced Fields by default, unless the user has a cookie
// that specifies otherwise.
// &#9656; and &#9662; are both utf8 escaped characters for right
......@@ -143,7 +64,7 @@ TUI_hide_default('information_query');
accesskey = "s"
%]
<script type="text/javascript"> <!--
document.forms[queryform].short_desc.focus();
document.getElementById('short_desc').focus();
// -->
</script>
......@@ -154,23 +75,26 @@ TUI_hide_default('information_query');
</div>
[%# *** Classification Product Component *** %]
[% value_controllers = {
'classification' => { 'products' => bug_fields.product },
'product' => { 'components' => bug_fields.component,
'versions' => bug_fields.version,
'milestones' => bug_fields.target_milestone },
} %]
[% Hook.process('before_selects_top') %]
[% IF Param('useclassification') %]
[% fake_classfication = { name => bug_fields.classification.name,
type => constants.FIELD_TYPE_SINGLE_SELECT } %]
[% INCLUDE "search/field.html.tmpl"
field => fake_classfication
accesskey => "c"
onchange => "doOnSelectProduct(1);"
value => default.classification
%]
[% INCLUDE "search/field.html.tmpl"
field => bug_fields.classification
accesskey => "c"
value => default.classification
%]
[% END %]
[% INCLUDE "search/field.html.tmpl"
field => bug_fields.product
accesskey => "p"
onchange => "doOnSelectProduct(2);"
value => default.product
%]
[% INCLUDE "search/field.html.tmpl"
......
......@@ -30,21 +30,12 @@
[% cgi = Bugzilla.cgi %]
[% js_data = BLOCK %]
var queryform = "queryform"
[% END %]
[% PROCESS global/header.html.tmpl
title = "Search for $terms.bugs"
onload = "doOnSelectProduct(0);"
javascript = js_data
yui = [ 'autocomplete', 'calendar' ]
javascript_urls = [ "js/productform.js", "js/util.js", "js/TUI.js", "js/field.js"]
javascript_urls = [ "js/util.js", "js/TUI.js", "js/field.js"]
style_urls = [ "skins/standard/search_form.css" ]
doc_section = "query.html"
style = "dl.bug_changes dt {
margin-top: 15px;
}"
%]
[% WRAPPER search/tabs.html.tmpl %]
......@@ -63,7 +54,6 @@ var queryform = "queryform"
</form>
[% END %]
[% PROCESS global/footer.html.tmpl %]
......@@ -35,7 +35,7 @@
onload = "doOnSelectProduct(0);"
yui = [ 'autocomplete', 'calendar' ]
javascript = js_data
javascript_urls = [ "js/util.js", "js/productform.js", "js/TUI.js", "js/field.js" ]
javascript_urls = [ "js/util.js", "js/TUI.js", "js/field.js" ]
style_urls = [ "skins/standard/search_form.css" ]
doc_section = "reporting.html#charts-new-series"
%]
......
......@@ -34,7 +34,7 @@ var queryform = "reportform"
onload = "doOnSelectProduct(0); chartTypeChanged()"
yui = [ 'autocomplete', 'calendar' ]
javascript = js_data
javascript_urls = [ "js/util.js", "js/productform.js", "js/TUI.js", "js/field.js" ]
javascript_urls = [ "js/util.js", "js/TUI.js", "js/field.js" ]
style_urls = [ "skins/standard/search_form.css" ]
doc_section = "reporting.html#reports"
%]
......
......@@ -34,7 +34,7 @@ var queryform = "reportform"
onload = "doOnSelectProduct(0)"
yui = [ 'autocomplete', 'calendar' ]
javascript = js_data
javascript_urls = [ "js/util.js", "js/productform.js", "js/TUI.js", "js/field.js" ]
javascript_urls = [ "js/util.js", "js/TUI.js", "js/field.js" ]
style_urls = [ "skins/standard/search_form.css" ]
doc_section = "reporting.html#reports"
%]
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment