| /* The contents of this file are subject to the Mozilla Public |
| * License Version 1.1 (the "License"); you may not use this file |
| * except in compliance with the License. You may obtain a copy of |
| * the License at http://www.mozilla.org/MPL/ |
| * |
| * Software distributed under the License is distributed on an "AS |
| * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or |
| * implied. See the License for the specific language governing |
| * rights and limitations under the License. |
| * |
| * The Original Code is the Bugzilla Bug Tracking System. |
| * |
| * The Initial Developer of the Original Code is Netscape Communications |
| * Corporation. Portions created by Netscape are |
| * Copyright (C) 1998 Netscape Communications Corporation. All |
| * Rights Reserved. |
| * |
| * Contributor(s): Christian Reis <kiko@async.com.br> |
| */ |
| |
| // Functions to update form select elements based on a |
| // collection of javascript arrays containing strings. |
| |
| /** |
| * Reads the selected classifications and updates product, component, |
| * version and milestone lists accordingly. |
| * |
| * @param classfield Select element that contains classifications. |
| * @param product Select element that contains products. |
| * @param component Select element that contains components. Can be null if |
| * there is no such element to update. |
| * @param version Select element that contains versions. Can be null if |
| * there is no such element to update. |
| * @param milestone Select element that contains milestones. Can be null if |
| * there is no such element to update. |
| * |
| * @global prods Array of products indexed by classification name. |
| * @global first_load Boolean; true if this is the first time this page loads |
| * or false if not. |
| * @global last_sel Array that contains last list of products so we know what |
| * has changed, and optimize for additions. |
| */ |
| function selectClassification(classfield, product, component, version, milestone) { |
| // This is to avoid handling events that occur before the form |
| // itself is ready, which could happen in buggy browsers. |
| if (!classfield) |
| return; |
| |
| // If this is the first load and nothing is selected, no need to |
| // merge and sort all lists; they are created sorted. |
| if ((first_load) && (classfield.selectedIndex == -1)) { |
| first_load = false; |
| return; |
| } |
| |
| // Don't reset first_load as done in selectProduct. That's because we |
| // want selectProduct to handle the first_load attribute. |
| |
| // Stores classifications that are selected. |
| var sel = Array(); |
| |
| // True if sel array has a full list or false if sel contains only |
| // new classifications that are to be merged to the current list. |
| var merging = false; |
| |
| // If nothing selected, pick all. |
| var findall = classfield.selectedIndex == -1; |
| sel = get_selection(classfield, findall, false); |
| if (!findall) { |
| // Save sel for the next invocation of selectClassification(). |
| var tmp = sel; |
| |
| // This is an optimization: if we have just added classifications to an |
| // existing selection, no need to clear the form elements and add |
| // everything again; just merge the new ones with the existing |
| // options. |
| if ((last_sel.length > 0) && (last_sel.length < sel.length)) { |
| sel = fake_diff_array(sel, last_sel); |
| merging = true; |
| } |
| last_sel = tmp; |
| } |
| |
| // Save original options selected. |
| var saved_prods = get_selection(product, false, true, null); |
| |
| // Do the actual fill/update, reselect originally selected options. |
| updateSelect(prods, sel, product, merging, null); |
| restoreSelection(product, saved_prods); |
| selectProduct(product, component, version, milestone, null); |
| } |
| |
| /** |
| * Reads the selected products and updates component, version and milestone |
| * lists accordingly. |
| * |
| * @param product Select element that contains products. |
| * @param component Select element that contains components. Can be null if |
| * there is no such element to update. |
| * @param version Select element that contains versions. Can be null if |
| * there is no such element to update. |
| * @param milestone Select element that contains milestones. Can be null if |
| * there is no such element to update. |
| * @param anyval Value to use for a special "Any" list item. Can be null |
| * to not use any. If used must and will be first item in |
| * the select element. |
| * |
| * @global cpts Array of arrays, indexed by product name. The subarrays |
| * contain a list of components to be fed to the respective |
| * select element. |
| * @global vers Array of arrays, indexed by product name. The subarrays |
| * contain a list of versions to be fed to the respective |
| * select element. |
| * @global tms Array of arrays, indexed by product name. The subarrays |
| * contain a list of milestones to be fed to the respective |
| * select element. |
| * @global first_load Boolean; true if this is the first time this page loads |
| * or false if not. |
| * @global last_sel Array that contains last list of products so we know what |
| * has changed, and optimize for additions. |
| */ |
| function selectProduct(product, component, version, milestone, anyval) { |
| // This is to avoid handling events that occur before the form |
| // itself is ready, which could happen in buggy browsers. |
| if (!product) |
| return; |
| |
| // Do nothing if no products are defined. This is to avoid the |
| // "a has no properties" error from merge_arrays function. |
| if (product.length == (anyval != null ? 1 : 0)) |
| return; |
| |
| // If this is the first load and nothing is selected, no need to |
| // merge and sort all lists; they are created sorted. |
| if ((first_load) && (product.selectedIndex == -1)) { |
| first_load = false; |
| return; |
| } |
| |
| // 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 = false; |
| |
| // Stores products that are selected. |
| var sel = Array(); |
| |
| // True if sel array has a full list or false if sel contains only |
| // new products that are to be merged to the current list. |
| var merging = false; |
| |
| // If nothing is selected, or the special "Any" option is selected |
| // which represents all products, then pick all products so we show |
| // all components. |
| var findall = (product.selectedIndex == -1 |
| || (anyval != null && product.options[0].selected)); |
| |
| if (useclassification) { |
| // Update index based on the complete product array. |
| sel = get_selection(product, findall, true, anyval); |
| for (var i=0; i<sel.length; i++) |
| sel[i] = prods[sel[i]]; |
| } |
| else { |
| sel = get_selection(product, findall, false, anyval); |
| } |
| if (!findall) { |
| // Save sel for the next invocation of selectProduct(). |
| var tmp = sel; |
| |
| // This is an optimization: if we have just added products to an |
| // existing selection, no need to clear the form controls and add |
| // everybody again; just merge the new ones with the existing |
| // options. |
| if ((last_sel.length > 0) && (last_sel.length < sel.length)) { |
| sel = fake_diff_array(sel, last_sel); |
| merging = true; |
| } |
| last_sel = tmp; |
| } |
| |
| // Do the actual fill/update. |
| if (component) { |
| var saved_cpts = get_selection(component, false, true, null); |
| updateSelect(cpts, sel, component, merging, anyval); |
| restoreSelection(component, saved_cpts); |
| } |
| |
| if (version) { |
| var saved_vers = get_selection(version, false, true, null); |
| updateSelect(vers, sel, version, merging, anyval); |
| restoreSelection(version, saved_vers); |
| } |
| |
| if (milestone) { |
| var saved_tms = get_selection(milestone, false, true, null); |
| updateSelect(tms, sel, milestone, merging, anyval); |
| restoreSelection(milestone, saved_tms); |
| } |
| } |
| |
| /** |
| * Adds to the target select element all elements from array that |
| * correspond to the selected items. |
| * |
| * @param array An array of arrays, indexed by number. The array should |
| * contain elements for each selection. |
| * @param sel A list of selected items, either whole or a diff depending |
| * on merging parameter. |
| * @param target Select element that is to be updated. |
| * @param merging Boolean that determines if we are merging in a diff or |
| * substituting the whole selection. A diff is used to optimize |
| * adding selections. |
| * @param anyval Name of special "Any" value to add. Can be null if not used. |
| * @return Boolean; true if target contains options or false if target |
| * is empty. |
| * |
| * Example (compsel is a select form element): |
| * |
| * var components = Array(); |
| * components[1] = [ 'ComponentA', 'ComponentB' ]; |
| * components[2] = [ 'ComponentC', 'ComponentD' ]; |
| * source = [ 2 ]; |
| * updateSelect(components, source, compsel, false, null); |
| * |
| * This would clear compsel and add 'ComponentC' and 'ComponentD' to it. |
| */ |
| function updateSelect(array, sel, target, merging, anyval) { |
| var i, item; |
| |
| // If we have no versions/components/milestones. |
| if (array.length < 1) { |
| target.options.length = 0; |
| return false; |
| } |
| |
| if (merging) { |
| // Array merging/sorting in the case of multiple selections |
| // merge in the current options with the first selection. |
| item = merge_arrays(array[sel[0]], target.options, 1); |
| |
| // Merge the rest of the selection with the results. |
| for (i = 1 ; i < sel.length ; i++) |
| item = merge_arrays(array[sel[i]], item, 0); |
| } |
| else if (sel.length > 1) { |
| // Here we micro-optimize for two arrays to avoid merging with a |
| // null array. |
| item = merge_arrays(array[sel[0]],array[sel[1]], 0); |
| |
| // Merge the arrays. Not very good for multiple selections. |
| for (i = 2; i < sel.length; i++) |
| item = merge_arrays(item, array[sel[i]], 0); |
| } |
| else { |
| // Single item in selection, just get me the list. |
| item = array[sel[0]]; |
| } |
| |
| // Clear current selection. |
| target.options.length = 0; |
| |
| // Add special "Any" value back to the list. |
| if (anyval != null) |
| target.options[0] = new Option(anyval, ""); |
| |
| // Load elements of list into select element. |
| for (i = 0; i < item.length; i++) |
| target.options[target.options.length] = new Option(item[i], item[i]); |
| |
| return true; |
| } |
| |
| /** |
| * Selects items in select element that are defined to be selected. |
| * |
| * @param control Select element of which selected options are to be restored. |
| * @param selnames Array of option names to select. |
| */ |
| function restoreSelection(control, selnames) { |
| // Right. This sucks but I see no way to avoid going through the |
| // list and comparing to the contents of the control. |
| for (var j = 0; j < selnames.length; j++) |
| for (var i = 0; i < control.options.length; i++) |
| if (control.options[i].value == selnames[j]) |
| control.options[i].selected = true; |
| } |
| |
| /** |
| * Returns elements in a that are not in b. |
| * NOT A REAL DIFF: does not check the reverse. |
| * |
| * @param a First array to compare. |
| * @param b Second array to compare. |
| * @return Array of elements in a but not in b. |
| */ |
| function fake_diff_array(a, b) { |
| var newsel = new Array(); |
| var found = false; |
| |
| // Do a boring array diff to see who's new. |
| for (var ia in a) { |
| for (var ib in b) |
| if (a[ia] == b[ib]) |
| found = true; |
| |
| if (!found) |
| newsel[newsel.length] = a[ia]; |
| |
| found = false; |
| } |
| |
| return newsel; |
| } |
| |
| /** |
| * Takes two arrays and sorts them by string, returning a new, sorted |
| * array. The merge removes dupes, too. |
| * |
| * @param a First array to merge. |
| * @param b Second array or an optionitem element to merge. |
| * @param b_is_select Boolean; true if b is an optionitem element (need to |
| * access its value by item.value) or false if b is a |
| * an array. |
| * @return Merged and sorted array. |
| */ |
| function merge_arrays(a, b, b_is_select) { |
| var pos_a = 0; |
| var pos_b = 0; |
| var ret = new Array(); |
| var bitem, aitem; |
| |
| // Iterate through both arrays and add the larger item to the return |
| // list. Remove dupes, too. Use toLowerCase to provide |
| // case-insensitivity. |
| while ((pos_a < a.length) && (pos_b < b.length)) { |
| aitem = a[pos_a]; |
| if (b_is_select) |
| bitem = b[pos_b].value; |
| else |
| bitem = b[pos_b]; |
| |
| // Smaller item in list a. |
| if (aitem.toLowerCase() < bitem.toLowerCase()) { |
| 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, include both counters. |
| ret[ret.length] = aitem; |
| pos_a++; |
| pos_b++; |
| } |
| } |
| } |
| |
| // Catch leftovers here. These sections are ugly code-copying. |
| if (pos_a < a.length) |
| for (; pos_a < a.length ; pos_a++) |
| ret[ret.length] = a[pos_a]; |
| |
| if (pos_b < b.length) { |
| for (; pos_b < b.length; pos_b++) { |
| if (b_is_select) |
| bitem = b[pos_b].value; |
| else |
| bitem = b[pos_b]; |
| ret[ret.length] = bitem; |
| } |
| } |
| |
| return ret; |
| } |
| |
| /** |
| * Returns an array of indexes or values of options in a select form element. |
| * |
| * @param control Select form element from which to find selections. |
| * @param findall Boolean; true to return all options or false to return |
| * only selected options. |
| * @param want_values Boolean; true to return values and false to return |
| * indexes. |
| * @param anyval Name of a special "Any" value that should be skipped. Can |
| * be null if not used. |
| * @return Array of all or selected indexes or values. |
| */ |
| function get_selection(control, findall, want_values, anyval) { |
| var ret = new Array(); |
| |
| if ((!findall) && (control.selectedIndex == -1)) |
| return ret; |
| |
| for (var i = (anyval != null ? 1 : 0); i < control.length; i++) |
| if (findall || control.options[i].selected) |
| ret[ret.length] = want_values ? control.options[i].value : i; |
| |
| return ret; |
| } |