Fix for bug 96534: The version, component, and milestone select lists on the…

Fix for bug 96534: The version, component, and milestone select lists on the query page were slow to reflow when you changed the product in the first select list. This patch is a complete rewrite from scratch of the javascript used on query.cgi, which results in an approximately 700% speed increase in the reflow time when changing the selected product. Patch by Chris Lahey <clahey@ximian.com> and Christian Reis <kiko@async.com.br> Javascript changes r= caillon@returnzero.com, louie@ximian.com Perl changes r= jake@acutex.net, justdave@syndicomm.com
parent 155a9860
...@@ -21,6 +21,8 @@ ...@@ -21,6 +21,8 @@
# Contributor(s): Terry Weissman <terry@mozilla.org> # Contributor(s): Terry Weissman <terry@mozilla.org>
# David Gardiner <david.gardiner@unisa.edu.au> # David Gardiner <david.gardiner@unisa.edu.au>
# Matthias Radestock <matthias@sorted.org> # Matthias Radestock <matthias@sorted.org>
# Chris Lahey <clahey@ximian.com> [javascript fixes]
# Christian Reis <kiko@async.com.br> [javascript rewrite]
use diagnostics; use diagnostics;
use strict; use strict;
...@@ -324,202 +326,297 @@ foreach my $m (@::legal_target_milestone) { ...@@ -324,202 +326,297 @@ foreach my $m (@::legal_target_milestone) {
} }
} }
# javascript # SELECT box javascript handling. This is done to make the component,
# versions and milestone SELECTs repaint automatically when a product is
# selected. Refactored for bug 96534.
# make_js_array: iterates through the product array creating a
# javascript array keyed by product with an alphabetically ordered array
# for the corresponding elements in the components array passed in.
# return a string with javascript definitions for the product in a nice
# arrays which can be linearly appended later on.
# make_js_array ( \@products, \%[components/versions/milestones], $array )
sub make_js_array {
my @prods = @{$_[0]};
my %data = %{$_[1]};
my $arr = $_[2];
my $ret = "\nvar $arr = new Array();\n";
foreach my $p ( @prods ) {
# join each element with a "," case-insensitively alpha sorted
if ( $data{$p} ) {
$ret .= $arr."['$p'] = [";
# the SqlQuote() protects our 's.
my @tmp = map( SqlQuote( $_ ), @{ $data{$p} } );
# do the join on a sorted, quoted list
@tmp = sort { lc( $a ) cmp lc( $b ) } @tmp;
$ret .= join( ", ", @tmp );
$ret .= "];\n";
}
}
return $ret;
}
my $jscript = << 'ENDSCRIPT'; my $jscript = '<script language="JavaScript" type="text/javascript">';
<script language="Javascript1.1" type="text/javascript"> $jscript .= "\n<!--\n\n";
<!--
var cpts = new Array();
var vers = new Array();
var tms = new Array();
ENDSCRIPT
# Add the javascript code for the arrays of components and versions
# This is used in our javascript functions
my $p; $jscript .= "var usetms = 0; // do we have target milestone?\n";
my $v; $jscript .= "var first_load = 1; // is this the first time we load the page?\n";
my $c; $jscript .= "var last_sel = []; // caches last selection\n";
my $m; $jscript .= make_js_array( \@::product_list, \%::components, "cpts" );
my $i = 0; $jscript .= make_js_array( \@::product_list, \%::versions, "vers" );
my $j = 0;
foreach $c (@::component_list) { if ( Param( "usetargetmilestone" ) ) {
$jscript .= "cpts['$c'] = new Array();\n"; $jscript .= make_js_array(\@::product_list, \%::target_milestone, "tms");
$jscript .= "\nusetms = 1; // hooray, we use target milestones\n";
} }
foreach $v (@::version_list) { $jscript .= << 'ENDSCRIPT';
$jscript .= "vers['$v'] = new Array();\n";
}
my $tm; // Adds to the target select object all elements in array that
foreach $tm (@::milestone_list) { // correspond to the elements selected in source.
$jscript .= "tms['$tm'] = new Array();\n"; // - array should be a array of arrays, indexed by product name. the
} // array should contain the elements that correspont to that
// product. Example:
// var array = Array();
// array['ProductOne'] = [ 'ComponentA', 'ComponentB' ];
// updateSelect(array, source, target);
// - sel is a list of selected items, either whole or a diff
// depending on sel_is_diff.
// - sel_is_diff determines if we are sending in just a diff or the
// whole selection. a diff is used to optimize adding selections.
// - target should be the target select object.
// - single specifies if we selected a single item. if we did, no
// need to merge.
function updateSelect( array, sel, target, sel_is_diff, single ) {
for $p (@::product_list) { var i;
if ($::components{$p}) {
foreach $c (@{$::components{$p}}) {
$jscript .= "cpts['$c'][cpts['$c'].length] = '$p';\n";
}
}
if ($::versions{$p}) { // if single, even if it's a diff (happens when you have nothing
foreach $v (@{$::versions{$p}}) { // selected and select one item alone), skip this.
$jscript .= "vers['$v'][vers['$v'].length] = '$p';\n"; if ( ! single ) {
}
// array merging/sorting in the case of multiple selections
if ( sel_is_diff ) {
// merge in the current options with the first selection
comp = merge_arrays( array[sel[0]], target.options, 1 );
// merge the rest of the selection with the results
for ( i = 1 ; i < sel.length ; i++ ) {
comp = merge_arrays( array[sel[i]], comp, 0 );
} }
} else {
// here we micro-optimize for two arrays to avoid merging with a
// null array
comp = merge_arrays( array[sel[0]],array[sel[1]], 0 );
if ($::target_milestone{$p}) { // merge the arrays. not very good for multiple selections.
foreach $m (@{$::target_milestone{$p}}) { for ( i = 2; i < sel.length; i++ ) {
$jscript .= "tms['$m'][tms['$m'].length] = '$p';\n"; comp = merge_arrays( comp, array[sel[i]], 0 );
} }
} }
} } else {
// single item in selection, just get me the list
$i = 0; comp = array[sel[0]];
$jscript .= << 'ENDSCRIPT'; }
// Only display versions/components valid for selected product(s) // clear select
target.options.length = 0;
function selectProduct(f) { // load elements of list into select
// Netscape 4.04 and 4.05 also choke with an "undefined" for ( i = 0; i < comp.length; i++ ) {
// error. if someone can figure out how to "define" the target.options[i] = new Option( comp[i], comp[i] );
// whatever, we'll remove this hack. in the mean time, we'll }
// assume that v4.00-4.03 also die, so we'll disable the neat }
// javascript stuff for Netscape 4.05 and earlier.
var cnt = 0; // Returns elements in a that are not in b.
var i; // NOT A REAL DIFF: does not check the reverse.
var j; // - a,b: arrays of values to be compare.
if (!f) { function fake_diff_array( a, b ) {
return; var newsel = new Array();
}
for (i=0 ; i<f.product.length ; i++) { // do a boring array diff to see who's new
if (f.product[i].selected) { for ( ia in a ) {
cnt++; var found = 0;
for ( ib in b ) {
if ( a[ia] == b[ib] ) {
found = 1;
} }
} }
var doall = (cnt == f.product.length || cnt == 0); if ( ! found ) {
newsel[newsel.length] = a[ia];
var csel = new Object();
for (i=0 ; i<f.component.length ; i++) {
if (f.component[i].selected) {
csel[f.component[i].value] = 1;
} }
found = 0;
} }
return newsel;
}
f.component.options.length = 0; // takes two arrays and sorts them by string, returning a new, sorted
// array. the merge removes dupes, too.
// - a, b: arrays to be merge.
// - b_is_select: if true, then b is actually an optionitem and as
// such we need to use item.value on it.
for (c in cpts) { function merge_arrays( a, b, b_is_select ) {
if (typeof(cpts[c]) == 'function') continue; var pos_a = 0;
var doit = doall; var pos_b = 0;
for (i=0 ; !doit && i<f.product.length ; i++) { var ret = new Array();
if (f.product[i].selected) {
var p = f.product[i].value; // iterate through both arrays and add the larger item to the return
for (j in cpts[c]) { // list. remove dupes, too. Use toLowerCase to provide
if (typeof(cpts[c][j]) == 'function') continue; // case-insensitivity.
var p2 = cpts[c][j];
if (p2 == p) { while ( ( pos_a < a.length ) && ( pos_b < b.length ) ) {
doit = true;
break; if ( b_is_select ) {
} bitem = b[pos_b].value;
} } else {
} bitem = b[pos_b];
} }
if (doit) { aitem = a[pos_a];
var l = f.component.length;
f.component[l] = new Option(c, c); // smaller item in list a
if (csel[c]) { if ( aitem.toLowerCase() < bitem.toLowerCase() ) {
f.component[l].selected = true; ret[ret.length] = aitem;
pos_a++;
} else {
// smaller item in list b
if ( aitem.toLowerCase() > bitem.toLowerCase() ) {
ret[ret.length] = bitem;
pos_b++;
} else {
// list contents are equal, inc both counters.
ret[ret.length] = aitem;
pos_a++;
pos_b++;
} }
} }
} }
var vsel = new Object(); // catch leftovers here. these sections are ugly code-copying.
for (i=0 ; i<f.version.length ; i++) { if ( pos_a < a.length ) {
if (f.version[i].selected) { for ( ; pos_a < a.length ; pos_a++ ) {
vsel[f.version[i].value] = 1; ret[ret.length] = a[pos_a];
} }
} }
f.version.options.length = 0; if ( pos_b < b.length ) {
for ( ; pos_b < b.length; pos_b++ ) {
for (v in vers) { if ( b_is_select ) {
if (typeof(vers[v]) == 'function') continue; bitem = b[pos_b].value;
doit = doall; } else {
for (i=0 ; !doit && i<f.product.length ; i++) { bitem = b[pos_b];
if (f.product[i].selected) {
p = f.product[i].value;
for (j in vers[v]) {
if (typeof(vers[v][j]) == 'function') continue;
p2 = vers[v][j];
if (p2 == p) {
doit = true;
break;
}
}
}
}
if (doit) {
l = f.version.length;
f.version[l] = new Option(v, v);
if (vsel[v]) {
f.version[l].selected = true;
} }
ret[ret.length] = bitem;
} }
} }
return ret;
}
ENDSCRIPT // selectProduct reads the selection from f.product and updates
if (Param("usetargetmilestone")) { // f.version, component and target_milestone accordingly.
$jscript .= q{ // - f: a form containing product, component, varsion and
if (f.target_milestone) { // target_milestone select boxes.
var tmsel = new Object(); // globals (3vil!):
for (i=0 ; i<f.target_milestone.length ; i++) { // - cpts, vers, tms: array of arrays, indexed by product name. the
if (f.target_milestone[i].selected) { // subarrays contain a list of names to be fed to the respective
tmsel[f.target_milestone[i].value] = 1; // selectboxes. For bugzilla, these are generated with perl code
} // at page start.
// - usetms: this is a global boolean that is defined if the
// bugzilla installation has it turned on. generated in perl too.
// - first_load: boolean, specifying if it's the first time we load
// the query page.
// - last_sel: saves our last selection list so we know what has
// changed, and optimize for additions.
function selectProduct( f ) {
// this is to avoid events that occur before the form itself is
// ready. mozilla doesn't seem to trigger this, though.
if ( !f ) {
return;
} }
f.target_milestone.options.length = 0; // if this is the first load and nothing is selected, no need to
// merge and sort all components; perl gives it to us sorted.
for (tm in tms) { if ( ( first_load ) && ( f.product.selectedIndex == -1 ) ) {
if (typeof(tms[v]) == 'function') continue; first_load = 0;
doit = doall; return;
for (i=0 ; !doit && i<f.product.length ; i++) {
if (f.product[i].selected) {
p = f.product[i].value;
for (j in tms[tm]) {
if (typeof(tms[tm][j]) == 'function') continue;
p2 = tms[tm][j];
if (p2 == p) {
doit = true;
break;
}
}
} }
// turn first_load off. this is tricky, since it seems to be
// redundant with the above clause. It's not: if when we first load
// the page there is _one_ element selected, it won't fall into that
// clause, and first_load will remain 1. Then, if we unselect that
// item, selectProduct will be called but the clause will be valid
// (since selectedIndex == -1), and we will return - incorrectly -
// without merge/sorting.
first_load = 0;
// - sel keeps the array of products we are selected.
// - is_diff says if it's a full list or just a list of products that
// were added to the current selection.
// - single indicates if a single item was selected
var sel = Array();
var is_diff = 0;
var single;
// is nothing selected, pick all
if ( f.product.selectedIndex == -1 ) {
for ( i=0 ; i<f.product.length ; i++ ) {
sel[sel.length] = f.product.options[i].value;
} }
if (doit) { single = 0;
l = f.target_milestone.length; } else {
f.target_milestone[l] = new Option(tm, tm);
if (tmsel[tm]) { for ( i=0 ; i<f.product.length ; i++ ) {
f.target_milestone[l].selected = true; if ( f.product.options[i].selected ) {
sel[sel.length] = f.product.options[i].value;
} }
} }
single = ( sel.length == 1 );
// save last_sel before we kill it
var tmp = last_sel;
last_sel = sel;
// this is an optimization: if we've added components, no need
// to remerge them; just merge the new ones with the existing
// options.
if ( ( tmp ) && ( tmp.length < sel.length ) ) {
sel = fake_diff_array(sel, tmp);
is_diff = 1;
} }
} }
};
// do the actual fill/update
updateSelect( cpts, sel, f.component, is_diff, single );
updateSelect( vers, sel, f.version, is_diff, single );
if ( usetms ) {
updateSelect( tms, sel, f.target_milestone, is_diff, single );
}
} }
$jscript .= << 'ENDSCRIPT';
}
// --> // -->
</script> </script>
ENDSCRIPT ENDSCRIPT
#
# End the fearsome Javascript section.
#
# Muck the "legal product" list so that the default one is always first (and # Muck the "legal product" list so that the default one is always first (and
# is therefore visibly selected. # is therefore visibly selected.
......
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