field.js 43 KB
Newer Older
1 2 3
/* 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/.
4
 *
5 6
 * This Source Code Form is "Incompatible With Secondary Licenses", as
 * defined by the Mozilla Public License, v. 2.0.
7 8 9 10 11
 */

/* This library assumes that the needed YUI libraries have been loaded 
   already. */

12
var bz_no_validate_enter_bug = false;
13
function validateEnterBug(theform) {
14 15 16 17 18 19 20
    // This is for the "bookmarkable templates" button.
    if (bz_no_validate_enter_bug) {
        // Set it back to false for people who hit the "back" button
        bz_no_validate_enter_bug = false;
        return true;
    }

21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
    var component = theform.component;
    var short_desc = theform.short_desc;
    var version = theform.version;
    var bug_status = theform.bug_status;
    var description = theform.comment;
    var attach_data = theform.data;
    var attach_desc = theform.description;

    var current_errors = YAHOO.util.Dom.getElementsByClassName(
        'validation_error_text', null, theform);
    for (var i = 0; i < current_errors.length; i++) {
        current_errors[i].parentNode.removeChild(current_errors[i]);
    }
    var current_error_fields = YAHOO.util.Dom.getElementsByClassName(
        'validation_error_field', null, theform);
    for (var i = 0; i < current_error_fields.length; i++) {
        var field = current_error_fields[i];
        YAHOO.util.Dom.removeClass(field, 'validation_error_field');
    }

    var focus_me;

    // These are checked in the reverse order that they appear on the page,
    // so that the one closest to the top of the form will be focused.
45
    if (attach_data && attach_data.value && YAHOO.lang.trim(attach_desc.value) == '') {
46 47 48
        _errorFor(attach_desc, 'attach_desc');
        focus_me = attach_desc;
    }
49 50 51 52 53 54 55 56
    // bug_status can be undefined if the bug_status field is not editable by
    // the currently logged in user.
    if (bug_status) {
        var check_description = status_comment_required[bug_status.value];
        if (check_description && YAHOO.lang.trim(description.value) == '') {
            _errorFor(description, 'description');
            focus_me = description;
        }
57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89
    }
    if (YAHOO.lang.trim(short_desc.value) == '') {
        _errorFor(short_desc);
        focus_me = short_desc;
    }
    if (version.selectedIndex < 0) {
        _errorFor(version);
        focus_me = version;
    }
    if (component.selectedIndex < 0) {
        _errorFor(component);
        focus_me = component;
    }

    if (focus_me) {
        focus_me.focus();
        return false;
    }

    return true;
}

function _errorFor(field, name) {
    if (!name) name = field.id;
    var string_name = name + '_required';
    var error_text = BUGZILLA.string[string_name];
    var new_node = document.createElement('div');
    YAHOO.util.Dom.addClass(new_node, 'validation_error_text');
    new_node.innerHTML = error_text;
    YAHOO.util.Dom.insertAfter(new_node, field);
    YAHOO.util.Dom.addClass(field, 'validation_error_field');
}

90 91 92 93 94
/* This function is never to be called directly, but only indirectly
 * using template/en/default/global/calendar.js.tmpl, so that localization
 * works. For the same reason, if you modify this function's parameter list,
 * you need to modify the documentation in said template as well. */
function createCalendar(name, start_weekday, months_long, weekdays_short) {
95
    var cal = new YAHOO.widget.Calendar('calendar_' + name, 
96 97 98 99 100
                                        'con_calendar_' + name,
                                        { START_WEEKDAY:  start_weekday,
                                          MONTHS_LONG:    months_long,
                                          WEEKDAYS_SHORT: weekdays_short
                                        });
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162
    YAHOO.bugzilla['calendar_' + name] = cal;
    var field = document.getElementById(name);
    cal.selectEvent.subscribe(setFieldFromCalendar, field, false);
    updateCalendarFromField(field);
    cal.render();
}

/* The onclick handlers for the button that shows the calendar. */
function showCalendar(field_name) {
    var calendar  = YAHOO.bugzilla["calendar_" + field_name];
    var field     = document.getElementById(field_name);
    var button    = document.getElementById('button_calendar_' + field_name);

    bz_overlayBelow(calendar.oDomContainer, field);
    calendar.show();
    button.onclick = function() { hideCalendar(field_name); };

    // Because of the way removeListener works, this has to be a function
    // attached directly to this calendar.
    calendar.bz_myBodyCloser = function(event) {
        var container = this.oDomContainer;
        var target    = YAHOO.util.Event.getTarget(event);
        if (target != container && target != button
            && !YAHOO.util.Dom.isAncestor(container, target))
        {
            hideCalendar(field_name);
        }
    };

    // If somebody clicks outside the calendar, hide it.
    YAHOO.util.Event.addListener(document.body, 'click', 
                                 calendar.bz_myBodyCloser, calendar, true);

    // Make Esc close the calendar.
    calendar.bz_escCal = function (event) {
        var key = YAHOO.util.Event.getCharCode(event);
        if (key == 27) {
            hideCalendar(field_name);
        }
    };
    YAHOO.util.Event.addListener(document.body, 'keydown', calendar.bz_escCal);
}

