productform.js 15 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
/* 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>
 */

21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
// 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.
42 43
 */
function selectClassification(classfield, product, component, version, milestone) {
44 45 46
    // This is to avoid handling events that occur before the form
    // itself is ready, which could happen in buggy browsers.
    if (!classfield)
47 48
        return;

49 50
    // If this is the first load and nothing is selected, no need to
    // merge and sort all lists; they are created sorted.
51 52 53 54 55
    if ((first_load) && (classfield.selectedIndex == -1)) {
        first_load = false;
        return;
    }
    
56 57 58 59
    // 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.
60 61
    var sel = Array();

62 63 64 65 66
    // 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.
67 68 69
    var findall = classfield.selectedIndex == -1;
    sel = get_selection(classfield, findall, false);
    if (!findall) {
70
        // Save sel for the next invocation of selectClassification().
71 72
        var tmp = sel;
    
73 74 75 76
        // 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.
77 78 79 80 81 82 83
        if ((last_sel.length > 0) && (last_sel.length < sel.length)) {
            sel = fake_diff_array(sel, last_sel);
            merging = true;
        }
        last_sel = tmp;
    }

84 85 86 87 88
    // 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);
89
    restoreSelection(product, saved_prods);
90
    selectProduct(product, component, version, milestone, null);
91 92
}

93 94 95 96 97 98 99 100 101 102 103 104 105 106
/**
 * 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.
107
 *
108 109 110 111 112 113 114 115 116 117 118 119 120
 * @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.
121
 */
122 123 124 125 126
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;
127

128 129 130
    // 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))
131 132
        return;

133 134
    // If this is the first load and nothing is selected, no need to
    // merge and sort all lists; they are created sorted.
135 136 137 138 139
    if ((first_load) && (product.selectedIndex == -1)) {
        first_load = false;
        return;
    }

140 141 142 143 144 145 146
    // 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.
147 148
    first_load = false;

149
    // Stores products that are selected.
150 151
    var sel = Array();

152 153 154 155 156 157 158 159 160 161
    // 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));

162
    if (useclassification) {
163 164 165
        // Update index based on the complete product array.
        sel = get_selection(product, findall, true, anyval);
        for (var i=0; i<sel.length; i++)
166
           sel[i] = prods[sel[i]];
167 168 169
    }
    else {
        sel = get_selection(product, findall, false, anyval);
170
    }
171
    if (!findall) {
172
        // Save sel for the next invocation of selectProduct().
173 174
        var tmp = sel;

175 176 177 178
        // 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.
179 180 181 182 183 184 185
        if ((last_sel.length > 0) && (last_sel.length < sel.length)) {
            sel = fake_diff_array(sel, last_sel);
            merging = true;
        }
        last_sel = tmp;
    }

186
    // Do the actual fill/update.
187
    if (component) {
188 189
        var saved_cpts = get_selection(component, false, true, null);
        updateSelect(cpts, sel, component, merging, anyval);
190 191 192 193
        restoreSelection(component, saved_cpts);
    }

    if (version) {
194 195
        var saved_vers = get_selection(version, false, true, null);
        updateSelect(vers, sel, version, merging, anyval);
196 197 198 199
        restoreSelection(version, saved_vers);
    }

    if (milestone) {
200 201
        var saved_tms = get_selection(milestone, false, true, null);
        updateSelect(tms, sel, milestone, merging, anyval);
202 203 204 205
        restoreSelection(milestone, saved_tms);
    }
}

206 207 208
/**
 * Adds to the target select element all elements from array that
 * correspond to the selected items.
209
 *
210 211 212 213 214 215 216 217 218 219 220
 * @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.
221
 *
222
 * Example (compsel is a select form element):
223 224 225 226 227
 *
 *     var components = Array();
 *     components[1] = [ 'ComponentA', 'ComponentB' ];
 *     components[2] = [ 'ComponentC', 'ComponentD' ];
 *     source = [ 2 ];
228
 *     updateSelect(components, source, compsel, false, null);
229
 *
230
 * This would clear compsel and add 'ComponentC' and 'ComponentD' to it.
231
 */
