PageRenderTime 116ms CodeModel.GetById 42ms app.highlight 61ms RepoModel.GetById 1ms app.codeStats 1ms

/hudson-war/src/main/webapp/scripts/hudson-behavior.js

http://github.com/hudson/hudson
JavaScript | 1943 lines | 1387 code | 263 blank | 293 comment | 301 complexity | 77e8a188c22bbc744e5c43a65da094b1 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   1/*
   2 * The MIT License
   3 * 
   4 * Copyright (c) 2004-2011, Oracle Corporation, Kohsuke Kawaguchi,
   5 * Daniel Dyer, Yahoo! Inc., Alan Harder, InfraDNA, Inc., Anton Kozak
   6 * 
   7 * Permission is hereby granted, free of charge, to any person obtaining a copy
   8 * of this software and associated documentation files (the "Software"), to deal
   9 * in the Software without restriction, including without limitation the rights
  10 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  11 * copies of the Software, and to permit persons to whom the Software is
  12 * furnished to do so, subject to the following conditions:
  13 * 
  14 * The above copyright notice and this permission notice shall be included in
  15 * all copies or substantial portions of the Software.
  16 * 
  17 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  18 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  19 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  20 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  21 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  22 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  23 * THE SOFTWARE.
  24 */
  25//
  26//
  27// JavaScript for Hudson
  28//     See http://www.ibm.com/developerworks/web/library/wa-memleak/?ca=dgr-lnxw97JavascriptLeaks
  29//     for memory leak patterns and how to prevent them.
  30//
  31
  32// We use both jQuery and protoype. Both defines $ as an alias in the global namespace.
  33// By specifying jQuery.noConflict(), $ is no longer used as alias. Use jQuery("div").hide();
  34// instead of $("div").hide();
  35// http://docs.jquery.com/Using_jQuery_with_Other_Libraries
  36
  37jQuery.noConflict();
  38
  39
  40// create a new object whose prototype is the given object
  41function object(o) {
  42    function F() {}
  43    F.prototype = o;
  44    return new F();
  45}
  46
  47
  48
  49// id generator
  50var iota = 0;
  51
  52// crumb information
  53var crumb = {
  54    fieldName: null,
  55    value: null,
  56
  57    init: function(crumbField, crumbValue) {
  58        if (crumbField=="") return; // layout.jelly passes in "" whereas it means null.
  59        this.fieldName = crumbField;
  60        this.value = crumbValue;
  61    },
  62
  63    /**
  64     * Adds the crumb value into the given hash or array and returns it.
  65     */
  66    wrap: function(headers) {
  67        if (this.fieldName!=null) {
  68            if (headers instanceof Array)
  69                headers.push(this.fieldName, this.value);
  70            else
  71                headers[this.fieldName]=this.value;
  72        }
  73        return headers;
  74    },
  75
  76    /**
  77     * Puts a hidden input field to the form so that the form submission will have the crumb value
  78     */
  79    appendToForm : function(form) {
  80        if(this.fieldName==null)    return; // noop
  81        var div = document.createElement("div");
  82        div.innerHTML = "<input type=hidden name='"+this.fieldName+"' value='"+this.value+"'>";
  83        form.appendChild(div);
  84    }
  85}
  86
  87// Form check code
  88//========================================================
  89var FormChecker = {
  90    // pending requests
  91    queue : [],
  92
  93    // conceptually boolean, but doing so create concurrency problem.
  94    // that is, during unit tests, the AJAX.send works synchronously, so
  95    // the onComplete happens before the send method returns. On a real environment,
  96    // more likely it's the other way around. So setting a boolean flag to true or false
  97    // won't work.
  98    inProgress : 0,
  99
 100    /**
 101     * Schedules a form field check. Executions are serialized to reduce the bandwidth impact.
 102     *
 103     * @param url
 104     *      Remote doXYZ URL that performs the check. Query string should include the field value.
 105     * @param method
 106     *      HTTP method. GET or POST. I haven't confirmed specifics, but some browsers seem to cache GET requests.
 107     * @param target
 108     *      HTML element whose innerHTML will be overwritten when the check is completed.
 109     */
 110    delayedCheck : function(url, method, target) {
 111        if(url==null || method==null || target==null)
 112            return; // don't know whether we should throw an exception or ignore this. some broken plugins have illegal parameters
 113        this.queue.push({url:url, method:method, target:target});
 114        this.schedule();
 115    },
 116
 117    sendRequest : function(url, params) {
 118        if (params.method == "post") {
 119            var idx = url.indexOf('?');
 120            params.parameters = url.substring(idx + 1);
 121            url = url.substring(0, idx);
 122        }
 123        new Ajax.Request(url, params);
 124    },
 125
 126    schedule : function() {
 127        if (this.inProgress>0)  return;
 128        if (this.queue.length == 0) return;
 129
 130        var next = this.queue.shift();
 131        this.sendRequest(next.url, {
 132            method : next.method,
 133            onComplete : function(x) {
 134                var i;
 135                next.target.innerHTML = x.status==200 ? x.responseText
 136                    : '<a href="" onclick="document.getElementById(\'valerr' + (i=iota++)
 137                    + '\').style.display=\'block\';return false">ERROR</a><div id="valerr'
 138                    + i + '" style="display:none">' + x.responseText + '</div>';
 139                Behaviour.applySubtree(next.target);
 140                FormChecker.inProgress--;
 141                FormChecker.schedule();
 142            }
 143        });
 144        this.inProgress++;
 145    }
 146}
 147
 148/**
 149 * Find the sibling (in the sense of the structured form submission) form item of the given name,
 150 * and returns that DOM node.
 151 *
 152 * @param {HTMLElement} e
 153 * @param {string} name
 154 *      Name of the control to find. Can include "../../" etc in the prefix.
 155 *      See @RelativePath.
 156 */
 157function findNearBy(e,name) {
 158    while (name.startsWith("../")) {
 159        name = name.substring(3);
 160        e = findFormParent(e,null,true);
 161    }
 162
 163    // does 'e' itself match the criteria?
 164    // as some plugins use the field name as a parameter value, instead of 'value'
 165    var p = findFormItem(e,name,function(e,filter) {
 166        if (filter(e))    return e;
 167        return null;
 168    });
 169    if (p!=null)    return p;
 170
 171    var owner = findFormParent(e,null,true);
 172
 173    p = findPreviousFormItem(e,name);
 174    if (p!=null && findFormParent(p,null,true)==owner)
 175        return p;
 176
 177    var n = findNextFormItem(e,name);
 178    if (n!=null && findFormParent(n,null,true)==owner)
 179        return n;
 180
 181    return null; // not found
 182}
 183
 184function controlValue(e) {
 185    if (e==null)    return null;
 186    // compute the form validation value to be sent to the server
 187    var type = e.getAttribute("type");
 188    if(type!=null && type.toLowerCase()=="checkbox")
 189        return e.checked;
 190    return e.value;
 191}
 192
 193function toValue(e) {
 194    return encodeURIComponent(controlValue(e));
 195}
 196
 197/**
 198 * Builds a query string in a fluent API pattern.
 199 * @param {HTMLElement} owner
 200 *      The 'this' control.
 201 */
 202function qs(owner) {
 203    return {
 204        params : "",
 205
 206        append : function(s) {
 207            if (this.params.length==0)  this.params+='?';
 208            else                        this.params+='&';
 209            this.params += s;
 210            return this;
 211        },
 212
 213        nearBy : function(name) {
 214            var e = findNearBy(owner,name);
 215            if (e==null)    return this;    // skip
 216            return this.append(Path.tail(name)+'='+toValue(e));
 217        },
 218
 219        addThis : function() {
 220            return this.append("value="+toValue(owner));
 221        },
 222
 223        toString : function() {
 224            return this.params;
 225        }
 226    };
 227}
 228
 229// find the nearest ancestor node that has the given tag name
 230function findAncestor(e, tagName) {
 231    do {
 232        e = e.parentNode;
 233    } while (e != null && e.tagName != tagName);
 234    return e;
 235}
 236
 237function findAncestorClass(e, cssClass) {
 238    do {
 239        e = e.parentNode;
 240    } while (e != null && !Element.hasClassName(e,cssClass));
 241    return e;
 242}
 243
 244function findFollowingTR(input, className) {
 245    // identify the parent TR
 246    var tr = input;
 247    while (tr.tagName != "TR")
 248        tr = tr.parentNode;
 249
 250    // then next TR that matches the CSS
 251    do {
 252        tr = tr.nextSibling;
 253    } while (tr != null && (tr.tagName != "TR" || !Element.hasClassName(tr,className)));
 254
 255    return tr;
 256}
 257
 258function find(src,filter,traversalF) {
 259    while(src!=null) {
 260        src = traversalF(src);
 261        if(src!=null && filter(src))
 262            return src;
 263    }
 264    return null;
 265}
 266
 267/**
 268 * Traverses a form in the reverse document order starting from the given element (but excluding it),
 269 * until the given filter matches, or run out of an element.
 270 */
 271function findPrevious(src,filter) {
 272    return find(src,filter,function (e) {
 273        var p = e.previousSibling;
 274        if(p==null) return e.parentNode;
 275        while(p.lastChild!=null)
 276            p = p.lastChild;
 277        return p;
 278    });
 279}
 280
 281function findNext(src,filter) {
 282    return find(src,filter,function (e) {
 283        var n = e.nextSibling;
 284        if(n==null) return e.parentNode;
 285        while(n.firstChild!=null)
 286            n = n.firstChild;
 287        return n;
 288    });
 289}
 290
 291function findFormItem(src,name,directionF) {
 292    var name2 = "_."+name; // handles <textbox field="..." /> notation silently
 293    return directionF(src,function(e){ return (e.tagName=="INPUT" || e.tagName=="TEXTAREA" || e.tagName=="SELECT") && (e.name==name || e.name==name2); });
 294}
 295
 296/**
 297 * Traverses a form in the reverse document order and finds an INPUT element that matches the given name.
 298 */
 299function findPreviousFormItem(src,name) {
 300    return findFormItem(src,name,findPrevious);
 301}
 302
 303function findNextFormItem(src,name) {
 304    return findFormItem(src,name,findNext);
 305}
 306
 307/**
 308 * Parse HTML into DOM.
 309 */
 310function parseHtml(html) {
 311    var c = document.createElement("div");
 312    c.innerHTML = html;
 313    return c.firstChild;
 314}
 315
 316/**
 317 * Emulate the firing of an event.
 318 *
 319 * @param {HTMLElement} element
 320 *      The element that will fire the event
 321 * @param {String} event
 322 *      like 'change', 'blur', etc.
 323 */
 324function fireEvent(element,event){
 325    if (document.createEvent) {
 326        // dispatch for firefox + others
 327        var evt = document.createEvent("HTMLEvents");
 328        evt.initEvent(event, true, true ); // event type,bubbling,cancelable
 329        return !element.dispatchEvent(evt);
 330    } else {
 331        // dispatch for IE
 332        var evt = document.createEventObject();
 333        return element.fireEvent('on'+event,evt)
 334    }
 335}
 336
 337// shared tooltip object
 338var tooltip;
 339
 340
 341
 342// Behavior rules
 343//========================================================
 344// using tag names in CSS selector makes the processing faster
 345function registerValidator(e) {
 346    e.targetElement = findFollowingTR(e, "validation-error-area").firstChild.nextSibling;
 347    e.targetUrl = function() {
 348        return eval(this.getAttribute("checkUrl"));
 349    };
 350    var method = e.getAttribute("checkMethod");
 351    if (!method) method = "get";
 352
 353    var url = e.targetUrl();
 354    try {
 355      FormChecker.delayedCheck(url, method, e.targetElement);
 356    } catch (x) {
 357        // this happens if the checkUrl refers to a non-existing element.
 358        // don't let this kill off the entire JavaScript
 359        YAHOO.log("Failed to register validation method: "+e.getAttribute("checkUrl")+" : "+e);
 360        return;
 361    }
 362
 363    var checker = function() {
 364        var target = this.targetElement;
 365        FormChecker.sendRequest(this.targetUrl(), {
 366            method : method,
 367            onComplete : function(x) {
 368                target.innerHTML = x.responseText;
 369                Behaviour.applySubtree(target);
 370            }
 371        });
 372    }
 373    var oldOnchange = e.onchange;
 374    if(typeof oldOnchange=="function") {
 375        e.onchange = function() { checker.call(this); oldOnchange.call(this); }
 376    } else
 377        e.onchange = checker;
 378    e.onblur = checker;
 379
 380    e = null; // avoid memory leak
 381}
 382
 383function registerRegexpValidator(e,regexp,message) {
 384    e.targetElement = findFollowingTR(e, "validation-error-area").firstChild.nextSibling;
 385    var checkMessage = e.getAttribute('checkMessage');
 386    if (checkMessage) message = checkMessage;
 387    var oldOnchange = e.onchange;
 388    e.onchange = function() {
 389        var set = oldOnchange != null ? oldOnchange.call(this) : false;
 390        if (this.value.match(regexp)) {
 391            if (!set) this.targetElement.innerHTML = "";
 392        } else {
 393            this.targetElement.innerHTML = "<div class=error>" + message + "</div>";
 394            set = true;
 395        }
 396        return set;
 397    }
 398    e.onchange.call(e);
 399    e = null; // avoid memory leak
 400}
 401
 402/**
 403 * Wraps a <button> into YUI button.
 404 *
 405 * @param e
 406 *      button element
 407 * @param onclick
 408 *      onclick handler
 409 */
 410function makeButton(e,onclick) {
 411    var h = e.onclick;
 412    var clsName = e.className;
 413    var n = e.name;
 414    var btn = new YAHOO.widget.Button(e,{});
 415    if(onclick!=null)
 416        btn.addListener("click",onclick);
 417    if(h!=null)
 418        btn.addListener("click",h);
 419    var be = btn.get("element");
 420    Element.addClassName(be,clsName);
 421    if(n!=null) // copy the name
 422        be.setAttribute("name",n);
 423    return btn;
 424}
 425
 426/*
 427    If we are inside 'to-be-removed' class, some HTML altering behaviors interact badly, because
 428    the behavior re-executes when the removed master copy gets reinserted later.
 429 */
 430function isInsideRemovable(e) {
 431    return Element.ancestors(e).find(function(f){return f.hasClassName("to-be-removed");});
 432}
 433
 434var hudsonRules = {
 435    "BODY" : function() {
 436        tooltip = new YAHOO.widget.Tooltip("tt", {context:[], zindex:999});
 437    },
 438
 439// do the ones that extract innerHTML so that they can get their original HTML before
 440// other behavior rules change them (like YUI buttons.)
 441
 442    "DIV.hetero-list-container" : function(e) {
 443        if(isInsideRemovable(e))    return;
 444
 445        // components for the add button
 446        var menu = document.createElement("SELECT");
 447        var btns = findElementsBySelector(e,"INPUT.hetero-list-add"),
 448            btn = btns[btns.length-1]; // In case nested content also uses hetero-list
 449        YAHOO.util.Dom.insertAfter(menu,btn);
 450
 451        var prototypes = e.lastChild;
 452        while(!Element.hasClassName(prototypes,"prototypes"))
 453            prototypes = prototypes.previousSibling;
 454        var insertionPoint = prototypes.previousSibling;    // this is where the new item is inserted.
 455
 456        // extract templates
 457        var templates = []; var i=0;
 458        for(var n=prototypes.firstChild;n!=null;n=n.nextSibling,i++) {
 459            var name = n.getAttribute("name");
 460            var tooltip = n.getAttribute("tooltip");
 461            menu.options[i] = new Option(n.getAttribute("title"),""+i);
 462            templates.push({html:n.innerHTML, name:name, tooltip:tooltip});
 463        }
 464        Element.remove(prototypes);
 465
 466        var withDragDrop = initContainerDD(e);
 467
 468        var menuButton = new YAHOO.widget.Button(btn, { type: "menu", menu: menu });
 469        menuButton.getMenu().clickEvent.subscribe(function(type,args,value) {
 470            var t = templates[parseInt(args[1].value)]; // where this args[1] comes is a real mystery
 471
 472            var nc = document.createElement("div");
 473            nc.className = "repeated-chunk";
 474            nc.setAttribute("name",t.name);
 475            nc.innerHTML = t.html;
 476            insertionPoint.parentNode.insertBefore(nc, insertionPoint);
 477            if(withDragDrop)    prepareDD(nc);
 478
 479            hudsonRules['DIV.repeated-chunk'](nc);  // applySubtree doesn't get nc itself
 480            Behaviour.applySubtree(nc);
 481        });
 482
 483        menuButton.getMenu().renderEvent.subscribe(function(type,args,value) {
 484            // hook up tooltip for menu items
 485            var items = menuButton.getMenu().getItems();
 486            for(i=0; i<items.length; i++) {
 487                var t = templates[i].tooltip;
 488                if(t!=null)
 489                    applyTooltip(items[i].element,t);
 490            }
 491        });
 492    },
 493
 494    "DIV.repeated-container" : function(e) {
 495        if(isInsideRemovable(e))    return;
 496
 497        // compute the insertion point
 498        var ip = e.lastChild;
 499        while (!Element.hasClassName(ip, "repeatable-insertion-point"))
 500            ip = ip.previousSibling;
 501        // set up the logic
 502        object(repeatableSupport).init(e, e.firstChild, ip);
 503    },
 504
 505
 506    "TABLE.sortable" : function(e) {// sortable table
 507        ts_makeSortable(e);
 508    },
 509
 510    "TABLE.progress-bar" : function(e) {// sortable table
 511        e.onclick = function() {
 512            var href = this.getAttribute("href");
 513            if(href!=null)      window.location = href;
 514        }
 515        e = null; // avoid memory leak
 516    },
 517
 518    "INPUT.advancedButton" : function(e) {
 519        makeButton(e,function(e) {
 520            var link = e.target;
 521            while(!Element.hasClassName(link,"advancedLink"))
 522                link = link.parentNode;
 523            link.style.display = "none"; // hide the button
 524
 525            var container = link.nextSibling.firstChild; // TABLE -> TBODY
 526
 527            var tr = link;
 528            while (tr.tagName != "TR")
 529                tr = tr.parentNode;
 530
 531            // move the contents of the advanced portion into the main table
 532            var nameRef = tr.getAttribute("nameref");
 533            while (container.lastChild != null) {
 534                var row = container.lastChild;
 535                if(nameRef!=null && row.getAttribute("nameref")==null){
 536                    row.setAttribute("nameref",nameRef); // to handle inner rowSets, don't override existing values
 537                }
 538                if(Element.hasClassName(tr,"modified")){
 539                    addModifiedClass(row);
 540                }
 541                tr.parentNode.insertBefore(row, tr.nextSibling);
 542            }
 543        });
 544        e = null; // avoid memory leak
 545    },
 546
 547    "INPUT.expandButton" : function(e) {
 548        makeButton(e,function(e) {
 549            var link = e.target;
 550            while(!Element.hasClassName(link,"advancedLink"))
 551                link = link.parentNode;
 552            link.style.display = "none";
 553            link.nextSibling.style.display="block";
 554        });
 555        e = null; // avoid memory leak
 556    },
 557
 558// scripting for having default value in the input field
 559    "INPUT.has-default-text" : function(e) {
 560        var defaultValue = e.value;
 561        Element.addClassName(e, "defaulted");
 562        e.onfocus = function() {
 563            if (this.value == defaultValue) {
 564                this.value = "";
 565                Element.removeClassName(this, "defaulted");
 566            }
 567        }
 568        e.onblur = function() {
 569            if (this.value == "") {
 570                this.value = defaultValue;
 571                Element.addClassName(this, "defaulted");
 572            }
 573        }
 574        e = null; // avoid memory leak
 575    },
 576
 577// <label> that doesn't use ID, so that it can be copied in <repeatable>
 578    "LABEL.attach-previous" : function(e) {
 579        e.onclick = function() {
 580            var e = this.previousSibling;
 581            while (e!=null) {
 582                if (e.tagName=="INPUT") {
 583                    e.click();
 584                    break;
 585                }
 586                e = e.previousSibling;
 587            }
 588        }
 589        e = null;
 590    },
 591
 592// form fields that are validated via AJAX call to the server
 593// elements with this class should have two attributes 'checkUrl' that evaluates to the server URL.
 594    "INPUT.validated" : registerValidator,
 595    "SELECT.validated" : registerValidator,
 596    "TEXTAREA.validated" : registerValidator,
 597
 598// validate required form values
 599    "INPUT.required" : function(e) { registerRegexpValidator(e,/./,"Field is required"); },
 600
 601// validate form values to be a number
 602    "INPUT.number" : function(e) { registerRegexpValidator(e,/^(\d+|)$/,"Not a number"); },
 603    "INPUT.positive-number" : function(e) {
 604        registerRegexpValidator(e,/^(\d*[1-9]\d*|)$/,"Not a positive number");
 605    },
 606
 607    "INPUT.auto-complete": function(e) {// form field with auto-completion support 
 608        // insert the auto-completion container
 609        var div = document.createElement("DIV");
 610        e.parentNode.insertBefore(div,e.nextSibling);
 611        e.style.position = "relative"; // or else by default it's absolutely positioned, making "width:100%" break
 612
 613        var ds = new YAHOO.widget.DS_XHR(e.getAttribute("autoCompleteUrl"),["suggestions","name"]);
 614        ds.scriptQueryParam = "value";
 615        
 616        // Instantiate the AutoComplete
 617        var ac = new YAHOO.widget.AutoComplete(e, div, ds);
 618        ac.prehighlightClassName = "yui-ac-prehighlight";
 619        ac.animSpeed = 0;
 620        ac.useShadow = true;
 621        ac.autoSnapContainer = true;
 622        ac.delimChar = e.getAttribute("autoCompleteDelimChar");
 623        ac.doBeforeExpandContainer = function(textbox,container) {// adjust the width every time we show it
 624            container.style.width=textbox.clientWidth+"px";
 625            var Dom = YAHOO.util.Dom;
 626            Dom.setXY(container, [Dom.getX(textbox), Dom.getY(textbox) + textbox.offsetHeight] );
 627            return true;
 628        }
 629    },
 630
 631    "A.help-button" : function(e) {
 632        e.onclick = function() {
 633            var tr = findFollowingTR(this, "help-area");
 634            var div = tr.firstChild.nextSibling.firstChild;
 635
 636            if (div.style.display != "block") {
 637                div.style.display = "block";
 638                // make it visible
 639                new Ajax.Request(this.getAttribute("helpURL"), {
 640                    method : 'get',
 641                    onSuccess : function(x) {
 642                        div.innerHTML = x.responseText;
 643                    },
 644                    onFailure : function(x) {
 645                        div.innerHTML = "<b>ERROR</b>: Failed to load help file: " + x.statusText;
 646                    }
 647                });
 648            } else {
 649                div.style.display = "none";
 650            }
 651
 652            return false;
 653        };
 654        e.tabIndex = 9999; // make help link unnavigable from keyboard
 655        e = null; // avoid memory leak
 656    },
 657
 658// deferred client-side clickable map.
 659// this is useful where the generation of <map> element is time consuming
 660    "IMG[lazymap]" : function(e) {
 661        new Ajax.Request(
 662            e.getAttribute("lazymap"),
 663            {
 664                method : 'get',
 665                onSuccess : function(x) {
 666                    var div = document.createElement("div");
 667                    document.body.appendChild(div);
 668                    div.innerHTML = x.responseText;
 669                    var id = "map" + (iota++);
 670                    div.firstChild.setAttribute("name", id);
 671                    e.setAttribute("usemap", "#" + id);
 672                }
 673            });
 674    },
 675
 676    // button to add a new repeatable block
 677    "INPUT.repeatable-add" : function(e) {
 678        makeButton(e,function(e) {
 679            repeatableSupport.onAdd(e.target);
 680        });
 681        e = null; // avoid memory leak
 682    },
 683
 684    "INPUT.repeatable-delete" : function(e) {
 685        makeButton(e,function(e) {
 686            repeatableSupport.onDelete(e.target);
 687        });
 688        e = null; // avoid memory leak
 689    },
 690
 691    // resizable text area
 692    "TEXTAREA" : function(textarea) {
 693        if(Element.hasClassName(textarea,"rich-editor")) {
 694            // rich HTML editor
 695            try {
 696                var editor = new YAHOO.widget.Editor(textarea, {
 697                    dompath: true,
 698                    animate: true,
 699                    handleSubmit: true
 700                });
 701                // probably due to the timing issue, we need to let the editor know
 702                // that DOM is ready
 703                editor.DOMReady=true;
 704                editor.fireQueue();
 705                editor.render();
 706            } catch(e) {
 707                alert(e);
 708            }
 709            return;
 710        }
 711
 712        var handle = textarea.nextSibling;
 713        if(handle==null || !Element.hasClassName(handle, "textarea-handle")) return;
 714
 715        var Event = YAHOO.util.Event;
 716
 717        handle.onmousedown = function(ev) {
 718            ev = Event.getEvent(ev);
 719            var offset = textarea.offsetHeight-Event.getPageY(ev);
 720            textarea.style.opacity = 0.5;
 721            document.onmousemove = function(ev) {
 722                ev = Event.getEvent(ev);
 723                function max(a,b) { if(a<b) return b; else return a; }
 724                textarea.style.height = max(32, offset + Event.getPageY(ev)) + 'px';
 725                return false;
 726            };
 727            document.onmouseup = function() {
 728                document.onmousemove = null;
 729                document.onmouseup = null;
 730                textarea.style.opacity = 1;
 731            }
 732        };
 733        handle.ondblclick = function() {
 734            textarea.style.height = "";
 735            textarea.rows = textarea.value.split("\n").length;
 736        }
 737    },
 738
 739    // structured form submission
 740    "FORM" : function(form) {
 741        crumb.appendToForm(form);
 742        if(Element.hasClassName(form, "no-json"))
 743            return;
 744        // add the hidden 'json' input field, which receives the form structure in JSON
 745        var div = document.createElement("div");
 746        div.innerHTML = "<input type=hidden name=json value=init>";
 747        form.appendChild(div);
 748
 749        var oldOnsubmit = form.onsubmit;
 750        if (typeof oldOnsubmit == "function") {
 751            form.onsubmit = function() { return buildFormTree(this) && oldOnsubmit.call(this); }
 752        } else {
 753            form.onsubmit = function() { return buildFormTree(this); };
 754        }
 755
 756        form = null; // memory leak prevention
 757    },
 758
 759    // hook up tooltip.
 760    //   add nodismiss="" if you'd like to display the tooltip forever as long as the mouse is on the element.
 761    "[tooltip]" : function(e) {
 762        applyTooltip(e,e.getAttribute("tooltip"));
 763    },
 764
 765    "INPUT.submit-button" : function(e) {
 766        makeButton(e);
 767    },
 768
 769    "INPUT.yui-button" : function(e) {
 770        makeButton(e);
 771    },
 772
 773    "TR.optional-block-start": function(e) { // see optionalBlock.jelly
 774        // set start.ref to checkbox in preparation of row-set-end processing
 775        var checkbox = e.firstChild.getElementsByTagName('input')[0];
 776        e.setAttribute("ref", checkbox.id = "cb"+(iota++));
 777    },
 778
 779    "TR.row-set-end": function(e) { // see rowSet.jelly and optionalBlock.jelly
 780        // figure out the corresponding start block
 781        var end = e;
 782
 783        for( var depth=0; ; e=e.previousSibling) {
 784            if(Element.hasClassName(e,"row-set-end"))        depth++;
 785            if(Element.hasClassName(e,"row-set-start"))      depth--;
 786            if(depth==0)    break;
 787        }
 788        var start = e;
 789
 790        var ref = start.getAttribute("ref");
 791        if(ref==null)
 792            start.id = ref = "rowSetStart"+(iota++);
 793
 794        applyNameRef(start,end,ref);
 795    },
 796
 797    "TR.optional-block-start ": function(e) { // see optionalBlock.jelly
 798        // this is suffixed by a pointless string so that two processing for optional-block-start
 799        // can sandwitch row-set-end
 800        // this requires "TR.row-set-end" to mark rows
 801        var checkbox = e.firstChild.getElementsByTagName('input')[0];
 802        updateOptionalBlock(checkbox,false);
 803    },
 804
 805    // image that shows [+] or [-], with hover effect.
 806    // oncollapsed and onexpanded will be called when the button is triggered.
 807    "IMG.fold-control" : function(e) {
 808        function changeTo(e,img) {
 809            var src = e.src;
 810            e.src = src.substring(0,src.lastIndexOf('/'))+"/"+e.getAttribute("state")+img;
 811        }
 812        e.onmouseover = function() {
 813            changeTo(this,"-hover.png");
 814        };
 815        e.onmouseout = function() {
 816            changeTo(this,".png");
 817        };
 818        e.parentNode.onclick = function(event) {
 819            var e = this.firstChild;
 820            var s = e.getAttribute("state");
 821            if(s=="plus") {
 822                e.setAttribute("state","minus");
 823                if(e.onexpanded)    e.onexpanded();
 824            } else {
 825                e.setAttribute("state","plus");
 826                if(e.oncollapsed)    e.oncollapsed();
 827            }
 828            changeTo(e,"-hover.png");
 829            YAHOO.util.Event.stopEvent(event);
 830            return false;
 831        };
 832        e = null; // memory leak prevention
 833    },
 834
 835    // radio buttons in repeatable content
 836    "DIV.repeated-chunk" : function(d) {
 837        var inputs = d.getElementsByTagName('INPUT');
 838        for (var i = 0; i < inputs.length; i++) {
 839            if (inputs[i].type == 'radio') {
 840                // Need to uniquify each set of radio buttons in repeatable content.
 841                // buildFormTree will remove the prefix before form submission.
 842                var prefix = d.getAttribute('radioPrefix');
 843                if (!prefix) {
 844                    prefix = 'removeme' + (iota++) + '_';
 845                    d.setAttribute('radioPrefix', prefix);
 846                }
 847                inputs[i].name = prefix + inputs[i].name;
 848                // Reselect anything unselected by browser before names uniquified:
 849                if (inputs[i].defaultChecked) inputs[i].checked = true;
 850            }
 851        }
 852    },
 853
 854    // radioBlock.jelly
 855    "INPUT.radio-block-control" : function(r) {
 856        r.id = "radio-block-"+(iota++);
 857
 858        // when one radio button is clicked, we need to update foldable block for
 859        // other radio buttons with the same name. To do this, group all the
 860        // radio buttons with the same name together and hang it under the form object
 861        var f = r.form;
 862        var radios = f.radios;
 863        if (radios == null)
 864            f.radios = radios = {};
 865
 866        var g = radios[r.name];
 867        if (g == null) {
 868            radios[r.name] = g = object(radioBlockSupport);
 869            g.buttons = [];
 870        }
 871
 872        var s = findAncestorClass(r,"radio-block-start");
 873
 874        // find the end node
 875        var e = (function() {
 876            var e = s;
 877            var cnt=1;
 878            while(cnt>0) {
 879                e = e.nextSibling;
 880                if (Element.hasClassName(e,"radio-block-start"))
 881                    cnt++;
 882                if (Element.hasClassName(e,"radio-block-end"))
 883                    cnt--;
 884            }
 885            return e;
 886        })();
 887
 888        var u = function() {
 889            g.updateSingleButton(r,s,e);
 890        };
 891        applyNameRef(s,e,r.id);
 892        g.buttons.push(u);
 893
 894        // apply the initial visibility
 895        u();
 896
 897        // install event handlers to update visibility.
 898        // needs to use onclick and onchange for Safari compatibility
 899        r.onclick = r.onchange = function() { g.updateButtons(); };
 900    },
 901
 902    // editableComboBox.jelly
 903    "INPUT.combobox" : function(c) {
 904        // Next element after <input class="combobox"/> should be <div class="combobox-values">
 905        var vdiv = c.nextSibling;
 906        if (Element.hasClassName(vdiv, "combobox-values")) {
 907            createComboBox(c, function() {
 908                var values = [];
 909                for (var value = vdiv.firstChild; value; value = value.nextSibling)
 910                    values.push(value.getAttribute('value'));
 911                return values;
 912            });
 913        }
 914    },
 915
 916    // dropdownList.jelly
 917    "SELECT.dropdownList" : function(e) {
 918        if(isInsideRemovable(e))    return;
 919
 920        e.subForms = [];
 921        var start = findFollowingTR(e, 'dropdownList-container').firstChild.nextSibling, end;
 922        do { start = start.firstChild; } while (start && start.tagName != 'TR');
 923        if (start && !Element.hasClassName(start,'dropdownList-start'))
 924            start = findFollowingTR(start, 'dropdownList-start');
 925        while (start != null) {
 926            end = findFollowingTR(start, 'dropdownList-end');
 927            e.subForms.push({ 'start': start, 'end': end });
 928            start = findFollowingTR(end, 'dropdownList-start');
 929        }
 930
 931        updateDropDownList(e);
 932    },
 933
 934    // select.jelly
 935    "SELECT.select" : function(e) {
 936        // controls that this SELECT box depends on
 937        refillOnChange(e,function(params) {
 938            var value = e.value;
 939            updateListBox(e,e.getAttribute("fillUrl"),{
 940                parameters: params,
 941                onSuccess: function() {
 942                    if (value=="") {
 943                        // reflect the initial value. if the control depends on several other SELECT.select,
 944                        // it may take several updates before we get the right items, which is why all these precautions.
 945                        var v = e.getAttribute("value");
 946                        if (v) {
 947                            e.value = v;
 948                            if (e.value==v) e.removeAttribute("value"); // we were able to apply our initial value
 949                        }
 950                    }
 951
 952                    // if the update changed the current selection, others listening to this control needs to be notified.
 953                    if (e.value!=value) fireEvent(e,"change");
 954                }
 955            });
 956        });
 957    },
 958
 959    // combobox.jelly
 960    "INPUT.combobox2" : function(e) {
 961        var items = [];
 962
 963        var c = new ComboBox(e,function(value) {
 964            var candidates = [];
 965            for (var i=0; i<items.length; i++) {
 966                if (items[i].indexOf(value)==0) {
 967                    candidates.push(items[i]);
 968                    if (candidates.length>20)   break;
 969                }
 970            } 
 971            return candidates;
 972        }, {});
 973
 974        refillOnChange(e,function(params) {
 975            new Ajax.Request(e.getAttribute("fillUrl"),{
 976                parameters: params,
 977                onSuccess : function(rsp) {
 978                    items = eval('('+rsp.responseText+')');
 979                }
 980            });
 981        });
 982    },
 983
 984    "A.showDetails" : function(e) {
 985        e.onclick = function() {
 986            this.style.display = 'none';
 987            this.nextSibling.style.display = 'block';
 988            return false;
 989        };
 990        e = null; // avoid memory leak
 991    },
 992
 993    "DIV.behavior-loading" : function(e) {
 994        e.style.display = 'none';
 995    },
 996
 997    ".button-with-dropdown" : function (e) {
 998        new YAHOO.widget.Button(e, { type: "menu", menu: e.nextSibling });
 999    }
1000};
1001
1002function applyTooltip(e,text) {
1003        // copied from YAHOO.widget.Tooltip.prototype.configContext to efficiently add a new element
1004        // event registration via YAHOO.util.Event.addListener leaks memory, so do it by ourselves here
1005        e.onmouseover = function(ev) {
1006            var delay = this.getAttribute("nodismiss")!=null ? 99999999 : 5000;
1007            tooltip.cfg.setProperty("autodismissdelay",delay);
1008            return tooltip.onContextMouseOver.call(this,YAHOO.util.Event.getEvent(ev),tooltip);
1009        }
1010        e.onmousemove = function(ev) { return tooltip.onContextMouseMove.call(this,YAHOO.util.Event.getEvent(ev),tooltip); }
1011        e.onmouseout  = function(ev) { return tooltip.onContextMouseOut .call(this,YAHOO.util.Event.getEvent(ev),tooltip); }
1012        e.title = text;
1013        e = null; // avoid memory leak
1014}
1015
1016var Path = {
1017  tail : function(p) {
1018      var idx = p.lastIndexOf("/");
1019      if (idx<0)    return p;
1020      return p.substring(idx+1);
1021  }
1022};
1023
1024/**
1025 * Install change handlers based on the 'fillDependsOn' attribute.
1026 */
1027function refillOnChange(e,onChange) {
1028    var deps = [];
1029
1030    function h() {
1031        var params = {};
1032        deps.each(function (d) {
1033            params[d.name] = controlValue(d.control);
1034        });
1035        onChange(params);
1036    }
1037    var v = e.getAttribute("fillDependsOn");
1038    if (v!=null) {
1039        v.split(" ").each(function (name) {
1040            var c = findNearBy(e,name);
1041            if (c==null) {
1042                if (window.console!=null)  console.warn("Unable to find nearby "+name);
1043                if (window.YUI!=null)      YUI.log("Unable to find a nearby control of the name "+name,"warn")
1044                return;
1045            }
1046            try { c.addEventListener("change",h,false); } catch (ex) { c.attachEvent("change",h); }
1047            deps.push({name:Path.tail(name),control:c});
1048        });
1049    }
1050    h();   // initial fill
1051}
1052
1053Behaviour.register(hudsonRules);
1054
1055
1056
1057function xor(a,b) {
1058    // convert both values to boolean by '!' and then do a!=b
1059    return !a != !b;
1060}
1061
1062// used by editableDescription.jelly to replace the description field with a form
1063function replaceDescription() {
1064    var d = document.getElementById("description");
1065    d.firstChild.nextSibling.innerHTML = "<div class='spinner-right'>loading...</div>";
1066    new Ajax.Request(
1067        "./descriptionForm",
1068        {
1069          onComplete : function(x) {
1070            d.innerHTML = x.responseText;
1071            Behaviour.applySubtree(d);
1072            d.getElementsByTagName("TEXTAREA")[0].focus();
1073          }
1074        }
1075    );
1076    return false;
1077}
1078
1079function applyNameRef(s,e,id) {
1080    $(id).groupingNode = true;
1081    // s contains the node itself
1082    var modified = Element.hasClassName(s, "modified") || Element.hasClassName(s.firstChild, "modified");
1083    for(var x=s.nextSibling; x!=e; x=x.nextSibling) {
1084        // to handle nested <f:rowSet> correctly, don't overwrite the existing value
1085        if(x.getAttribute("nameRef")==null)
1086            x.setAttribute("nameRef",id);
1087        if (modified) {
1088            addModifiedClass(x);
1089        }
1090    }
1091}
1092
1093// Remove 'original' class from elements and its children and
1094// add 'modified' class to it.
1095function addModifiedClass(element) {
1096    Element.removeClassName(element, "original");
1097    Element.addClassName(element, "modified");
1098    var elements = Element.childElements(element)
1099    for (var key = 0; key < elements.size(); key++) {
1100        Element.removeClassName(elements[key], "original");
1101        Element.addClassName(elements[key], "modified");
1102    }
1103}
1104
1105// used by optionalBlock.jelly to update the form status
1106//   @param c     checkbox element
1107function updateOptionalBlock(c,scroll) {
1108    // find the start TR
1109    var s = c;
1110    while(!Element.hasClassName(s, "optional-block-start"))
1111        s = s.parentNode;
1112    var tbl = s.parentNode;
1113    var i = false;
1114    var o = false;
1115
1116    var checked = xor(c.checked,Element.hasClassName(c,"negative"));
1117    var lastRow = null;
1118
1119    for (var j = 0; tbl.rows[j]; j++) {
1120        var n = tbl.rows[j];
1121
1122        if (i && Element.hasClassName(n, "optional-block-end"))
1123            o = true;
1124
1125        if (i && !o) {
1126            if (checked) {
1127                n.style.display = "";
1128                lastRow = n;
1129            } else
1130                n.style.display = "none";
1131        }
1132
1133        if (n==s) {
1134            if (n.getAttribute('hasHelp') == 'true')
1135                j++;
1136            i = true;
1137        }
1138    }
1139
1140    if(checked && scroll) {
1141        var D = YAHOO.util.Dom;
1142
1143        var r = D.getRegion(s);
1144        if(lastRow!=null)   r = r.union(D.getRegion(lastRow));
1145        scrollIntoView(r);
1146    }
1147
1148    if (c.name == 'hudson-tools-InstallSourceProperty') {
1149        // Hack to hide tool home when "Install automatically" is checked.
1150        var homeField = findPreviousFormItem(c, 'home');
1151        if (homeField != null && homeField.value == '') {
1152            var tr = findAncestor(homeField, 'TR');
1153            if (tr != null) {
1154                tr.style.display = c.checked ? 'none' : '';
1155            }
1156        }
1157    }
1158}
1159
1160
1161//
1162// Auto-scroll support for progressive log output.
1163//   See http://radio.javaranch.com/pascarello/2006/08/17/1155837038219.html
1164//
1165function AutoScroller(scrollContainer) {
1166    // get the height of the viewport.
1167    // See http://www.howtocreate.co.uk/tutorials/javascript/browserwindow
1168    function getViewportHeight() {
1169        if (typeof( window.innerWidth ) == 'number') {
1170            //Non-IE
1171            return window.innerHeight;
1172        } else if (document.documentElement && ( document.documentElement.clientWidth || document.documentElement.clientHeight )) {
1173            //IE 6+ in 'standards compliant mode'
1174            return document.documentElement.clientHeight;
1175        } else if (document.body && ( document.body.clientWidth || document.body.clientHeight )) {
1176            //IE 4 compatible
1177            return document.body.clientHeight;
1178        }
1179        return null;
1180    }
1181
1182    return {
1183        bottomThreshold : 25,
1184        scrollContainer: scrollContainer,
1185
1186        getCurrentHeight : function() {
1187            var scrollDiv = $(this.scrollContainer);
1188
1189            if (scrollDiv.scrollHeight > 0)
1190                return scrollDiv.scrollHeight;
1191            else
1192                if (objDiv.offsetHeight > 0)
1193                    return scrollDiv.offsetHeight;
1194
1195            return null; // huh?
1196        },
1197
1198        // return true if we are in the "stick to bottom" mode
1199        isSticking : function() {
1200            var scrollDiv = $(this.scrollContainer);
1201            var currentHeight = this.getCurrentHeight();
1202
1203            // when used with the BODY tag, the height needs to be the viewport height, instead of
1204            // the element height.
1205            //var height = ((scrollDiv.style.pixelHeight) ? scrollDiv.style.pixelHeight : scrollDiv.offsetHeight);
1206            var height = getViewportHeight();
1207            var diff = currentHeight - scrollDiv.scrollTop - height;
1208            // window.alert("currentHeight=" + currentHeight + ",scrollTop=" + scrollDiv.scrollTop + ",height=" + height);
1209
1210            return diff < this.bottomThreshold;
1211        },
1212
1213        scrollToBottom : function() {
1214            var scrollDiv = $(this.scrollContainer);
1215            scrollDiv.scrollTop = this.getCurrentHeight();
1216        }
1217    };
1218}
1219
1220// scroll the current window to display the given element or the region.
1221function scrollIntoView(e) {
1222    function calcDelta(ex1,ex2,vx1,vw) {
1223        var vx2=vx1+vw;
1224        var a;
1225        a = Math.min(vx1-ex1,vx2-ex2);
1226        if(a>0)     return -a;
1227        a = Math.min(ex1-vx1,ex2-vx2);
1228        if(a>0)     return a;
1229        return 0;
1230    }
1231
1232    var D = YAHOO.util.Dom;
1233
1234    var r;
1235    if(e.tagName!=null) r = D.getRegion(e);
1236    else                r = e;
1237
1238    var dx = calcDelta(r.left,r.right, document.body.scrollLeft, D.getViewportWidth());
1239    var dy = calcDelta(r.top, r.bottom,document.body.scrollTop,  D.getViewportHeight());
1240    window.scrollBy(dx,dy);
1241}
1242
1243// used in expandableTextbox.jelly to change a input field into a text area
1244function expandTextArea(button,id) {
1245    button.style.display="none";
1246    var field = button.parentNode.parentNode.getElementsByTagName("input")[0];
1247    var value = field.value.replace(/ +/g,'\n');
1248    var n = field;
1249    while(n.tagName!="TABLE")
1250        n = n.parentNode;
1251    n.parentNode.innerHTML =
1252        "<textarea rows=8 class='setting-input' name='"+field.name+"'>"+value+"</textarea>";
1253}
1254
1255
1256// refresh a part of the HTML specified by the given ID,
1257// by using the contents fetched from the given URL.
1258function refreshPart(id,url) {
1259    var f = function() {
1260        new Ajax.Request(url, {
1261            onSuccess: function(rsp) {
1262                var hist = $(id);
1263                var p = hist.parentNode;
1264                var next = hist.nextSibling;
1265                p.removeChild(hist);
1266
1267                var div = document.createElement('div');
1268                div.innerHTML = rsp.responseText;
1269
1270                var node = div.firstChild;
1271                p.insertBefore(node, next);
1272
1273                Behaviour.applySubtree(node);
1274
1275                if(isRunAsTest) return;
1276                refreshPart(id,url);
1277            }
1278        });
1279    };
1280    // if run as test, just do it once and do it now to make sure it's working,
1281    // but don't repeat.
1282    if(isRunAsTest) f();
1283    else    window.setTimeout(f, 5000);
1284}
1285
1286
1287/*
1288    Perform URL encode.
1289    Taken from http://www.cresc.co.jp/tech/java/URLencoding/JavaScript_URLEncoding.htm
1290    @deprecated Use standard javascript method "encodeURIComponent" instead
1291*/
1292function encode(str){
1293    var s, u;
1294    var s0 = "";                // encoded str
1295
1296    for (var i = 0; i < str.length; i++){   // scan the source
1297        s = str.charAt(i);
1298        u = str.charCodeAt(i);          // get unicode of the char
1299
1300        if (s == " "){s0 += "+";}       // SP should be converted to "+"
1301        else {
1302            if ( u == 0x2a || u == 0x2d || u == 0x2e || u == 0x5f || ((u >= 0x30) && (u <= 0x39)) || ((u >= 0x41) && (u <= 0x5a)) || ((u >= 0x61) && (u <= 0x7a))){     // check for escape
1303                s0 = s0 + s;           // don't escape
1304            } else {                      // escape
1305                if ((u >= 0x0) && (u <= 0x7f)){     // single byte format
1306                    s = "0"+u.toString(16);
1307                    s0 += "%"+ s.substr(s.length-2);
1308                } else
1309                if (u > 0x1fffff){     // quaternary byte format (extended)
1310                    s0 += "%" + (0xF0 + ((u & 0x1c0000) >> 18)).toString(16);
1311                    s0 += "%" + (0x80 + ((u & 0x3f000) >> 12)).toString(16);
1312                    s0 += "%" + (0x80 + ((u & 0xfc0) >> 6)).toString(16);
1313                    s0 += "%" + (0x80 + (u & 0x3f)).toString(16);
1314                } else
1315                if (u > 0x7ff){        // triple byte format
1316                    s0 += "%" + (0xe0 + ((u & 0xf000) >> 12)).toString(16);
1317                    s0 += "%" + (0x80 + ((u & 0xfc0) >> 6)).toString(16);
1318                    s0 += "%" + (0x80 + (u & 0x3f)).toString(16);
1319                } else {                      // double byte format
1320                    s0 += "%" + (0xc0 + ((u & 0x7c0) >> 6)).toString(16);
1321                    s0 += "%" + (0x80 + (u & 0x3f)).toString(16);
1322                }
1323            }
1324        }
1325    }
1326    return s0;
1327}
1328
1329// when there are multiple form elements of the same name,
1330// this method returns the input field of the given name that pairs up
1331// with the specified 'base' input element.
1332Form.findMatchingInput = function(base, name) {
1333    // find the FORM element that owns us
1334    var f = base;
1335    while (f.tagName != "FORM")
1336        f = f.parentNode;
1337
1338    var bases = Form.getInputs(f, null, base.name);
1339    var targets = Form.getInputs(f, null, name);
1340
1341    for (var i=0; i<bases.length; i++) {
1342        if (bases[i] == base)
1343            return targets[i];
1344    }
1345
1346    return null;        // not found
1347}
1348
1349// used witih <dropdownList> and <dropdownListBlock> to control visibility
1350function updateDropDownList(sel) {
1351    for (var i = 0; i < sel.subForms.length; i++) {
1352        var show = sel.selectedIndex == i;
1353        var f = sel.subForms[i];
1354        var tr = f.start;
1355        while (true) {
1356            tr.style.display = (show ? "" : "none");
1357            if(show)
1358                tr.removeAttribute("field-disabled");
1359            else    // buildFormData uses this attribute and ignores the contents
1360                tr.setAttribute("field-disabled","true");
1361            if (tr == f.end) break;
1362            tr = tr.nextSibling;
1363        }
1364    }
1365}
1366
1367
1368// code for supporting repeatable.jelly
1369var repeatableSupport = {
1370    // set by the inherited instance to the insertion point DIV
1371    insertionPoint: null,
1372
1373    // HTML text of the repeated chunk
1374    blockHTML: null,
1375
1376    // containing <div>.
1377    container: null,
1378
1379    // block name for structured HTML
1380    name : null,
1381
1382    withDragDrop: false,
1383
1384    // do the initialization
1385    init : function(container,master,insertionPoint) {
1386        this.container = $(container);
1387        this.container.tag = this;
1388        master = $(master);
1389        this.blockHTML = master.innerHTML;
1390        master.parentNode.removeChild(master);
1391        this.insertionPoint = $(insertionPoint);
1392        this.name = master.getAttribute("name");
1393        this.update();
1394        this.withDragDrop = initContainerDD(container);
1395    },
1396
1397    // insert one more block at the insertion position
1398    expand : function() {
1399        // importNode isn't supported in IE.
1400        // nc = document.importNode(node,true);
1401        var nc = document.createElement("div");
1402        nc.className = "repeated-chunk";
1403        nc.setAttribute("name",this.name);
1404        nc.innerHTML = this.blockHTML;
1405        this.insertionPoint.parentNode.insertBefore(nc, this.insertionPoint);
1406        if (this.withDragDrop) prepareDD(nc);
1407
1408        hudsonRules['DIV.repeated-chunk'](nc);  // applySubtree doesn't get nc itself
1409        Behaviour.applySubtree(nc);
1410        this.update();
1411    },
1412
1413    // update CSS classes associated with repeated items.
1414    update : function() {
1415        var children = [];
1416        for( var n=this.container.firstChild; n!=null; n=n.nextSibling )
1417            if(Element.hasClassName(n,"repeated-chunk"))
1418                children.push(n);
1419
1420        if(children.length==0) {
1421            // noop
1422        } else
1423        if(children.length==1) {
1424            children[0].addClassName("repeated-chunk first last only");
1425        } else {
1426            Element.removeClassName(children[0], "last only");
1427            children[0].addClassName("repeated-chunk first");
1428            for(var i=1; i<children.length-1; i++)
1429                children[i].addClassName("repeated-chunk middle");
1430            children[children.length-1].addClassName("repeated-chunk last");
1431        }
1432    },
1433
1434    // these are static methods that don't rely on 'this'
1435
1436    // called when 'delete' button is clicked
1437    onDelete : function(n) {
1438        while (!Element.hasClassName(n,"repeated-chunk"))
1439            n = n.parentNode;
1440
1441        var p = n.parentNode;
1442        p.removeChild(n);
1443        p.tag.update();
1444    },
1445
1446    // called when 'add' button is clicked
1447    onAdd : function(n) {
1448        while(n.tag==null)
1449            n = n.parentNode;
1450        n.tag.expand();
1451        // Hack to hide tool home when a new tool has some installers.
1452        var inputs = n.getElementsByTagName('INPUT');
1453        for (var i = 0; i < inputs.length; i++) {
1454            var input = inputs[i];
1455            if (input.name == 'hudson-tools-InstallSourceProperty') {
1456                updateOptionalBlock(input, false);
1457            }
1458        }
1459    }
1460};
1461
1462// prototype object to be duplicated for each radio button group
1463var radioBlockSupport = {
1464    buttons : null,
1465
1466    updateButtons : function() {
1467        for( var i=0; i<this.buttons.length; i++ )
1468            this.buttons[i]();
1469    },
1470
1471    // update…

Large files files are truncated, but you can click here to view the full file