function hideCalendar(field_name) {
    var cal = YAHOO.bugzilla["calendar_" + field_name];
    cal.hide();
    var button = document.getElementById('button_calendar_' + field_name);
    button.onclick = function() { showCalendar(field_name); };
    YAHOO.util.Event.removeListener(document.body, 'click',
                                    cal.bz_myBodyCloser);
    YAHOO.util.Event.removeListener(document.body, 'keydown', cal.bz_escCal);
}

/* This is the selectEvent for our Calendar objects on our custom 
 * DateTime fields.
 */
function setFieldFromCalendar(type, args, date_field) {
    var dates = args[0];
    var setDate = dates[0];

    // We can't just write the date straight into the field, because there 
    // might already be a time there.
163
    var timeRe = /\b(\d{1,2}):(\d\d)(?::(\d\d))?/;
164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188
    var currentTime = timeRe.exec(date_field.value);
    var d = new Date(setDate[0], setDate[1] - 1, setDate[2]);
    if (currentTime) {
        d.setHours(currentTime[1], currentTime[2]);
        if (currentTime[3]) {
            d.setSeconds(currentTime[3]);
        }
    }

    var year = d.getFullYear();
    // JavaScript's "Date" represents January as 0 and December as 11.
    var month = d.getMonth() + 1;
    if (month < 10) month = '0' + String(month);
    var day = d.getDate();
    if (day < 10) day = '0' + String(day);
    var dateStr = year + '-' + month  + '-' + day;

    if (currentTime) {
        var minutes = d.getMinutes();
        if (minutes < 10) minutes = '0' + String(minutes);
        var seconds = d.getSeconds();
        if (seconds > 0 && seconds < 10) {
            seconds = '0' + String(seconds);
        }

189
        dateStr = dateStr + ' ' + d.getHours() + ':' + minutes;
190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211
        if (seconds) dateStr = dateStr + ':' + seconds;
    }

    date_field.value = dateStr;
    hideCalendar(date_field.id);
}

/* Sets the calendar based on the current field value. 
 */ 
function updateCalendarFromField(date_field) {
    var dateRe = /(\d\d\d\d)-(\d\d?)-(\d\d?)/;
    var pieces = dateRe.exec(date_field.value);
    if (pieces) {
        var cal = YAHOO.bugzilla["calendar_" + date_field.id];
        cal.select(new Date(pieces[1], pieces[2] - 1, pieces[3]));
        var selectedArray = cal.getSelectedDates();
        var selected = selectedArray[0];
        cal.cfg.setProperty("pagedate", (selected.getMonth() + 1) + '/' 
                                        + selected.getFullYear());
        cal.render();
    }
}
212

213 214 215 216 217 218
function setupEditLink(id) {
    var link_container = 'container_showhide_' + id;
    var input_container = 'container_' + id;
    var link = 'showhide_' + id;
    hideEditableField(link_container, input_container, link);
}
219

220
/* Hide input/select fields and show the text with (edit) next to it */
221
function hideEditableField( container, input, action, field_id, original_value, new_value, hide_input ) {
222 223
    YAHOO.util.Dom.removeClass(container, 'bz_default_hidden');
    YAHOO.util.Dom.addClass(input, 'bz_default_hidden');
224
    YAHOO.util.Event.addListener(action, 'click', showEditableField,
225
                                 new Array(container, input, field_id, new_value));
226
    if(field_id != ""){
227
        YAHOO.util.Event.addListener(window, 'load', checkForChangedFieldValues,
228
                        new Array(container, input, field_id, original_value, hide_input ));
229 230 231 232
    }
}

/* showEditableField (e, ContainerInputArray)
233
 * Function hides the (edit) link and the text and displays the input/select field
234 235 236
 *
 * var e: the event
 * var ContainerInputArray: An array containing the (edit) and text area and the input being displayed
237
 * var ContainerInputArray[0]: the container that will be hidden usually shows the (edit) or (take) text
238
 * var ContainerInputArray[1]: the input area and label that will be displayed
239 240
 * var ContainerInputArray[2]: the input/select field id for which the new value must be set
 * var ContainerInputArray[3]: the new value to set the input/select field to when (take) is clicked
241 242
 */
