productform.js 11.6 KB
Newer Older
1 2 3 4 5 6
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 *
 * This Source Code Form is "Incompatible With Secondary Licenses", as
 * defined by the Mozilla Public License, v. 2.0.
7 8
 */

9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
// Functions to update form select elements based on a
// collection of javascript arrays containing strings.

/**
 * 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.
26
 *
27 28 29 30 31 32 33 34 35 36 37 38 39
 * @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.
40
 */
41 42 43 44 45
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;
46

47 48 49
    // 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))
50 51
        return;

52 53
    // If this is the first load and nothing is selected, no need to
    // merge and sort all lists; they are created sorted.
54 55 56 57 58
    if ((first_load) && (product.selectedIndex == -1)) {
        first_load = false;
        return;
    }

59 60 61 62 63 64 65
    // 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.
66 67
    first_load = false;

68
    // Stores products that are selected.
69 70
    var sel = Array();

71 72 73 74 75 76 77 78 79 80
    // 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));

81
    if (useclassification) {
82 83 84
        // Update index based on the complete product array.
        sel = get_selection(product, findall, true, anyval);
        for (var i=0; i<sel.length; i++)
85
           sel[i] = prods[sel[i]];
86 87 88
    }
    else {
        sel = get_selection(product, findall, false, anyval);
89
    }
90
    if (!findall) {
91
        // Save sel for the next invocation of selectProduct().
92 93
        var tmp = sel;

94 95 96 97
        // 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.
98 99 100 101 102 103 104
        if ((last_sel.length > 0) && (last_sel.length < sel.length)) {
            sel = fake_diff_array(sel, last_sel);
            merging = true;
        }
        last_sel = tmp;
    }

105
    // Do the actual fill/update.
106
    if (component) {
107 108
        var saved_cpts = get_selection(component, false, true, null);
        updateSelect(cpts, sel, component, merging, anyval);
109 110 111 112
        restoreSelection(component, saved_cpts);
    }

    if (version) {
113 114
        var saved_vers = get_selection(version, false, true, null);
        updateSelect(vers, sel, version, merging, anyval);
115 116 117 118
        restoreSelection(version, saved_vers);
    }

    if (milestone) {
119 120
        var saved_tms = get_selection(milestone, false, true, null);
        updateSelect(tms, sel, milestone, merging, anyval);
121 122 123 124
        restoreSelection(milestone, saved_tms);
    }
}

125 126 127
/**
 * Adds to the target select element all elements from array that
 * correspond to the selected items.
128
 *
129 130 131 132 133 134 135 136 137 138 139
 * @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.
140
 *
141
 * Example (compsel is a select form element):
142 143 144 145 146
 *
 *     var components = Array();
 *     components[1] = [ 'ComponentA', 'ComponentB' ];
 *     components[2] = [ 'ComponentC', 'ComponentD' ];
 *     source = [ 2 ];
147
 *     updateSelect(components, source, compsel, false, null);
148
 *
149
 * This would clear compsel and add 'ComponentC' and 'ComponentD' to it.
150
 */
151
function updateSelect(array, sel, target, merging, anyval) {
152 153
    var i, item;

154
    // If we have no versions/components/milestones.
155 156 157 158 159 160
    if (array.length < 1) {
        target.options.length = 0;
        return false;
    }

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

165 166
        // Merge the rest of the selection with the results.
        for (i = 1 ; i < sel.length ; i++)
167
            item = merge_arrays(array[sel[i]], item, 0);
168 169 170 171
    }
    else if (sel.length > 1) {
        // Here we micro-optimize for two arrays to avoid merging with a
        // null array.
172 173
        item = merge_arrays(array[sel[0]],array[sel[1]], 0);

174 175
        // Merge the arrays. Not very good for multiple selections.
        for (i = 2; i < sel.length; i++)
176
            item = merge_arrays(item, array[sel[i]], 0);
177 178 179
    }
    else {
        // Single item in selection, just get me the list.
180 181 182
        item = array[sel[0]];
    }

183
    // Clear current selection.
184 185
    target.options.length = 0;

186 187 188 189 190 191 192 193
    // 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]);

194 195 196
    return true;
}

197 198 199 200 201 202
/**
 * 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.
 */
203
function restoreSelection(control, selnames) {
204 205 206 207 208
    // 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])
209 210 211
                control.options[i].selected = true;
}

212 213
/**
 * Returns elements in a that are not in b.
214
 * NOT A REAL DIFF: does not check the reverse.
215 216 217 218 219
 *
 * @param  a First array to compare.
 * @param  b Second array to compare.
 * @return   Array of elements in a but not in b.
 */
220 221 222 223
function fake_diff_array(a, b) {
    var newsel = new Array();
    var found = false;

224
    // Do a boring array diff to see who's new.
225
    for (var ia in a) {
226 227
        for (var ib in b)
            if (a[ia] == b[ib])
228
                found = true;
229 230

        if (!found)
231
            newsel[newsel.length] = a[ia];
232

233 234
        found = false;
    }
235

236 237 238
    return newsel;
}

239 240 241 242 243 244 245 246 247 248 249
/**
 * 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.
 */
250 251 252 253 254 255
function merge_arrays(a, b, b_is_select) {
    var pos_a = 0;
    var pos_b = 0;
    var ret = new Array();
    var bitem, aitem;

256 257 258
    // Iterate through both arrays and add the larger item to the return
    // list. Remove dupes, too. Use toLowerCase to provide
    // case-insensitivity.
259
    while ((pos_a < a.length) && (pos_b < b.length)) {
260 261
        aitem = a[pos_a];
        if (b_is_select)
262
            bitem = b[pos_b].value;
263
        else
264 265
            bitem = b[pos_b];

266
        // Smaller item in list a.
267 268 269
        if (aitem.toLowerCase() < bitem.toLowerCase()) {
            ret[ret.length] = aitem;
            pos_a++;
270 271 272
        }
        else {
            // Smaller item in list b.
273 274 275
            if (aitem.toLowerCase() > bitem.toLowerCase()) {
                ret[ret.length] = bitem;
                pos_b++;
276 277 278
            }
            else {
                // List contents are equal, include both counters.
279 280 281 282 283 284 285
                ret[ret.length] = aitem;
                pos_a++;
                pos_b++;
            }
        }
    }

286 287 288
    // Catch leftovers here. These sections are ugly code-copying.
    if (pos_a < a.length)
        for (; pos_a < a.length ; pos_a++)
289 290 291 292
            ret[ret.length] = a[pos_a];

    if (pos_b < b.length) {
        for (; pos_b < b.length; pos_b++) {
293
            if (b_is_select)
294
                bitem = b[pos_b].value;
295
            else
296 297 298 299
                bitem = b[pos_b];
            ret[ret.length] = bitem;
        }
    }
300

301 302 303
    return ret;
}

304 305 306 307 308 309 310 311 312 313 314 315 316
/**
 * 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) {
317 318
    var ret = new Array();

319
    if ((!findall) && (control.selectedIndex == -1))
320 321
        return ret;

322 323
    for (var i = (anyval != null ? 1 : 0); i < control.length; i++)
        if (findall || control.options[i].selected)
324
            ret[ret.length] = want_values ? control.options[i].value : i;
325

326 327
    return ret;
}