232
function updateSelect(array, sel, target, merging, anyval) {
233 234
    var i, item;

235
    // If we have no versions/components/milestones.
236 237 238 239 240 241
    if (array.length < 1) {
        target.options.length = 0;
        return false;
    }

    if (merging) {
242 243
        // Array merging/sorting in the case of multiple selections
        // merge in the current options with the first selection.
244 245
        item = merge_arrays(array[sel[0]], target.options, 1);

246 247
        // Merge the rest of the selection with the results.
        for (i = 1 ; i < sel.length ; i++)
248
            item = merge_arrays(array[sel[i]], item, 0);
249 250 251 252
    }
    else if (sel.length > 1) {
        // Here we micro-optimize for two arrays to avoid merging with a
        // null array.
253 254
        item = merge_arrays(array[sel[0]],array[sel[1]], 0);

255 256
        // Merge the arrays. Not very good for multiple selections.
        for (i = 2; i < sel.length; i++)
257
            item = merge_arrays(item, array[sel[i]], 0);
258 259 260
    }
    else {
        // Single item in selection, just get me the list.
261 262 263
        item = array[sel[0]];
    }

264
    // Clear current selection.
265 266
    target.options.length = 0;

267 268 269 270 271 272 273 274
    // 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]);

275 276 277
    return true;
}

278 279 280 281 282 283
/**
 * 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.
 */
284
function restoreSelection(control, selnames) {
285 286 287 288 289
    // 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])
290 291 292
                control.options[i].selected = true;
}

293 294
/**
 * Returns elements in a that are not in b.
295
 * NOT A REAL DIFF: does not check the reverse.
296 297 298 299 300
 *
 * @param  a First array to compare.
 * @param  b Second array to compare.
 * @return   Array of elements in a but not in b.
 */
301 302 303 304
function fake_diff_array(a, b) {
    var newsel = new Array();
    var found = false;

305
    // Do a boring array diff to see who's new.
306
    for (var ia in a) {
307 308
        for (var ib in b)
            if (a[ia] == b[ib])
309
                found = true;
310 311

        if (!found)
312
            newsel[newsel.length] = a[ia];
313

314 315
        found = false;
    }
316

317 318 319
    return newsel;
}

320 321 322 323 324 325 326 327 328 329 330
/**
 * 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.
 */
331 332 333 334 335 336
function merge_arrays(a, b, b_is_select) {
    var pos_a = 0;
    var pos_b = 0;
    var ret = new Array();
    var bitem, aitem;

337 338 339
    // Iterate through both arrays and add the larger item to the return
    // list. Remove dupes, too. Use toLowerCase to provide
    // case-insensitivity.
340
    while ((pos_a < a.length) && (pos_b < b.length)) {
341 342
        aitem = a[pos_a];
        if (b_is_select)
343
            bitem = b[pos_b].value;
344
        else
345 346
            bitem = b[pos_b];

347
        // Smaller item in list a.
348 349 350
        if (aitem.toLowerCase() < bitem.toLowerCase()) {
            ret[ret.length] = aitem;
            pos_a++;
351 352 353
        }
        else {
            // Smaller item in list b.
354 355 356
            if (aitem.toLowerCase() > bitem.toLowerCase()) {
                ret[ret.length] = bitem;
                pos_b++;
357 358 359
            }
            else {
                // List contents are equal, include both counters.
360 361 362 363 364 365 366
                ret[ret.length] = aitem;
                pos_a++;
                pos_b++;
            }
        }
    }

367 368 369
    // Catch leftovers here. These sections are ugly code-copying.
    if (pos_a < a.length)
        for (; pos_a < a.length ; pos_a++)
370 371 372 373
            ret[ret.length] = a[pos_a];

    if (pos_b < b.length) {
        for (; pos_b < b.length; pos_b++) {
374
            if (b_is_select)
375
                bitem = b[pos_b].value;
376
            else
377 378 379 380
                bitem = b[pos_b];
            ret[ret.length] = bitem;
        }
    }
381

382 383 384
    return ret;
}

385 386 387 388 389 390 391 392 393 394 395 396 397
/**
 * 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) {
398 399
    var ret = new Array();

400
    if ((!findall) && (control.selectedIndex == -1))
401 402
        return ret;

403 404
    for (var i = (anyval != null ? 1 : 0); i < control.length; i++)
        if (findall || control.options[i].selected)
405
            ret[ret.length] = want_values ? control.options[i].value : i;
406

407 408
    return ret;
}