function showEditableField (e, ContainerInputArray) {
243 244 245 246 247 248
    var inputs = new Array();
    var inputArea = YAHOO.util.Dom.get(ContainerInputArray[1]);    
    if ( ! inputArea ){
        YAHOO.util.Event.preventDefault(e);
        return;
    }
249 250
    YAHOO.util.Dom.addClass(ContainerInputArray[0], 'bz_default_hidden');
    YAHOO.util.Dom.removeClass(inputArea, 'bz_default_hidden');
251 252
    if ( inputArea.tagName.toLowerCase() == "input" ) {
        inputs.push(inputArea);
253 254
    } else if (ContainerInputArray[2]) {
        inputs.push(document.getElementById(ContainerInputArray[2]));
255 256 257 258
    } else {
        inputs = inputArea.getElementsByTagName('input');
    }
    if ( inputs.length > 0 ) {
259 260
        // Change the first field's value to ContainerInputArray[2]
        // if present before focusing.
261 262 263 264 265 266 267 268 269 270 271 272
        var type = inputs[0].tagName.toLowerCase();
        if (ContainerInputArray[3]) {
            if ( type == "input" ) {
                inputs[0].value = ContainerInputArray[3];
            } else {
                for (var i = 0; inputs[0].length; i++) {
                    if ( inputs[0].options[i].value == ContainerInputArray[3] ) {
                        inputs[0].options[i].selected = true;
                        break;
                    }
                }
            }
273
        }
274 275
        // focus on the first field, this makes it easier to edit
        inputs[0].focus();
276
        if ( type == "input" || type == "textarea" ) {
277 278
            inputs[0].select();
        }
279
    }
280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297
    YAHOO.util.Event.preventDefault(e);
}


/* checkForChangedFieldValues(e, array )
 * Function checks if after the autocomplete by the browser if the values match the originals.
 *   If they don't match then hide the text and show the input so users don't get confused.
 *
 * var e: the event
 * var ContainerInputArray: An array containing the (edit) and text area and the input being displayed
 * var ContainerInputArray[0]: the conainer that will be hidden usually shows the (edit) text
 * var ContainerInputArray[1]: the input area and label that will be displayed
 * var ContainerInputArray[2]: the field that is on the page, might get changed by browser autocomplete 
 * var ContainerInputArray[3]: the original value from the page loading.
 *
 */  
function checkForChangedFieldValues(e, ContainerInputArray ) {
    var el = document.getElementById(ContainerInputArray[2]);
298
    var unhide = false;
299
    if ( el ) {
300 301
        if ( !ContainerInputArray[4]
             && (el.value != ContainerInputArray[3]
302
                 || (el.value == "" && el.id != "qa_contact")) )
303
        {
304 305 306
            unhide = true;
        }
        else {
307 308
            var set_default = document.getElementById("set_default_" +
                                                      ContainerInputArray[2]);
309 310 311
            if ( set_default ) {
                if(set_default.checked){
                    unhide = true;
312
                }
313 314 315 316
            }
        }
    }
    if(unhide){
317 318
        YAHOO.util.Dom.addClass(ContainerInputArray[0], 'bz_default_hidden');
        YAHOO.util.Dom.removeClass(ContainerInputArray[1], 'bz_default_hidden');
319 320 321 322 323 324
    }

}

function showPeopleOnChange( field_id_list ) {
    for(var i = 0; i < field_id_list.length; i++) {
325
        YAHOO.util.Event.addListener( field_id_list[i],'change', showEditableField,
326
                                      new Array('bz_qa_contact_edit_container',
327 328
                                                'bz_qa_contact_input'));
        YAHOO.util.Event.addListener( field_id_list[i],'change',showEditableField,
329
                                      new Array('bz_assignee_edit_container',
330
                                                'bz_assignee_input'));
331 332 333
    }
}

334 335 336 337 338 339 340 341 342 343 344 345 346
function assignToDefaultOnChange(field_id_list, default_assignee, default_qa_contact) {
    showPeopleOnChange(field_id_list);
    for(var i = 0, l = field_id_list.length; i < l; i++) {
        YAHOO.util.Event.addListener(field_id_list[i], 'change', function(evt, defaults) {
            if (document.getElementById('assigned_to').value == defaults[0]) {
                setDefaultCheckbox(evt, 'set_default_assignee');
            }
            if (document.getElementById('qa_contact')
                && document.getElementById('qa_contact').value == defaults[1])
            {
                setDefaultCheckbox(evt, 'set_default_qa_contact');
            }
        }, [default_assignee, default_qa_contact]);
347 348 349
    }
}

350
function initDefaultCheckbox(field_id){
351 352 353
    YAHOO.util.Event.addListener( 'set_default_' + field_id,'change', boldOnChange,
                                  'set_default_' + field_id);
    YAHOO.util.Event.addListener( window,'load', checkForChangedFieldValues,
354 355
                                  new Array( 'bz_' + field_id + '_edit_container',
                                             'bz_' + field_id + '_input',
356
                                             'set_default_' + field_id ,'1'));
357
    
358 359
    YAHOO.util.Event.addListener( window, 'load', boldOnChange,
                                 'set_default_' + field_id ); 
360 361
}

362 363 364 365 366
function showHideStatusItems(e, dupArrayInfo) {
    var el = document.getElementById('bug_status');
    // finish doing stuff based on the selection.
    if ( el ) {
        showDuplicateItem(el);
367 368 369 370 371

        // Make sure that fields whose visibility or values are controlled
        // by "resolution" behave properly when resolution is hidden.
        var resolution = document.getElementById('resolution');
        if (resolution && resolution.options[0].value != '') {
372
            resolution.bz_lastSelected = resolution.selectedIndex;
373 374 375 376
            var emptyOption = new Option('', '');
            resolution.insertBefore(emptyOption, resolution.options[0]);
            emptyOption.selected = true;
        }
377
        YAHOO.util.Dom.addClass('resolution_settings', 'bz_default_hidden');
378
        if (document.getElementById('resolution_settings_warning')) {
379 380
            YAHOO.util.Dom.addClass('resolution_settings_warning',
                                    'bz_default_hidden');
381
        }
382
        YAHOO.util.Dom.addClass('duplicate_display', 'bz_default_hidden');
383

384 385 386 387

        if ( (el.value == dupArrayInfo[1] && dupArrayInfo[0] == "is_duplicate")
             || bz_isValueInArray(close_status_array, el.value) ) 
        {
388 389 390 391
            YAHOO.util.Dom.removeClass('resolution_settings', 
                                       'bz_default_hidden');
            YAHOO.util.Dom.removeClass('resolution_settings_warning', 
                                       'bz_default_hidden');
392 393 394 395

            // Remove the blank option we inserted.
            if (resolution && resolution.options[0].value == '') {
                resolution.removeChild(resolution.options[0]);
396
                resolution.selectedIndex = resolution.bz_lastSelected;
397 398 399 400 401
            }
        }

        if (resolution) {
            bz_fireEvent(resolution, 'change');
402 403 404 405 406 407 408
        }
    }
}

function showDuplicateItem(e) {
    var resolution = document.getElementById('resolution');
    var bug_status = document.getElementById('bug_status');
409
    var dup_id = document.getElementById('dup_id');
410
    if (resolution) {
411
        if (resolution.value == 'DUPLICATE' && bz_isValueInArray( close_status_array, bug_status.value) ) {
412
            // hide resolution show duplicate
413 414 415
            YAHOO.util.Dom.removeClass('duplicate_settings', 
                                       'bz_default_hidden');
            YAHOO.util.Dom.addClass('dup_id_discoverable', 'bz_default_hidden');
416 417 418 419 420
            // check to make sure the field is visible or IE throws errors
            if( ! YAHOO.util.Dom.hasClass( dup_id, 'bz_default_hidden' ) ){
                dup_id.focus();
                dup_id.select();
            }
421 422
        }
        else {
423 424 425
            YAHOO.util.Dom.addClass('duplicate_settings', 'bz_default_hidden');
            YAHOO.util.Dom.removeClass('dup_id_discoverable', 
                                       'bz_default_hidden');
426
            dup_id.blur();
427 428 429 430 431 432 433 434
        }
    }
    YAHOO.util.Event.preventDefault(e); //prevents the hyperlink from going to the url in the href.
}

function setResolutionToDuplicate(e, duplicate_or_move_bug_status) {
    var status = document.getElementById('bug_status');
    var resolution = document.getElementById('resolution');
435
    YAHOO.util.Dom.addClass('dup_id_discoverable', 'bz_default_hidden');
436
    status.value = duplicate_or_move_bug_status;
437
    bz_fireEvent(status, 'change');
438
    resolution.value = "DUPLICATE";
439
    bz_fireEvent(resolution, 'change');
440 441 442
    YAHOO.util.Event.preventDefault(e);
}

443
function setDefaultCheckbox(e, field_id) {
444 445 446 447 448
    var el = document.getElementById(field_id);
    var elLabel = document.getElementById(field_id + "_label");
    if( el && elLabel ) {
        el.checked = "true";
        YAHOO.util.Dom.setStyle(elLabel, 'font-weight', 'bold');
449 450 451
    }
}

452 453 454 455 456 457 458 459 460 461 462
function boldOnChange(e, field_id){
    var el = document.getElementById(field_id);
    var elLabel = document.getElementById(field_id + "_label");
    if( el && elLabel ) {
        if( el.checked ){
            YAHOO.util.Dom.setStyle(elLabel, 'font-weight', 'bold');
        }
        else{
            YAHOO.util.Dom.setStyle(elLabel, 'font-weight', 'normal');
        }
    }
463
}
464

465
function updateCommentTagControl(checkbox, field) {
466
    if (checkbox.checked) {
467
        YAHOO.util.Dom.addClass(field, 'bz_private');
468
    } else {
469
        YAHOO.util.Dom.removeClass(field, 'bz_private');
470 471 472
    }
}

473 474 475 476 477 478 479 480 481 482 483 484 485 486
/**
 * Reset the value of the classification field and fire an event change
 * on it.  Called when the product changes, in case the classification
 * field (which is hidden) controls the visibility of any other fields.
 */
function setClassification() {
    var classification = document.getElementById('classification');
    var product = document.getElementById('product');
    var selected_product = product.value; 
    var select_classification = all_classifications[selected_product];
    classification.value = select_classification;
    bz_fireEvent(classification, 'change');
}

487 488 489 490 491
/**
 * Says that a field should only be displayed when another field has
 * a certain value. May only be called after the controller has already
 * been added to the DOM.
 */
492
function showFieldWhen(controlled_id, controller_id, values) {
493 494 495
    var controller = document.getElementById(controller_id);
    // Note that we don't get an object for "controlled" here, because it
    // might not yet exist in the DOM. We just pass along its id.
496 497
    YAHOO.util.Event.addListener(controller, 'change',
        handleVisControllerValueChange, [controlled_id, controller, values]);
498 499 500 501 502 503 504 505 506
}

/**
 * Called by showFieldWhen when a field's visibility controller 
 * changes values. 
 */
function handleVisControllerValueChange(e, args) {
    var controlled_id = args[0];
    var controller = args[1];
507
    var values = args[2];
508

509 510
    var field = document.getElementById(controlled_id);
    var label_container =
511 512 513
        document.getElementById('field_label_' + controlled_id);
    var field_container =
        document.getElementById('field_container_' + controlled_id);
514 515 516 517 518 519 520 521 522
    var selected = false;
    for (var i = 0; i < values.length; i++) {
        if (bz_valueSelected(controller, values[i])) {
            selected = true;
            break;
        }
    }

    if (selected) {
523 524
        YAHOO.util.Dom.removeClass(label_container, 'bz_hidden_field');
        YAHOO.util.Dom.removeClass(field_container, 'bz_hidden_field');
525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549
        /* If a custom field such as a textarea field contains some text, then
         * its content is visible by default as a readonly field (assuming that
         * the field is displayed). But if such a custom field contains no text,
         * then it's not displayed at all and an (edit) link is displayed instead.
         * This is problematic if the custom field is mandatory, because at least
         * Firefox complains that you must enter a value, but is unable to point
         * to the custom field because this one is hidden, and so the user has
         * to guess what the web browser is talking about, which is confusing.
         * So in that case, we display the custom field automatically instead of
         * the (edit) link, so that the user can enter some text in it.
         */
        var field_readonly = document.getElementById(controlled_id + '_readonly');

        if (!field_readonly) {
            var field_input = document.getElementById(controlled_id + '_input');
            var edit_container =
                document.getElementById(controlled_id + '_edit_container');

            if (field_input) {
                YAHOO.util.Dom.removeClass(field_input, 'bz_default_hidden');
            }
            if (edit_container) {
                YAHOO.util.Dom.addClass(edit_container, 'bz_hidden_field');
            }
        }
550 551 552 553 554
        // Restore the 'required' attribute for mandatory fields.
        if (field.getAttribute('data-required') == "true") {
            field.setAttribute('required', 'true');
            field.setAttribute('aria-required', 'true');
        }
555 556 557 558
    }
    else {
        YAHOO.util.Dom.addClass(label_container, 'bz_hidden_field');
        YAHOO.util.Dom.addClass(field_container, 'bz_hidden_field');
559 560 561 562 563
        // A hidden field must never be required, because the user cannot set it.
        if (field.getAttribute('data-required') == "true") {
            field.removeAttribute('required');
            field.removeAttribute('aria-required');
        }
564 565
    }
}
566

567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588
/**
 * 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)
589
{
590 591 592 593 594 595 596 597 598 599 600 601 602 603 604
    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;
    }
605 606 607
}

function handleValControllerChange(e, args) {
608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635
    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);
636
        }
637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688
    }

    // 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;
689
        }
690 691 692 693 694 695 696 697
        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;
698
    }
699 700 701 702 703

    YAHOO.util.Dom.addClass(option, 'bz_hidden_option');
    option.selected = false;
    option.disabled = true;
    hideOptionInIE(option, field);
704 705
}

706 707 708 709 710 711
// A convenience function to generate the "id" tag of an <option>
// based on the numeric id that Bugzilla uses for that value.
function _value_id(field_name, id) {
    return 'v' + id + '_' + field_name;
}

712 713 714 715 716 717 718 719
/*********************************/
/* Code for Hiding Options in IE */
/*********************************/

/* IE 7 and below (and some other browsers) don't respond to "display: none"
 * 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. */
720
var ie_hidden_options = {};
721 722 723 724
function hideOptionInIE(anOption, aSelect) {
    if (browserCanHideOptions(aSelect)) return;

    var commentNode = document.createComment(anOption.value);
725 726 727 728 729 730 731 732 733 734 735 736 737 738 739
    commentNode.id = anOption.id;
    // This keeps the interface of Comments and Options the same for
    // our other functions.
    commentNode.disabled = true;
    // replaceChild is very slow on IE in a <select> that has a lot of
    // options, so we use replaceNode when we can.
    if (anOption.replaceNode) {
        anOption.replaceNode(commentNode);
    }
    else {
        aSelect.replaceChild(commentNode, anOption);
    }

    // Store the comment node for quick access for getPossiblyHiddenOption
    if (!ie_hidden_options[aSelect.id]) {
740
        ie_hidden_options[aSelect.id] = {};
741 742
    }
    ie_hidden_options[aSelect.id][anOption.id] = commentNode;
743 744 745
}

function showOptionInIE(aNode, aSelect) {
746
    if (browserCanHideOptions(aSelect)) return aNode;
747 748 749 750 751 752

    // We do this crazy thing with innerHTML and createElement because
    // this is the ONLY WAY that this works properly in IE.
    var optionNode = document.createElement('option');
    optionNode.innerHTML = aNode.data;
    optionNode.value = aNode.data;
753 754 755 756 757 758 759 760 761 762 763
    optionNode.id = aNode.id;
    // replaceChild is very slow on IE in a <select> that has a lot of
    // options, so we use replaceNode when we can.
    if (aNode.replaceNode) {
        aNode.replaceNode(optionNode);
    }
    else {
        aSelect.replaceChild(optionNode, aNode);
    }
    delete ie_hidden_options[aSelect.id][optionNode.id];
    return optionNode;
764 765 766 767 768
}

function initHidingOptionsForIE(select_name) {
    var aSelect = document.getElementById(select_name);
    if (browserCanHideOptions(aSelect)) return;
769
    if (!aSelect) return;
770 771 772 773 774 775 776 777 778 779 780

    for (var i = 0; ;i++) {
        var item = aSelect.options[i];
        if (!item) break;
        if (item.disabled) {
          hideOptionInIE(item, aSelect);
          i--; // Hiding an option means that the options array has changed.
        }
    }
}

781 782 783 784 785 786 787 788 789 790 791 792 793
/* 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 = {};

794
function getPossiblyHiddenOption(aSelect, optionId) {
795 796 797 798 799 800 801

    if (bz_option_duplicates[aSelect.id]
        && bz_option_duplicates[aSelect.id][optionId])
    {
        optionId = bz_option_duplicates[aSelect.id][optionId];
    }

802 803 804 805 806 807 808 809 810
    // Works always for <option> tags, and works for commentNodes
    // in IE (but not in Webkit).
    var id = _value_id(aSelect.id, optionId);
    var val = document.getElementById(id);

    // This is for WebKit and other browsers that can't "display: none"
    // an <option> and also can't getElementById for a commentNode.
    if (!val && ie_hidden_options[aSelect.id]) {
        val = ie_hidden_options[aSelect.id][id];
811 812
    }

813 814 815 816
    // We add this property for our own convenience, it's used in
    // other places.
    val.bz_value_id = optionId;

817
    return val;
818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841
}

var browser_can_hide_options;
function browserCanHideOptions(aSelect) {
    /* As far as I can tell, browsers that don't hide <option> tags
     * also never have a X position for <option> tags, even if
     * they're visible. This is the only reliable way I found to
     * differentiate browsers. So we create a visible option, see
     * if it has a position, and then remove it. */
    if (typeof(browser_can_hide_options) == "undefined") {
        var new_opt = bz_createOptionInSelect(aSelect, '', '');
        var opt_pos = YAHOO.util.Dom.getX(new_opt);
        aSelect.removeChild(new_opt);
        if (opt_pos) {
            browser_can_hide_options = true;
        }
        else {
            browser_can_hide_options = false;
        }
    }
    return browser_can_hide_options;
}

/* (end) option hiding code */
842 843 844 845 846 847 848 849 850 851 852 853 854 855 856

/**
 * The Autoselect
 */
YAHOO.bugzilla.userAutocomplete = {
    counter : 0,
    dataSource : null,
    generateRequest : function ( enteredText ){ 
      YAHOO.bugzilla.userAutocomplete.counter = 
                                   YAHOO.bugzilla.userAutocomplete.counter + 1;
      YAHOO.util.Connect.setDefaultPostHeader('application/json', true);
      var json_object = {
          method : "User.get",
          id : YAHOO.bugzilla.userAutocomplete.counter,
          params : [ { 
857
            Bugzilla_api_token: BUGZILLA.api_token,
858
            match : [ decodeURIComponent(enteredText) ],
859
            include_fields : [ "name", "real_name" ]
860 861 862 863 864 865 866 867 868
          } ]
      };
      var stringified =  YAHOO.lang.JSON.stringify(json_object);
      var debug = { msg: "json-rpc obj debug info", "json obj": json_object, 
                    "param" : stringified}
      YAHOO.bugzilla.userAutocomplete.debug_helper( debug );
      return stringified;
    },
    resultListFormat : function(oResultData, enteredText, sResultMatch) {
869 870
        return ( YAHOO.lang.escapeHTML(oResultData.real_name) + " ("
                 + YAHOO.lang.escapeHTML(oResultData.name) + ")");
871 872 873 874 875 876 877 878 879 880
    },
    debug_helper : function ( ){
        /* used to help debug any errors that might happen */
        if( typeof(console) !== 'undefined' && console != null && arguments.length > 0 ){
            console.log("debug helper info:", arguments);
        }
        return true;
    },    
    init_ds : function(){
        this.dataSource = new YAHOO.util.XHRDataSource("jsonrpc.cgi");
881
        this.dataSource.connTimeout = 30000;
882
        this.dataSource.connMethodPost = true;
883 884
        this.dataSource.connXhrMode = "cancelStaleRequests";
        this.dataSource.maxCacheEntries = 5;
885 886
        this.dataSource.responseSchema = {
            resultsList : "result.users",
887 888
            metaFields : { error: "error", jsonRpcId: "id"},
            fields : [
889
                { key : "name" },
890 891 892
                { key : "real_name"}
            ]
        };
893 894 895 896 897 898 899 900
    },
    init : function( field, container, multiple ) {
        if( this.dataSource == null ){
            this.init_ds();  
        }            
        var userAutoComp = new YAHOO.widget.AutoComplete( field, container, 
                                this.dataSource );
        // other stuff we might want to do with the autocomplete goes here
901
        userAutoComp.maxResultsDisplayed = BUGZILLA.param.maxusermatches;
902 903 904 905 906 907 908
        userAutoComp.generateRequest = this.generateRequest;
        userAutoComp.formatResult = this.resultListFormat;
        userAutoComp.doBeforeLoadData = this.debug_helper;
        userAutoComp.minQueryLength = 3;
        userAutoComp.autoHighlight = false;
        // this is a throttle to determine the delay of the query from typing
        // set this higher to cause fewer calls to the server
909 910
        userAutoComp.queryDelay = 0.05;
        userAutoComp.useIFrame = true;
911 912
        userAutoComp.resultTypeList = false;
        if( multiple == true ){
913
            userAutoComp.delimChar = [","];
914 915 916 917 918
        }
        
    }
};

919 920 921 922 923
YAHOO.bugzilla.fieldAutocomplete = {
    dataSource : [],
    init_ds : function( field ) {
        this.dataSource[field] =
          new YAHOO.util.LocalDataSource( YAHOO.bugzilla.field_array[field] );
924 925
    },
    init : function( field, container ) {
926 927
        if( this.dataSource[field] == null ) {
            this.init_ds( field );
928
        }
929 930 931
        var fieldAutoComp =
          new YAHOO.widget.AutoComplete(field, container, this.dataSource[field]);
        fieldAutoComp.maxResultsDisplayed = YAHOO.bugzilla.field_array[field].length;
932
        fieldAutoComp.formatResult = fieldAutoComp.formatEscapedResult;
933 934 935 936 937 938
        fieldAutoComp.minQueryLength = 0;
        fieldAutoComp.useIFrame = true;
        fieldAutoComp.delimChar = [","," "];
        fieldAutoComp.resultTypeList = false;
        fieldAutoComp.queryDelay = 0;
        /*  Causes all the possibilities in the field to appear when a user
939 940
         *  focuses on the textbox 
         */
941 942
        fieldAutoComp.textboxFocusEvent.subscribe( function(){
            var sInputValue = YAHOO.util.Dom.get(field).value;
943 944
            if( sInputValue.length === 0
                && YAHOO.bugzilla.field_array[field].length > 0 ){
945 946 947 948 949
                this.sendQuery(sInputValue);
                this.collapseContainer();
                this.expandContainer();
            }
        });
950 951 952
        fieldAutoComp.dataRequestEvent.subscribe( function(type, args) {
            args[0].autoHighlight = args[1] != '';
        });
953 954
    }
};
955 956 957 958 959 960 961 962 963 964 965 966 967

/**
 * Set the disable email checkbox to true if the user has disabled text
 */
function userDisabledTextOnChange(disabledtext) {
    var disable_mail = document.getElementById('disable_mail');
    if (disabledtext.value === "" && !disable_mail_manually_set) {
        disable_mail.checked = false;
    }
    if (disabledtext.value !== "" && !disable_mail_manually_set) {
        disable_mail.checked = true;
    }
}
968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015

/**
 * Force the browser to honour the selected option when a page is refreshed,
 * but only if the user hasn't explicitly selected a different option.
 */
function initDirtyFieldTracking() {
    // old IE versions don't provide the information we need to make this fix work
    // however they aren't affected by this issue, so it's ok to ignore them
    if (YAHOO.env.ua.ie > 0 && YAHOO.env.ua.ie <= 8) return;
    var selects = document.getElementById('changeform').getElementsByTagName('select');
    for (var i = 0, l = selects.length; i < l; i++) {
        var el = selects[i];
        var el_dirty = document.getElementById(el.name + '_dirty');
        if (!el_dirty) continue;
        if (!el_dirty.value) {
            var preSelected = bz_preselectedOptions(el);
            if (!el.multiple) {
                preSelected.selected = true;
            } else {
                el.selectedIndex = -1;
                for (var j = 0, m = preSelected.length; j < m; j++) {
                    preSelected[j].selected = true;
                }
            }
        }
        YAHOO.util.Event.on(el, "change", function(e) {
            var el = e.target || e.srcElement;
            var preSelected = bz_preselectedOptions(el);
            var currentSelected = bz_selectedOptions(el);
            var isDirty = false;
            if (!el.multiple) {
                isDirty = preSelected.index != currentSelected.index;
            } else {
                if (preSelected.length != currentSelected.length) {
                    isDirty = true;
                } else {
                    for (var i = 0, l = preSelected.length; i < l; i++) {
                        if (currentSelected[i].index != preSelected[i].index) {
                            isDirty = true;
                            break;
                        }
                    }
                }
            }
            document.getElementById(el.name + '_dirty').value = isDirty ? '1' : '';
        });
    }
}
1016 1017 1018 1019 1020 1021 1022

/**
 * Comment preview
 */

var last_comment_text = '';

1023
function show_comment_preview(bug_id) {
1024 1025 1026
    var Dom = YAHOO.util.Dom;
    var comment = document.getElementById('comment');
    var preview = document.getElementById('comment_preview');
1027

1028
    if (!comment || !preview) return;
1029
    if (Dom.hasClass('comment_preview_tab', 'active_comment_tab')) return;
1030

1031 1032
    preview.style.width = (comment.clientWidth - 4) + 'px';
    preview.style.height = comment.offsetHeight + 'px';
1033

1034
    var comment_tab = document.getElementById('comment_tab');
1035
    Dom.addClass(comment, 'bz_default_hidden');
1036 1037 1038 1039
    Dom.removeClass(comment_tab, 'active_comment_tab');
    comment_tab.setAttribute('aria-selected', 'false');

    var preview_tab = document.getElementById('comment_preview_tab');
1040
    Dom.removeClass(preview, 'bz_default_hidden');
1041 1042
    Dom.addClass(preview_tab, 'active_comment_tab');
    preview_tab.setAttribute('aria-selected', 'true');
1043 1044 1045

    Dom.addClass('comment_preview_error', 'bz_default_hidden');

1046
    if (last_comment_text == comment.value)
1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079
        return;

    Dom.addClass('comment_preview_text', 'bz_default_hidden');
    Dom.removeClass('comment_preview_loading', 'bz_default_hidden');

    YAHOO.util.Connect.setDefaultPostHeader('application/json', true);
    YAHOO.util.Connect.asyncRequest('POST', 'jsonrpc.cgi',
    {
        success: function(res) {
            data = YAHOO.lang.JSON.parse(res.responseText);
            if (data.error) {
                Dom.addClass('comment_preview_loading', 'bz_default_hidden');
                Dom.removeClass('comment_preview_error', 'bz_default_hidden');
                Dom.get('comment_preview_error').innerHTML =
                    YAHOO.lang.escapeHTML(data.error.message);
            } else {
                document.getElementById('comment_preview_text').innerHTML = data.result.html;
                Dom.addClass('comment_preview_loading', 'bz_default_hidden');
                Dom.removeClass('comment_preview_text', 'bz_default_hidden');
                last_comment_text = comment.value;
            }
        },
        failure: function(res) {
            Dom.addClass('comment_preview_loading', 'bz_default_hidden');
            Dom.removeClass('comment_preview_error', 'bz_default_hidden');
            Dom.get('comment_preview_error').innerHTML =
                YAHOO.lang.escapeHTML(res.responseText);
        }
    },
    YAHOO.lang.JSON.stringify({
        version: "1.1",
        method: 'Bug.render_comment',
        params: {
1080
            Bugzilla_api_token: BUGZILLA.api_token,
1081
            id: bug_id,
1082
            text: comment.value
1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093
        }
    })
    );
}

function show_comment_edit() {
    var comment = document.getElementById('comment');
    var preview = document.getElementById('comment_preview');
    if (!comment || !preview) return;
    if (YAHOO.util.Dom.hasClass(comment, 'active_comment_tab')) return;

1094
    var preview_tab = document.getElementById('comment_preview_tab');
1095
    YAHOO.util.Dom.addClass(preview, 'bz_default_hidden');
1096 1097 1098 1099
    YAHOO.util.Dom.removeClass(preview_tab, 'active_comment_tab');
    preview_tab.setAttribute('aria-selected', 'false');

    var comment_tab = document.getElementById('comment_tab');
1100
    YAHOO.util.Dom.removeClass(comment, 'bz_default_hidden');
1101 1102
    YAHOO.util.Dom.addClass(comment_tab, 'active_comment_tab');
    comment_tab.setAttribute('aria-selected', 'true');
1103
}
1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116

function adjustRemainingTime() {
    // subtracts time spent from remaining time
    // prevent negative values if work_time > bz_remaining_time
    var new_time = Math.max(bz_remaining_time - document.changeform.work_time.value, 0.0);
    // get upto 2 decimal places
    document.changeform.remaining_time.value = Math.round(new_time * 100)/100;
}

function updateRemainingTime() {
    // if the remaining time is changed manually, update bz_remaining_time
    bz_remaining_time = document.changeform.remaining_time.value;
}