hudson-behavior.js 90.6 KB
Newer Older
K
kohsuke 已提交
1 2 3
/*
 * The MIT License
 * 
4
 * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi,
5
 * Daniel Dyer, Yahoo! Inc., Alan Harder, InfraDNA, Inc.
K
kohsuke 已提交
6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
K
kohsuke 已提交
25
//
K
kohsuke 已提交
26
//
K
Jenkins  
Kohsuke Kawaguchi 已提交
27
// JavaScript for Jenkins
K
kohsuke 已提交
28 29 30 31
//     See http://www.ibm.com/developerworks/web/library/wa-memleak/?ca=dgr-lnxw97JavascriptLeaks
//     for memory leak patterns and how to prevent them.
//

32 33 34 35 36 37 38
// create a new object whose prototype is the given object
function object(o) {
    function F() {}
    F.prototype = o;
    return new F();
}

K
kohsuke 已提交
39 40 41
// id generator
var iota = 0;

42 43 44 45 46 47
// crumb information
var crumb = {
    fieldName: null,
    value: null,

    init: function(crumbField, crumbValue) {
48
        if (crumbField=="") return; // layout.jelly passes in "" whereas it means null.
49 50
        this.fieldName = crumbField;
        this.value = crumbValue;
51 52 53
    },

    /**
54
     * Adds the crumb value into the given hash or array and returns it.
55
     */
56 57 58 59 60 61 62 63
    wrap: function(headers) {
        if (this.fieldName!=null) {
            if (headers instanceof Array)
                headers.push(this.fieldName, this.value);
            else
                headers[this.fieldName]=this.value;
        }
        return headers;
64 65 66 67 68 69 70 71 72 73
    },

    /**
     * Puts a hidden input field to the form so that the form submission will have the crumb value
     */
    appendToForm : function(form) {
        if(this.fieldName==null)    return; // noop
        var div = document.createElement("div");
        div.innerHTML = "<input type=hidden name='"+this.fieldName+"' value='"+this.value+"'>";
        form.appendChild(div);
74 75 76
    }
}

K
kohsuke 已提交
77 78
// Form check code
//========================================================
K
kohsuke 已提交
79
var FormChecker = {
K
kohsuke 已提交
80 81
    // pending requests
    queue : [],
K
kohsuke 已提交
82

83 84 85 86 87 88
    // conceptually boolean, but doing so create concurrency problem.
    // that is, during unit tests, the AJAX.send works synchronously, so
    // the onComplete happens before the send method returns. On a real environment,
    // more likely it's the other way around. So setting a boolean flag to true or false
    // won't work.
    inProgress : 0,
K
kohsuke 已提交
89

90 91 92 93 94 95 96 97 98 99
    /**
     * Schedules a form field check. Executions are serialized to reduce the bandwidth impact.
     *
     * @param url
     *      Remote doXYZ URL that performs the check. Query string should include the field value.
     * @param method
     *      HTTP method. GET or POST. I haven't confirmed specifics, but some browsers seem to cache GET requests.
     * @param target
     *      HTML element whose innerHTML will be overwritten when the check is completed.
     */
K
kohsuke 已提交
100
    delayedCheck : function(url, method, target) {
101 102
        if(url==null || method==null || target==null)
            return; // don't know whether we should throw an exception or ignore this. some broken plugins have illegal parameters
K
kohsuke 已提交
103 104 105
        this.queue.push({url:url, method:method, target:target});
        this.schedule();
    },
K
kohsuke 已提交
106

K
kohsuke 已提交
107 108 109 110 111
    sendRequest : function(url, params) {
        if (params.method == "post") {
            var idx = url.indexOf('?');
            params.parameters = url.substring(idx + 1);
            url = url.substring(0, idx);
K
kohsuke 已提交
112
        }
K
kohsuke 已提交
113 114 115 116
        new Ajax.Request(url, params);
    },

    schedule : function() {
117
        if (this.inProgress>0)  return;
K
kohsuke 已提交
118 119 120 121 122 123
        if (this.queue.length == 0) return;

        var next = this.queue.shift();
        this.sendRequest(next.url, {
            method : next.method,
            onComplete : function(x) {
124 125 126 127 128
                var i;
                next.target.innerHTML = x.status==200 ? x.responseText
                    : '<a href="" onclick="document.getElementById(\'valerr' + (i=iota++)
                    + '\').style.display=\'block\';return false">ERROR</a><div id="valerr'
                    + i + '" style="display:none">' + x.responseText + '</div>';
129
                Behaviour.applySubtree(next.target);
130
                FormChecker.inProgress--;
K
kohsuke 已提交
131
                FormChecker.schedule();
132
                layoutUpdateCallback.call();
K
kohsuke 已提交
133 134
            }
        });
135
        this.inProgress++;
K
kohsuke 已提交
136
    }
K
kohsuke 已提交
137 138
}

139 140 141
/**
 * Find the sibling (in the sense of the structured form submission) form item of the given name,
 * and returns that DOM node.
142 143 144 145 146
 *
 * @param {HTMLElement} e
 * @param {string} name
 *      Name of the control to find. Can include "../../" etc in the prefix.
 *      See @RelativePath.
147 148
 */
function findNearBy(e,name) {
149 150 151 152 153
    while (name.startsWith("../")) {
        name = name.substring(3);
        e = findFormParent(e,null,true);
    }

154 155
    // does 'e' itself match the criteria?
    // as some plugins use the field name as a parameter value, instead of 'value'
156 157 158 159
    var p = findFormItem(e,name,function(e,filter) {
        if (filter(e))    return e;
        return null;
    });
160
    if (p!=null)    return p;
161

162
    var owner = findFormParent(e,null,true);
163

164
    p = findPreviousFormItem(e,name);
165
    if (p!=null && findFormParent(p,null,true)==owner)
166 167 168
        return p;

    var n = findNextFormItem(e,name);
169
    if (n!=null && findFormParent(n,null,true)==owner)
170 171 172 173 174
        return n;

    return null; // not found
}

175
function controlValue(e) {
K
kohsuke 已提交
176
    if (e==null)    return null;
K
kohsuke 已提交
177
    // compute the form validation value to be sent to the server
178 179
    var type = e.getAttribute("type");
    if(type!=null && type.toLowerCase()=="checkbox")
K
kohsuke 已提交
180
        return e.checked;
181 182 183 184 185
    return e.value;
}

function toValue(e) {
    return encodeURIComponent(controlValue(e));
K
kohsuke 已提交
186 187
}

K
kohsuke 已提交
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206
/**
 * Builds a query string in a fluent API pattern.
 * @param {HTMLElement} owner
 *      The 'this' control.
 */
function qs(owner) {
    return {
        params : "",

        append : function(s) {
            if (this.params.length==0)  this.params+='?';
            else                        this.params+='&';
            this.params += s;
            return this;
        },

        nearBy : function(name) {
            var e = findNearBy(owner,name);
            if (e==null)    return this;    // skip
207
            return this.append(Path.tail(name)+'='+toValue(e));
K
kohsuke 已提交
208 209 210 211 212 213 214 215 216 217 218 219
        },

        addThis : function() {
            return this.append("value="+toValue(owner));
        },

        toString : function() {
            return this.params;
        }
    };
}

K
kohsuke 已提交
220 221 222 223
// find the nearest ancestor node that has the given tag name
function findAncestor(e, tagName) {
    do {
        e = e.parentNode;
224
    } while (e != null && e.tagName != tagName);
K
kohsuke 已提交
225 226 227
    return e;
}

228 229 230 231 232 233 234
function findAncestorClass(e, cssClass) {
    do {
        e = e.parentNode;
    } while (e != null && !Element.hasClassName(e,cssClass));
    return e;
}

K
kohsuke 已提交
235 236 237 238 239
function findFollowingTR(input, className) {
    // identify the parent TR
    var tr = input;
    while (tr.tagName != "TR")
        tr = tr.parentNode;
K
kohsuke 已提交
240

K
kohsuke 已提交
241 242 243
    // then next TR that matches the CSS
    do {
        tr = tr.nextSibling;
244
    } while (tr != null && (tr.tagName != "TR" || !Element.hasClassName(tr,className)));
K
kohsuke 已提交
245

K
kohsuke 已提交
246
    return tr;
K
kohsuke 已提交
247 248
}

249 250 251
function find(src,filter,traversalF) {
    while(src!=null) {
        src = traversalF(src);
252
        if(src!=null && filter(src))
253 254 255 256 257
            return src;
    }
    return null;
}

K
kohsuke 已提交
258 259 260 261 262
/**
 * Traverses a form in the reverse document order starting from the given element (but excluding it),
 * until the given filter matches, or run out of an element.
 */
function findPrevious(src,filter) {
263
    return find(src,filter,function (e) {
K
kohsuke 已提交
264 265 266 267 268
        var p = e.previousSibling;
        if(p==null) return e.parentNode;
        while(p.lastChild!=null)
            p = p.lastChild;
        return p;
269 270
    });
}
K
kohsuke 已提交
271

272 273 274 275 276 277 278 279
function findNext(src,filter) {
    return find(src,filter,function (e) {
        var n = e.nextSibling;
        if(n==null) return e.parentNode;
        while(n.firstChild!=null)
            n = n.firstChild;
        return n;
    });
K
kohsuke 已提交
280
}
281 282 283 284 285 286

function findFormItem(src,name,directionF) {
    var name2 = "_."+name; // handles <textbox field="..." /> notation silently
    return directionF(src,function(e){ return (e.tagName=="INPUT" || e.tagName=="TEXTAREA" || e.tagName=="SELECT") && (e.name==name || e.name==name2); });
}

K
kohsuke 已提交
287 288 289 290
/**
 * Traverses a form in the reverse document order and finds an INPUT element that matches the given name.
 */
function findPreviousFormItem(src,name) {
291 292 293 294 295
    return findFormItem(src,name,findPrevious);
}

function findNextFormItem(src,name) {
    return findFormItem(src,name,findNext);
K
kohsuke 已提交
296 297
}

K
kohsuke 已提交
298 299 300 301 302 303 304 305 306
/**
 * Parse HTML into DOM.
 */
function parseHtml(html) {
    var c = document.createElement("div");
    c.innerHTML = html;
    return c.firstChild;
}

307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326
/**
 * Emulate the firing of an event.
 *
 * @param {HTMLElement} element
 *      The element that will fire the event
 * @param {String} event
 *      like 'change', 'blur', etc.
 */
function fireEvent(element,event){
    if (document.createEvent) {
        // dispatch for firefox + others
        var evt = document.createEvent("HTMLEvents");
        evt.initEvent(event, true, true ); // event type,bubbling,cancelable
        return !element.dispatchEvent(evt);
    } else {
        // dispatch for IE
        var evt = document.createEventObject();
        return element.fireEvent('on'+event,evt)
    }
}
K
kohsuke 已提交
327

328 329
// shared tooltip object
var tooltip;
K
kohsuke 已提交
330 331 332 333 334



// Behavior rules
//========================================================
K
kohsuke 已提交
335
// using tag names in CSS selector makes the processing faster
336
function registerValidator(e) {
K
kohsuke 已提交
337 338 339 340
    e.targetElement = findFollowingTR(e, "validation-error-area").firstChild.nextSibling;
    e.targetUrl = function() {
        return eval(this.getAttribute("checkUrl"));
    };
341
    var method = e.getAttribute("checkMethod");
K
kohsuke 已提交
342
    if (!method) method = "get";
343

344 345 346 347 348 349 350 351 352
    var url = e.targetUrl();
    try {
      FormChecker.delayedCheck(url, method, e.targetElement);
    } catch (x) {
        // this happens if the checkUrl refers to a non-existing element.
        // don't let this kill off the entire JavaScript
        YAHOO.log("Failed to register validation method: "+e.getAttribute("checkUrl")+" : "+e);
        return;
    }
353

354
    var checker = function() {
K
kohsuke 已提交
355 356 357 358 359
        var target = this.targetElement;
        FormChecker.sendRequest(this.targetUrl(), {
            method : method,
            onComplete : function(x) {
                target.innerHTML = x.responseText;
360
                Behaviour.applySubtree(target);
K
kohsuke 已提交
361 362
            }
        });
363
    }
364 365 366 367
    var oldOnchange = e.onchange;
    if(typeof oldOnchange=="function") {
        e.onchange = function() { checker.call(this); oldOnchange.call(this); }
    } else
368
        e.onchange = checker;
369 370
    e.onblur = checker;

K
kohsuke 已提交
371
    e = null; // avoid memory leak
K
kohsuke 已提交
372
}
373

374 375
function registerRegexpValidator(e,regexp,message) {
    e.targetElement = findFollowingTR(e, "validation-error-area").firstChild.nextSibling;
376 377 378
    var checkMessage = e.getAttribute('checkMessage');
    if (checkMessage) message = checkMessage;
    var oldOnchange = e.onchange;
379
    e.onchange = function() {
380
        var set = oldOnchange != null ? oldOnchange.call(this) : false;
381
        if (this.value.match(regexp)) {
382
            if (!set) this.targetElement.innerHTML = "";
383 384
        } else {
            this.targetElement.innerHTML = "<div class=error>" + message + "</div>";
385
            set = true;
386
        }
387
        return set;
388
    }
389
    e.onchange.call(e);
390 391 392
    e = null; // avoid memory leak
}

393 394 395 396 397 398 399 400 401
/**
 * Wraps a <button> into YUI button.
 *
 * @param e
 *      button element
 * @param onclick
 *      onclick handler
 */
function makeButton(e,onclick) {
K
kohsuke 已提交
402
    var h = e.onclick;
403
    var clsName = e.className;
404
    var n = e.name;
405
    var btn = new YAHOO.widget.Button(e,{});
K
kohsuke 已提交
406 407
    if(onclick!=null)
        btn.addListener("click",onclick);
K
kohsuke 已提交
408 409
    if(h!=null)
        btn.addListener("click",h);
410 411 412 413
    var be = btn.get("element");
    Element.addClassName(be,clsName);
    if(n!=null) // copy the name
        be.setAttribute("name",n);
K
kohsuke 已提交
414
    return btn;
415 416
}

K
kohsuke 已提交
417 418 419 420 421 422 423 424
/*
    If we are inside 'to-be-removed' class, some HTML altering behaviors interact badly, because
    the behavior re-executes when the removed master copy gets reinserted later.
 */
function isInsideRemovable(e) {
    return Element.ancestors(e).find(function(f){return f.hasClassName("to-be-removed");});
}

425 426 427 428 429 430 431 432 433
/**
 * Render the template captured by &lt;l:renderOnDemand> at the element 'e' and replace 'e' by the content.
 *
 * @param {HTMLElement} e
 *      The place holder element to be lazy-rendered.
 * @param {boolean} noBehaviour
 *      if specified, skip the application of behaviour rule.
 */
function renderOnDemand(e,callback,noBehaviour) {
434
    if (!e || !Element.hasClassName(e,"render-on-demand")) return;
435 436
    var proxy = eval(e.getAttribute("proxy"));
    proxy.render(function (t) {
437 438 439
        var contextTagName = e.parentNode.tagName;
        var c;
        if (contextTagName=="TBODY") {
440 441 442
            c = document.createElement("DIV");
            c.innerHTML = "<TABLE><TBODY>"+t.responseText+"</TBODY></TABLE>";
            c = c.firstChild.firstChild;
443 444 445 446
        } else {
            c = document.createElement(contextTagName);
            c.innerHTML = t.responseText;
        }
447

448
        var elements = [];
449 450 451
        while (c.firstChild!=null) {
            var n = c.firstChild;
            e.parentNode.insertBefore(n,e);
452
            if (n.nodeType==1 && !noBehaviour)
453
                elements.push(n);
454 455 456
        }
        Element.remove(e);

457 458 459 460 461 462
        evalInnerHtmlScripts(t.responseText,function() {
            Behaviour.applySubtree(elements,true);
            if (callback)   callback(t);
        });
    });
}
463

464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483
/**
 * Finds all the script tags 
 */
function evalInnerHtmlScripts(text,callback) {
    var q = [];
    var matchAll = new RegExp('<script([^>]*)>([\\S\\s]*?)<\/script>', 'img');
    var matchOne = new RegExp('<script([^>]*)>([\\S\\s]*?)<\/script>', 'im');
    var srcAttr  = new RegExp('src=[\'\"]([^\'\"]+)[\'\"]','i');
    (text.match(matchAll)||[]).map(function(s) {
        var m = s.match(srcAttr);
        if (m) {
            q.push(function(cont) {
                loadScript(m[1],cont);
            });
        } else {
            q.push(function(cont) {
                eval(s.match(matchOne)[2]);
                cont();
            });
        }
484
    });
485 486
    q.push(callback);
    sequencer(q);
487 488
}

489 490 491 492 493 494 495 496 497 498 499 500 501 502
/**
 * Take an array of (typically async) functions and run them in a sequence.
 * Each of the function in the array takes one 'continuation' parameter, and upon the completion
 * of the function it needs to invoke "continuation()" to signal the execution of the next function.
 */
function sequencer(fs) {
    var nullFunction = function() {}
    function next() {
        if (fs.length>0) {
            (fs.shift()||nullFunction)(next);
        }
    }
    return next();
}
503

K
kohsuke 已提交
504
var hudsonRules = {
K
kohsuke 已提交
505
    "BODY" : function() {
506
        tooltip = new YAHOO.widget.Tooltip("tt", {context:[], zindex:999});
K
kohsuke 已提交
507
    },
508

509 510 511 512
// do the ones that extract innerHTML so that they can get their original HTML before
// other behavior rules change them (like YUI buttons.)

    "DIV.hetero-list-container" : function(e) {
K
kohsuke 已提交
513 514
        if(isInsideRemovable(e))    return;

515 516
        // components for the add button
        var menu = document.createElement("SELECT");
517 518
        var btns = findElementsBySelector(e,"INPUT.hetero-list-add"),
            btn = btns[btns.length-1]; // In case nested content also uses hetero-list
519 520 521 522 523 524 525 526 527 528 529
        YAHOO.util.Dom.insertAfter(menu,btn);

        var prototypes = e.lastChild;
        while(!Element.hasClassName(prototypes,"prototypes"))
            prototypes = prototypes.previousSibling;
        var insertionPoint = prototypes.previousSibling;    // this is where the new item is inserted.

        // extract templates
        var templates = []; var i=0;
        for(var n=prototypes.firstChild;n!=null;n=n.nextSibling,i++) {
            var name = n.getAttribute("name");
530
            var tooltip = n.getAttribute("tooltip");
531
            var descriptorId = n.getAttribute("descriptorId");
532
            menu.options[i] = new Option(n.getAttribute("title"),""+i);
533
            templates.push({html:n.innerHTML, name:name, tooltip:tooltip,descriptorId:descriptorId});
534 535 536
        }
        Element.remove(prototypes);

537
        var withDragDrop = initContainerDD(e);
538

539 540 541 542 543 544 545 546
        var menuButton = new YAHOO.widget.Button(btn, { type: "menu", menu: menu });
        menuButton.getMenu().clickEvent.subscribe(function(type,args,value) {
            var t = templates[parseInt(args[1].value)]; // where this args[1] comes is a real mystery

            var nc = document.createElement("div");
            nc.className = "repeated-chunk";
            nc.setAttribute("name",t.name);
            nc.innerHTML = t.html;
547

548
            renderOnDemand(findElementsBySelector(nc,"TR.config-page")[0],function() {
549 550 551
                insertionPoint.parentNode.insertBefore(nc, insertionPoint);
                if(withDragDrop)    prepareDD(nc);

552
                Behaviour.applySubtree(nc,true);
553
            },true);
554
        });
555 556 557 558 559 560 561 562 563 564

        menuButton.getMenu().renderEvent.subscribe(function(type,args,value) {
            // hook up tooltip for menu items
            var items = menuButton.getMenu().getItems();
            for(i=0; i<items.length; i++) {
                var t = templates[i].tooltip;
                if(t!=null)
                    applyTooltip(items[i].element,t);
            }
        });
565 566 567
    },

    "DIV.repeated-container" : function(e) {
K
kohsuke 已提交
568 569
        if(isInsideRemovable(e))    return;

570 571 572 573 574 575 576 577 578
        // compute the insertion point
        var ip = e.lastChild;
        while (!Element.hasClassName(ip, "repeatable-insertion-point"))
            ip = ip.previousSibling;
        // set up the logic
        object(repeatableSupport).init(e, e.firstChild, ip);
    },


K
kohsuke 已提交
579 580 581 582
    "TABLE.sortable" : function(e) {// sortable table
        ts_makeSortable(e);
    },

K
kohsuke 已提交
583 584 585
    "TABLE.progress-bar" : function(e) {// sortable table
        e.onclick = function() {
            var href = this.getAttribute("href");
K
kohsuke 已提交
586
            if(href!=null)      window.location = href;
K
kohsuke 已提交
587 588 589 590
        }
        e = null; // avoid memory leak
    },

K
Kohsuke Kawaguchi 已提交
591
    "INPUT.applyButton":function (e) {
592 593 594 595 596 597 598 599 600 601 602 603 604 605 606
        var id = "iframe"+(iota++);

        var responseDialog = new YAHOO.widget.Panel("wait"+(iota++), {
            fixedcenter:true,
            close:true,
            draggable:true,
            zindex:4,
            modal:true,
            visible:false
        });

        responseDialog.setHeader("Error");
        responseDialog.setBody("<iframe id='"+id+"' name='"+id+"' style='height:100%; width:100%'></iframe>");
        responseDialog.render(document.body);
        var target = $(id); // iframe
K
Kohsuke Kawaguchi 已提交
607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622

        function attachIframeOnload(target, f) {
            if (target.attachEvent) {
                target.attachEvent("onload", f);
            } else {
                target.onload = f;
            }
        }

        var attached = false;
        makeButton(e,function (e) {
            var f = findAncestor(e.target, "FORM");

            if (!attached) {
                attached = true;
                attachIframeOnload(target, function () {
623 624 625 626 627 628 629 630 631 632 633
                    if (target.contentWindow && target.contentWindow.applyCompletionHandler) {
                        // apply-aware server is expected to set this handler
                        target.contentWindow.applyCompletionHandler(window);
                    } else {
                        // otherwise this is possibly an error from the server, so we need to render the whole content.
                        var r = YAHOO.util.Dom.getClientRegion();
                        responseDialog.cfg.setProperty("width",r.width*3/4+"px");
                        responseDialog.cfg.setProperty("height",r.height*3/4+"px");
                        responseDialog.center();
                        responseDialog.show();
                    }
K
Kohsuke Kawaguchi 已提交
634 635 636 637
                });
            }

            f.target = target.id;
638
            f.elements['core:apply'].value = "true";
K
Kohsuke Kawaguchi 已提交
639 640 641 642
            try {
                buildFormTree(f);
                f.submit();
            } finally {
643
                f.elements['core:apply'].value = null;
K
Kohsuke Kawaguchi 已提交
644 645 646 647 648
                f.target = null;
            }
        });
    },

K
kohsuke 已提交
649
    "INPUT.advancedButton" : function(e) {
650
        makeButton(e,function(e) {
K
kohsuke 已提交
651 652 653
            var link = e.target;
            while(!Element.hasClassName(link,"advancedLink"))
                link = link.parentNode;
K
kohsuke 已提交
654
            link.style.display = "none"; // hide the button
K
kohsuke 已提交
655

K
kohsuke 已提交
656
            var container = link.nextSibling.firstChild; // TABLE -> TBODY
K
kohsuke 已提交
657

K
kohsuke 已提交
658 659 660
            var tr = link;
            while (tr.tagName != "TR")
                tr = tr.parentNode;
K
kohsuke 已提交
661

K
kohsuke 已提交
662
            // move the contents of the advanced portion into the main table
663 664 665
            var nameRef = tr.getAttribute("nameref");
            while (container.lastChild != null) {
                var row = container.lastChild;
666 667
                if(nameRef!=null && row.getAttribute("nameref")==null)
                    row.setAttribute("nameref",nameRef); // to handle inner rowSets, don't override existing values
668 669
                tr.parentNode.insertBefore(row, tr.nextSibling);
            }
670
        });
K
kohsuke 已提交
671 672
        e = null; // avoid memory leak
    },
K
kohsuke 已提交
673

K
kohsuke 已提交
674 675 676 677 678 679 680 681 682 683 684
    "INPUT.expandButton" : function(e) {
        makeButton(e,function(e) {
            var link = e.target;
            while(!Element.hasClassName(link,"advancedLink"))
                link = link.parentNode;
            link.style.display = "none";
            link.nextSibling.style.display="block";
        });
        e = null; // avoid memory leak
    },

K
kohsuke 已提交
685 686 687 688 689 690 691
// scripting for having default value in the input field
    "INPUT.has-default-text" : function(e) {
        var defaultValue = e.value;
        Element.addClassName(e, "defaulted");
        e.onfocus = function() {
            if (this.value == defaultValue) {
                this.value = "";
K
kohsuke 已提交
692
                Element.removeClassName(this, "defaulted");
K
kohsuke 已提交
693 694 695 696 697
            }
        }
        e.onblur = function() {
            if (this.value == "") {
                this.value = defaultValue;
K
kohsuke 已提交
698
                Element.addClassName(this, "defaulted");
K
kohsuke 已提交
699 700 701 702
            }
        }
        e = null; // avoid memory leak
    },
K
kohsuke 已提交
703

704 705 706
// <label> that doesn't use ID, so that it can be copied in <repeatable>
    "LABEL.attach-previous" : function(e) {
        e.onclick = function() {
707 708 709 710 711 712 713 714
            var e = this.previousSibling;
            while (e!=null) {
                if (e.tagName=="INPUT") {
                    e.click();
                    break;
                }
                e = e.previousSibling;
            }
715 716 717 718
        }
        e = null;
    },

K
kohsuke 已提交
719 720 721 722 723 724
// form fields that are validated via AJAX call to the server
// elements with this class should have two attributes 'checkUrl' that evaluates to the server URL.
    "INPUT.validated" : registerValidator,
    "SELECT.validated" : registerValidator,
    "TEXTAREA.validated" : registerValidator,

725 726 727
// validate required form values
    "INPUT.required" : function(e) { registerRegexpValidator(e,/./,"Field is required"); },

728 729
// validate form values to be an integer
    "INPUT.number" : function(e) { registerRegexpValidator(e,/^(\d+|)$/,"Not an integer"); },
730
    "INPUT.positive-number" : function(e) {
731
        registerRegexpValidator(e,/^(\d*[1-9]\d*|)$/,"Not a positive integer");
K
kohsuke 已提交
732
    },
K
kohsuke 已提交
733

734 735 736 737 738 739
    "INPUT.auto-complete": function(e) {// form field with auto-completion support 
        // insert the auto-completion container
        var div = document.createElement("DIV");
        e.parentNode.insertBefore(div,e.nextSibling);
        e.style.position = "relative"; // or else by default it's absolutely positioned, making "width:100%" break

740 741 742 743 744 745
        var ds = new YAHOO.util.XHRDataSource(e.getAttribute("autoCompleteUrl"));
        ds.responseType = YAHOO.util.XHRDataSource.TYPE_JSON;
        ds.responseSchema = {
            resultsList: "suggestions",
            fields: ["name"]
        };
746 747 748
        
        // Instantiate the AutoComplete
        var ac = new YAHOO.widget.AutoComplete(e, div, ds);
749 750 751
        ac.generateRequest = function(query) {
            return "?value=" + query;
        };
752 753 754 755
        ac.prehighlightClassName = "yui-ac-prehighlight";
        ac.animSpeed = 0;
        ac.useShadow = true;
        ac.autoSnapContainer = true;
756
        ac.delimChar = e.getAttribute("autoCompleteDelimChar");
757 758 759 760 761 762 763 764
        ac.doBeforeExpandContainer = function(textbox,container) {// adjust the width every time we show it
            container.style.width=textbox.clientWidth+"px";
            var Dom = YAHOO.util.Dom;
            Dom.setXY(container, [Dom.getX(textbox), Dom.getY(textbox) + textbox.offsetHeight] );
            return true;
        }
    },

K
kohsuke 已提交
765 766 767 768
    "A.help-button" : function(e) {
        e.onclick = function() {
            var tr = findFollowingTR(this, "help-area");
            var div = tr.firstChild.nextSibling.firstChild;
K
kohsuke 已提交
769

K
kohsuke 已提交
770 771
            if (div.style.display != "block") {
                div.style.display = "block";
K
kohsuke 已提交
772
                // make it visible
K
kohsuke 已提交
773 774
                new Ajax.Request(this.getAttribute("helpURL"), {
                    method : 'get',
775
                    onSuccess : function(x) {
776 777
                        var from = x.getResponseHeader("X-Plugin-From");
                        div.innerHTML = x.responseText+(from?"<div class='from-plugin'>"+from+"</div>":"");
778
                        layoutUpdateCallback.call();
779 780 781
                    },
                    onFailure : function(x) {
                        div.innerHTML = "<b>ERROR</b>: Failed to load help file: " + x.statusText;
782
                        layoutUpdateCallback.call();
K
kohsuke 已提交
783 784
                    }
                });
K
kohsuke 已提交
785 786
            } else {
                div.style.display = "none";
787
                layoutUpdateCallback.call();
K
kohsuke 已提交
788 789
            }

K
kohsuke 已提交
790
            return false;
791 792
        };
        e.tabIndex = 9999; // make help link unnavigable from keyboard
K
kohsuke 已提交
793 794 795
        e = null; // avoid memory leak
    },

796 797 798 799
    "TEXTAREA.codemirror" : function(e) {
        var h = e.clientHeight;
        var config = e.getAttribute("codemirror-config") || "";
        config = eval('({'+config+'})');
800 801 802 803 804 805 806 807 808 809 810
        var codemirror = CodeMirror.fromTextArea(e,config);
        e.codemirrorObject = codemirror;
        if(typeof(codemirror.getScrollerElement) !== "function") {
            // Maybe older versions of CodeMirror do not provide getScrollerElement method.
            codemirror.getScrollerElement = function(){
                return findElementsBySelector(codemirror.getWrapperElement(), ".CodeMirror-scroll")[0];
            };
        }
        var scroller = codemirror.getScrollerElement();
        scroller.setAttribute("style","border:1px solid black;");
        scroller.style.height = h+"px";
811 812
    },

813 814 815 816
    // Script Console : settings and shortcut key
    "TEXTAREA.script" : function(e) {
        (function() {
            var cmdKeyDown = false;
817
            var mode = e.getAttribute("script-mode") || "text/x-groovy";
818 819 820
            var readOnly = eval(e.getAttribute("script-readOnly")) || false;
            
            var w = CodeMirror.fromTextArea(e,{
821
              mode: mode,
822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878
              lineNumbers: true,
              matchBrackets: true,
              readOnly: readOnly,
              onKeyEvent: function(editor, event){
                function isGeckoCommandKey() {
                    return Prototype.Browser.Gecko && event.keyCode == 224
                }
                function isOperaCommandKey() {
                    return Prototype.Browser.Opera && event.keyCode == 17
                }
                function isWebKitCommandKey() {
                    return Prototype.Browser.WebKit && (event.keyCode == 91 || event.keyCode == 93)
                }
                function isCommandKey() {
                    return isGeckoCommandKey() || isOperaCommandKey() || isWebKitCommandKey();
                }
                function isReturnKeyDown() {
                    return event.type == 'keydown' && event.keyCode == Event.KEY_RETURN;
                }
                function getParentForm(element) {
                    if (element == null) throw 'not found a parent form';
                    if (element instanceof HTMLFormElement) return element;
                    
                    return getParentForm(element.parentNode);
                }
                function saveAndSubmit() {
                    editor.save();
                    getParentForm(e).submit();
                    event.stop();
                }
                
                // Mac (Command + Enter)
                if (navigator.userAgent.indexOf('Mac') > -1) {
                    if (event.type == 'keydown' && isCommandKey()) {
                        cmdKeyDown = true;
                    }
                    if (event.type == 'keyup' && isCommandKey()) {
                        cmdKeyDown = false;
                    }
                    if (cmdKeyDown && isReturnKeyDown()) {
                        saveAndSubmit();
                        return true;
                    }
                  
                // Windows, Linux (Ctrl + Enter)
                } else {
                    if (event.ctrlKey && isReturnKeyDown()) {
                        saveAndSubmit();
                        return true;
                    }
                }
              }
            }).getWrapperElement();
            w.setAttribute("style","border:1px solid black; margin-top: 1em; margin-bottom: 1em")
        })();
	},

K
kohsuke 已提交
879 880 881 882 883 884 885
// deferred client-side clickable map.
// this is useful where the generation of <map> element is time consuming
    "IMG[lazymap]" : function(e) {
        new Ajax.Request(
            e.getAttribute("lazymap"),
            {
                method : 'get',
886
                onSuccess : function(x) {
K
kohsuke 已提交
887 888 889 890 891 892 893 894 895
                    var div = document.createElement("div");
                    document.body.appendChild(div);
                    div.innerHTML = x.responseText;
                    var id = "map" + (iota++);
                    div.firstChild.setAttribute("name", id);
                    e.setAttribute("usemap", "#" + id);
                }
            });
    },
896 897 898

    // button to add a new repeatable block
    "INPUT.repeatable-add" : function(e) {
899
        makeButton(e,function(e) {
K
kohsuke 已提交
900
            repeatableSupport.onAdd(e.target);
901
        });
K
kohsuke 已提交
902
        e = null; // avoid memory leak
903 904 905
    },

    "INPUT.repeatable-delete" : function(e) {
906
        makeButton(e,function(e) {
K
kohsuke 已提交
907
            repeatableSupport.onDelete(e.target);
908
        });
K
kohsuke 已提交
909
        e = null; // avoid memory leak
910 911
    },

K
kohsuke 已提交
912 913
    // resizable text area
    "TEXTAREA" : function(textarea) {
914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932
        if(Element.hasClassName(textarea,"rich-editor")) {
            // rich HTML editor
            try {
                var editor = new YAHOO.widget.Editor(textarea, {
                    dompath: true,
                    animate: true,
                    handleSubmit: true
                });
                // probably due to the timing issue, we need to let the editor know
                // that DOM is ready
                editor.DOMReady=true;
                editor.fireQueue();
                editor.render();
            } catch(e) {
                alert(e);
            }
            return;
        }

933 934 935 936 937
        // CodeMirror inserts a wrapper element next to the textarea.
        // textarea.nextSibling may not be the handle.
        var handles = findElementsBySelector(textarea.parentNode, ".textarea-handle");
        if(handles.length != 1) return;
        var handle = handles[0];
K
kohsuke 已提交
938 939 940

        var Event = YAHOO.util.Event;

941 942 943
        function getCodemirrorScrollerOrTextarea(){
            return textarea.codemirrorObject ? textarea.codemirrorObject.getScrollerElement() : textarea;
        }
K
kohsuke 已提交
944
        handle.onmousedown = function(ev) {
K
kohsuke 已提交
945
            ev = Event.getEvent(ev);
946 947 948
            var s = getCodemirrorScrollerOrTextarea();
            var offset = s.offsetHeight-Event.getPageY(ev);
            s.style.opacity = 0.5;
K
kohsuke 已提交
949
            document.onmousemove = function(ev) {
K
kohsuke 已提交
950
                ev = Event.getEvent(ev);
K
kohsuke 已提交
951
                function max(a,b) { if(a<b) return b; else return a; }
952
                s.style.height = max(32, offset + Event.getPageY(ev)) + 'px';
K
kohsuke 已提交
953
                return false;
K
kohsuke 已提交
954 955 956 957
            };
            document.onmouseup = function() {
                document.onmousemove = null;
                document.onmouseup = null;
958 959
                var s = getCodemirrorScrollerOrTextarea();
                s.style.opacity = 1;
K
kohsuke 已提交
960
            }
K
kohsuke 已提交
961 962
        };
        handle.ondblclick = function() {
963 964 965
            var s = getCodemirrorScrollerOrTextarea();
            s.style.height = "1px"; // To get actual height of the textbox, shrink it and show its scrollbar
            s.style.height = s.scrollHeight + 'px';
K
kohsuke 已提交
966 967 968
        }
    },

969 970
    // structured form submission
    "FORM" : function(form) {
971
        crumb.appendToForm(form);
972
        if(Element.hasClassName(form, "no-json"))
973
            return;
K
kohsuke 已提交
974
        // add the hidden 'json' input field, which receives the form structure in JSON
975
        var div = document.createElement("div");
976
        div.innerHTML = "<input type=hidden name=json value=init>";
977
        form.appendChild(div);
978 979 980

        var oldOnsubmit = form.onsubmit;
        if (typeof oldOnsubmit == "function") {
981
            form.onsubmit = function() { return buildFormTree(this) && oldOnsubmit.call(this); }
982
        } else {
983
            form.onsubmit = function() { return buildFormTree(this); };
984 985
        }

986 987 988
        form = null; // memory leak prevention
    },

989 990
    // hook up tooltip.
    //   add nodismiss="" if you'd like to display the tooltip forever as long as the mouse is on the element.
991
    "[tooltip]" : function(e) {
992 993 994 995 996
        applyTooltip(e,e.getAttribute("tooltip"));
    },

    "INPUT.submit-button" : function(e) {
        makeButton(e);
K
kohsuke 已提交
997 998 999 1000
    },

    "INPUT.yui-button" : function(e) {
        makeButton(e);
K
kohsuke 已提交
1001 1002
    },

K
kohsuke 已提交
1003 1004 1005 1006 1007 1008
    "TR.optional-block-start": function(e) { // see optionalBlock.jelly
        // set start.ref to checkbox in preparation of row-set-end processing
        var checkbox = e.firstChild.firstChild;
        e.setAttribute("ref", checkbox.id = "cb"+(iota++));
    },

K
Kohsuke Kawaguchi 已提交
1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056
    // radioBlock.jelly
    "INPUT.radio-block-control" : function(r) {
        r.id = "radio-block-"+(iota++);

        // when one radio button is clicked, we need to update foldable block for
        // other radio buttons with the same name. To do this, group all the
        // radio buttons with the same name together and hang it under the form object
        var f = r.form;
        var radios = f.radios;
        if (radios == null)
            f.radios = radios = {};

        var g = radios[r.name];
        if (g == null) {
            radios[r.name] = g = object(radioBlockSupport);
            g.buttons = [];
        }

        var s = findAncestorClass(r,"radio-block-start");
        s.setAttribute("ref", r.id);

        // find the end node
        var e = (function() {
            var e = s;
            var cnt=1;
            while(cnt>0) {
                e = e.nextSibling;
                if (Element.hasClassName(e,"radio-block-start"))
                    cnt++;
                if (Element.hasClassName(e,"radio-block-end"))
                    cnt--;
            }
            return e;
        })();

        var u = function() {
            g.updateSingleButton(r,s,e);
        };
        g.buttons.push(u);

        // apply the initial visibility
        u();

        // install event handlers to update visibility.
        // needs to use onclick and onchange for Safari compatibility
        r.onclick = r.onchange = function() { g.updateButtons(); };
    },

1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105
    // see RowVisibilityGroupTest
    "TR.rowvg-start" : function(e) {
        // figure out the corresponding end marker
        function findEnd(e) {
            for( var depth=0; ; e=e.nextSibling) {
                if(Element.hasClassName(e,"rowvg-start"))    depth++;
                if(Element.hasClassName(e,"rowvg-end"))      depth--;
                if(depth==0)    return e;
            }
        }

        e.rowVisibilityGroup = {
            outerVisible: true,
            innerVisible: true,
            /**
             * TR that marks the beginning of this visibility group.
             */
            start: e,
            /**
             * TR that marks the end of this visibility group.
             */
            end: findEnd(e),

            /**
             * Considers the visibility of the row group from the point of view of outside.
             * If you think of a row group like a logical DOM node, this is akin to its .style.display.
             */
            makeOuterVisisble : function(b) {
                this.outerVisible = b;
                this.updateVisibility();
            },

            /**
             * Considers the visibility of the rows in this row group. Since all the rows in a rowvg
             * shares the single visibility, this just needs to be one boolean, as opposed to many.
             *
             * If you think of a row group like a logical DOM node, this is akin to its children's .style.display.
             */
            makeInnerVisisble : function(b) {
                this.innerVisible = b;
                this.updateVisibility();
            },

            /**
             * Based on innerVisible and outerVisible, update the relevant rows' actual CSS display attribute.
             */
            updateVisibility : function() {
                var display = (this.outerVisible && this.innerVisible) ? "" : "none";
                for (var e=this.start; e!=this.end; e=e.nextSibling) {
1106
                    if (e.nodeType!=1)  continue;
1107 1108 1109 1110 1111 1112 1113
                    if (e.rowVisibilityGroup && e!=this.start) {
                        e.rowVisibilityGroup.makeOuterVisisble(this.innerVisible);
                        e = e.rowVisibilityGroup.end; // the above call updates visibility up to e.rowVisibilityGroup.end inclusive
                    } else {
                        e.style.display = display;
                    }
                }
1114
                layoutUpdateCallback.call();
1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133
            },

            /**
             * Enumerate each row and pass that to the given function.
             *
             * @param {boolean} recursive
             *      If true, this visits all the rows from nested visibility groups.
             */
            eachRow : function(recursive,f) {
                if (recursive) {
                    for (var e=this.start; e!=this.end; e=e.nextSibling)
                        f(e);
                } else {
                    throw "not implemented yet";
                }
            }
        };
    },

K
kohsuke 已提交
1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144
    "TR.row-set-end": function(e) { // see rowSet.jelly and optionalBlock.jelly
        // figure out the corresponding start block
        var end = e;

        for( var depth=0; ; e=e.previousSibling) {
            if(Element.hasClassName(e,"row-set-end"))        depth++;
            if(Element.hasClassName(e,"row-set-start"))      depth--;
            if(depth==0)    break;
        }
        var start = e;

K
Kohsuke Kawaguchi 已提交
1145 1146 1147
        // @ref on start refers to the ID of the element that controls the JSON object created from these rows
        // if we don't find it, turn the start node into the governing node (thus the end result is that you
        // created an intermediate JSON object that's always on.)
K
kohsuke 已提交
1148 1149 1150 1151 1152 1153 1154
        var ref = start.getAttribute("ref");
        if(ref==null)
            start.id = ref = "rowSetStart"+(iota++);

        applyNameRef(start,end,ref);
    },

1155 1156
    "TR.optional-block-start ": function(e) { // see optionalBlock.jelly
        // this is suffixed by a pointless string so that two processing for optional-block-start
K
kohsuke 已提交
1157 1158 1159 1160 1161 1162
        // can sandwitch row-set-end
        // this requires "TR.row-set-end" to mark rows
        var checkbox = e.firstChild.firstChild;
        updateOptionalBlock(checkbox,false);
    },

K
kohsuke 已提交
1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190
    // image that shows [+] or [-], with hover effect.
    // oncollapsed and onexpanded will be called when the button is triggered.
    "IMG.fold-control" : function(e) {
        function changeTo(e,img) {
            var src = e.src;
            e.src = src.substring(0,src.lastIndexOf('/'))+"/"+e.getAttribute("state")+img;
        }
        e.onmouseover = function() {
            changeTo(this,"-hover.png");
        };
        e.onmouseout = function() {
            changeTo(this,".png");
        };
        e.parentNode.onclick = function(event) {
            var e = this.firstChild;
            var s = e.getAttribute("state");
            if(s=="plus") {
                e.setAttribute("state","minus");
                if(e.onexpanded)    e.onexpanded();
            } else {
                e.setAttribute("state","plus");
                if(e.oncollapsed)    e.oncollapsed();
            }
            changeTo(e,"-hover.png");
            YAHOO.util.Event.stopEvent(event);
            return false;
        };
        e = null; // memory leak prevention
1191 1192
    },

1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205
    // radio buttons in repeatable content
    "DIV.repeated-chunk" : function(d) {
        var inputs = d.getElementsByTagName('INPUT');
        for (var i = 0; i < inputs.length; i++) {
            if (inputs[i].type == 'radio') {
                // Need to uniquify each set of radio buttons in repeatable content.
                // buildFormTree will remove the prefix before form submission.
                var prefix = d.getAttribute('radioPrefix');
                if (!prefix) {
                    prefix = 'removeme' + (iota++) + '_';
                    d.setAttribute('radioPrefix', prefix);
                }
                inputs[i].name = prefix + inputs[i].name;
1206 1207
                // Reselect anything unselected by browser before names uniquified:
                if (inputs[i].defaultChecked) inputs[i].checked = true;
1208 1209 1210 1211
            }
        }
    },

1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223
    // editableComboBox.jelly
    "INPUT.combobox" : function(c) {
        // Next element after <input class="combobox"/> should be <div class="combobox-values">
        var vdiv = c.nextSibling;
        if (Element.hasClassName(vdiv, "combobox-values")) {
            createComboBox(c, function() {
                var values = [];
                for (var value = vdiv.firstChild; value; value = value.nextSibling)
                    values.push(value.getAttribute('value'));
                return values;
            });
        }
1224 1225
    },

1226 1227 1228 1229
    // dropdownList.jelly
    "SELECT.dropdownList" : function(e) {
        if(isInsideRemovable(e))    return;

1230
        var subForms = [];
1231
        var start = findFollowingTR(e, 'dropdownList-container').firstChild.nextSibling, end;
1232
        do { start = start.firstChild; } while (start && start.tagName != 'TR');
1233 1234

        if (start && !Element.hasClassName(start,'dropdownList-start'))
1235 1236
            start = findFollowingTR(start, 'dropdownList-start');
        while (start != null) {
1237 1238
            subForms.push(start);
            start = findFollowingTR(start, 'dropdownList-start');
1239 1240
        }

1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258
        // control visibility
        function updateDropDownList() {
            for (var i = 0; i < subForms.length; i++) {
                var show = e.selectedIndex == i;
                var f = subForms[i];

                if (show)   renderOnDemand(f.nextSibling);
                f.rowVisibilityGroup.makeInnerVisisble(show);

                // TODO: this is actually incorrect in the general case if nested vg uses field-disabled
                // so far dropdownList doesn't create such a situation.
                f.rowVisibilityGroup.eachRow(true, show?function(e) {
                    e.removeAttribute("field-disabled");
                } : function(e) {
                    e.setAttribute("field-disabled","true");
                });
            }
        }
K
Kohsuke Kawaguchi 已提交
1259

1260 1261 1262
        e.onchange = updateDropDownList;

        updateDropDownList();
1263 1264
    },

1265 1266
    // select.jelly
    "SELECT.select" : function(e) {
1267 1268
        // controls that this SELECT box depends on
        refillOnChange(e,function(params) {
1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287
            var value = e.value;
            updateListBox(e,e.getAttribute("fillUrl"),{
                parameters: params,
                onSuccess: function() {
                    if (value=="") {
                        // reflect the initial value. if the control depends on several other SELECT.select,
                        // it may take several updates before we get the right items, which is why all these precautions.
                        var v = e.getAttribute("value");
                        if (v) {
                            e.value = v;
                            if (e.value==v) e.removeAttribute("value"); // we were able to apply our initial value
                        }
                    }

                    // if the update changed the current selection, others listening to this control needs to be notified.
                    if (e.value!=value) fireEvent(e,"change");
                }
            });
        });
1288
    },
1289

1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312
    // combobox.jelly
    "INPUT.combobox2" : function(e) {
        var items = [];

        var c = new ComboBox(e,function(value) {
            var candidates = [];
            for (var i=0; i<items.length; i++) {
                if (items[i].indexOf(value)==0) {
                    candidates.push(items[i]);
                    if (candidates.length>20)   break;
                }
            } 
            return candidates;
        }, {});

        refillOnChange(e,function(params) {
            new Ajax.Request(e.getAttribute("fillUrl"),{
                parameters: params,
                onSuccess : function(rsp) {
                    items = eval('('+rsp.responseText+')');
                }
            });
        });
1313
    },
1314

1315 1316 1317 1318 1319 1320 1321
    "A.showDetails" : function(e) {
        e.onclick = function() {
            this.style.display = 'none';
            this.nextSibling.style.display = 'block';
            return false;
        };
        e = null; // avoid memory leak
1322 1323
    },

1324 1325 1326 1327 1328 1329
    "DIV.behavior-loading" : function(e) {
        e.style.display = 'none';
    },

    ".button-with-dropdown" : function (e) {
        new YAHOO.widget.Button(e, { type: "menu", menu: e.nextSibling });
1330 1331
    },

1332 1333 1334 1335
    "DIV.textarea-preview-container" : function (e) {
        var previewDiv = findElementsBySelector(e,".textarea-preview")[0];
        var showPreview = findElementsBySelector(e,".textarea-show-preview")[0];
        var hidePreview = findElementsBySelector(e,".textarea-hide-preview")[0];
1336 1337
        $(hidePreview).hide();
        $(previewDiv).hide();
1338 1339 1340

        showPreview.onclick = function() {
            // Several TEXTAREAs may exist if CodeMirror is enabled. The first one has reference to the CodeMirror object.
1341
            var textarea = e.parentNode.getElementsByTagName("TEXTAREA")[0];
1342
            var text = textarea.codemirrorObject ? textarea.codemirrorObject.getValue() : textarea.value;
1343 1344 1345 1346
            var render = function(txt) {
                $(hidePreview).show();
                $(previewDiv).show();
                previewDiv.innerHTML = txt;
1347
                layoutUpdateCallback.call();
1348
            };
1349

1350
            new Ajax.Request(rootURL + showPreview.getAttribute("previewEndpoint"), {
1351 1352 1353
                method: "POST",
                requestHeaders: "Content-Type: application/x-www-form-urlencoded",
                parameters: {
1354
                    text: text
1355 1356
                },
                onSuccess: function(obj) {
1357
                    render(obj.responseText)
1358 1359 1360 1361 1362 1363 1364 1365
                },
                onFailure: function(obj) {
                    render(obj.status + " " + obj.statusText + "<HR/>" + obj.responseText)
                }
            });
            return false;
        }

1366
        hidePreview.onclick = function() {
1367 1368 1369
            $(hidePreview).hide();
            $(previewDiv).hide();
        };
1370 1371 1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382
    },

    /*
        Use on div tag to make it sticky visible on the bottom of the page.
        When page scrolls it remains in the bottom of the page
        Convenient on "OK" button and etc for a long form page
     */
    "#bottom-sticker" : function(sticker) {
        var DOM = YAHOO.util.Dom;

        var shadow = document.createElement("div");
        sticker.parentNode.insertBefore(shadow,sticker);

K
Kohsuke Kawaguchi 已提交
1383 1384 1385
        var edge = document.createElement("div");
        edge.className = "bottom-sticker-edge";
        sticker.insertBefore(edge,sticker.firstChild);
1386 1387 1388 1389 1390 1391 1392 1393 1394

        function adjustSticker() {
            shadow.style.height = sticker.offsetHeight + "px";

            var viewport = DOM.getClientRegion();
            var pos = DOM.getRegion(shadow);

            sticker.style.position = "fixed";
            sticker.style.bottom = Math.max(0, viewport.bottom - pos.bottom) + "px"
1395
            sticker.style.left = Math.max(0,pos.left-viewport.left) + "px"
1396 1397
        }

K
Kohsuke Kawaguchi 已提交
1398 1399 1400 1401 1402 1403
        // react to layout change
        Element.observe(window,"scroll",adjustSticker);
        Element.observe(window,"resize",adjustSticker);
        // initial positioning
        Element.observe(window,"load",adjustSticker);
        adjustSticker();
1404
        layoutUpdateCallback.add(adjustSticker);
K
Kohsuke Kawaguchi 已提交
1405 1406 1407 1408 1409 1410 1411 1412 1413 1414 1415 1416 1417 1418 1419 1420 1421 1422 1423 1424
    },

    "#top-sticker" : function(sticker) {
        var DOM = YAHOO.util.Dom;

        var shadow = document.createElement("div");
        sticker.parentNode.insertBefore(shadow,sticker);

        var edge = document.createElement("div");
        edge.className = "top-sticker-edge";
        sticker.insertBefore(edge);

        function adjustSticker() {
            shadow.style.height = sticker.offsetHeight + "px";

            var viewport = DOM.getClientRegion();
            var pos = DOM.getRegion(shadow);

            sticker.style.position = "fixed";
            sticker.style.top = Math.max(0, pos.top-viewport.top) + "px"
1425
            sticker.style.left = Math.max(0,pos.left-viewport.left) + "px"
K
Kohsuke Kawaguchi 已提交
1426 1427
        }

1428 1429 1430 1431 1432 1433
        // react to layout change
        Element.observe(window,"scroll",adjustSticker);
        Element.observe(window,"resize",adjustSticker);
        // initial positioning
        Element.observe(window,"load",adjustSticker);
        adjustSticker();
1434
    }
1435 1436 1437
};

function applyTooltip(e,text) {
1438
        // copied from YAHOO.widget.Tooltip.prototype.configContext to efficiently add a new element
K
kohsuke 已提交
1439
        // event registration via YAHOO.util.Event.addListener leaks memory, so do it by ourselves here
1440 1441 1442 1443 1444
        e.onmouseover = function(ev) {
            var delay = this.getAttribute("nodismiss")!=null ? 99999999 : 5000;
            tooltip.cfg.setProperty("autodismissdelay",delay);
            return tooltip.onContextMouseOver.call(this,YAHOO.util.Event.getEvent(ev),tooltip);
        }
1445 1446
        e.onmousemove = function(ev) { return tooltip.onContextMouseMove.call(this,YAHOO.util.Event.getEvent(ev),tooltip); }
        e.onmouseout  = function(ev) { return tooltip.onContextMouseOut .call(this,YAHOO.util.Event.getEvent(ev),tooltip); }
1447
        e.title = text;
K
kohsuke 已提交
1448
        e = null; // avoid memory leak
1449
}
K
kohsuke 已提交
1450

1451 1452 1453 1454 1455 1456 1457 1458
var Path = {
  tail : function(p) {
      var idx = p.lastIndexOf("/");
      if (idx<0)    return p;
      return p.substring(idx+1);
  }
};

1459 1460 1461 1462 1463 1464 1465 1466 1467
/**
 * Install change handlers based on the 'fillDependsOn' attribute.
 */
function refillOnChange(e,onChange) {
    var deps = [];

    function h() {
        var params = {};
        deps.each(function (d) {
1468
            params[d.name] = controlValue(d.control);
1469 1470 1471 1472 1473 1474 1475 1476
        });
        onChange(params);
    }
    var v = e.getAttribute("fillDependsOn");
    if (v!=null) {
        v.split(" ").each(function (name) {
            var c = findNearBy(e,name);
            if (c==null) {
1477 1478
                if (window.console!=null)  console.warn("Unable to find nearby "+name);
                if (window.YUI!=null)      YUI.log("Unable to find a nearby control of the name "+name,"warn")
1479 1480
                return;
            }
1481
            try { c.addEventListener("change",h,false); } catch (ex) { c.attachEvent("onchange",h); }
1482
            deps.push({name:Path.tail(name),control:c});
1483 1484 1485 1486 1487
        });
    }
    h();   // initial fill
}

K
kohsuke 已提交
1488 1489 1490
Behaviour.register(hudsonRules);


K
kohsuke 已提交
1491 1492 1493 1494 1495 1496

function xor(a,b) {
    // convert both values to boolean by '!' and then do a!=b
    return !a != !b;
}

K
kohsuke 已提交
1497
// used by editableDescription.jelly to replace the description field with a form
1498
function replaceDescription() {
K
kohsuke 已提交
1499 1500 1501 1502 1503 1504 1505
    var d = document.getElementById("description");
    d.firstChild.nextSibling.innerHTML = "<div class='spinner-right'>loading...</div>";
    new Ajax.Request(
        "./descriptionForm",
        {
          onComplete : function(x) {
            d.innerHTML = x.responseText;
1506 1507 1508 1509
            evalInnerHtmlScripts(x.responseText,function() {
                Behaviour.applySubtree(d);
                d.getElementsByTagName("TEXTAREA")[0].focus();
            });
1510
            layoutUpdateCallback.call();
K
kohsuke 已提交
1511 1512 1513 1514
          }
        }
    );
    return false;
K
kohsuke 已提交
1515 1516
}

K
Kohsuke Kawaguchi 已提交
1517 1518 1519 1520
/**
 * Indicates that form fields from rows [s,e) should be grouped into a JSON object,
 * and attached under the element identified by the specified id.
 */
1521 1522 1523
function applyNameRef(s,e,id) {
    $(id).groupingNode = true;
    // s contains the node itself
1524 1525
    for(var x=s.nextSibling; x!=e; x=x.nextSibling) {
        // to handle nested <f:rowSet> correctly, don't overwrite the existing value
1526
        if(x.nodeType==1 && x.getAttribute("nameRef")==null)
1527 1528
            x.setAttribute("nameRef",id);
    }
1529 1530
}

1531

K
kohsuke 已提交
1532
// used by optionalBlock.jelly to update the form status
1533
//   @param c     checkbox element
1534
function updateOptionalBlock(c,scroll) {
1535 1536
    // find the start TR
    var s = c;
1537
    while(!Element.hasClassName(s, "optional-block-start"))
1538
        s = s.parentNode;
K
kohsuke 已提交
1539

1540 1541 1542 1543
    // find the beginning of the rowvg
    var vg = s;
    while (!Element.hasClassName(vg,"rowvg-start"))
        vg = vg.nextSibling;
K
kohsuke 已提交
1544

1545
    var checked = xor(c.checked,Element.hasClassName(c,"negative"));
K
kohsuke 已提交
1546

1547
    vg.rowVisibilityGroup.makeInnerVisisble(checked);
1548

K
kohsuke 已提交
1549 1550 1551 1552
    if(checked && scroll) {
        var D = YAHOO.util.Dom;

        var r = D.getRegion(s);
1553
        r = r.union(D.getRegion(vg.rowVisibilityGroup.end));
K
kohsuke 已提交
1554 1555
        scrollIntoView(r);
    }
1556 1557 1558 1559 1560 1561 1562 1563 1564 1565 1566

    if (c.name == 'hudson-tools-InstallSourceProperty') {
        // Hack to hide tool home when "Install automatically" is checked.
        var homeField = findPreviousFormItem(c, 'home');
        if (homeField != null && homeField.value == '') {
            var tr = findAncestor(homeField, 'TR');
            if (tr != null) {
                tr.style.display = c.checked ? 'none' : '';
            }
        }
    }
K
kohsuke 已提交
1567
}
K
kohsuke 已提交
1568 1569 1570 1571 1572 1573 1574 1575 1576 1577 1578 1579 1580 1581 1582 1583 1584 1585 1586 1587 1588 1589 1590 1591 1592 1593 1594 1595 1596 1597 1598 1599 1600


//
// Auto-scroll support for progressive log output.
//   See http://radio.javaranch.com/pascarello/2006/08/17/1155837038219.html
//
function AutoScroller(scrollContainer) {
    // get the height of the viewport.
    // See http://www.howtocreate.co.uk/tutorials/javascript/browserwindow
    function getViewportHeight() {
        if (typeof( window.innerWidth ) == 'number') {
            //Non-IE
            return window.innerHeight;
        } else if (document.documentElement && ( document.documentElement.clientWidth || document.documentElement.clientHeight )) {
            //IE 6+ in 'standards compliant mode'
            return document.documentElement.clientHeight;
        } else if (document.body && ( document.body.clientWidth || document.body.clientHeight )) {
            //IE 4 compatible
            return document.body.clientHeight;
        }
        return null;
    }

    return {
        bottomThreshold : 25,
        scrollContainer: scrollContainer,

        getCurrentHeight : function() {
            var scrollDiv = $(this.scrollContainer);

            if (scrollDiv.scrollHeight > 0)
                return scrollDiv.scrollHeight;
            else
O
OHTAKE Tomohiro 已提交
1601
                if (scrollDiv.offsetHeight > 0)
K
kohsuke 已提交
1602 1603 1604 1605 1606 1607 1608 1609 1610 1611 1612 1613 1614 1615
                    return scrollDiv.offsetHeight;

            return null; // huh?
        },

        // return true if we are in the "stick to bottom" mode
        isSticking : function() {
            var scrollDiv = $(this.scrollContainer);
            var currentHeight = this.getCurrentHeight();

            // when used with the BODY tag, the height needs to be the viewport height, instead of
            // the element height.
            //var height = ((scrollDiv.style.pixelHeight) ? scrollDiv.style.pixelHeight : scrollDiv.offsetHeight);
            var height = getViewportHeight();
1616 1617
            var scrollPos = Math.max(scrollDiv.scrollTop, document.documentElement.scrollTop);
            var diff = currentHeight - scrollPos - height;
K
kohsuke 已提交
1618 1619 1620 1621 1622 1623 1624
            // window.alert("currentHeight=" + currentHeight + ",scrollTop=" + scrollDiv.scrollTop + ",height=" + height);

            return diff < this.bottomThreshold;
        },

        scrollToBottom : function() {
            var scrollDiv = $(this.scrollContainer);
1625 1626 1627
            var currentHeight = this.getCurrentHeight();
            if(document.documentElement) document.documentElement.scrollTop = currentHeight
            scrollDiv.scrollTop = currentHeight;
K
kohsuke 已提交
1628 1629 1630
        }
    };
}
1631

1632 1633 1634 1635 1636 1637 1638 1639 1640 1641 1642 1643 1644 1645 1646 1647 1648 1649 1650 1651 1652 1653
// scroll the current window to display the given element or the region.
function scrollIntoView(e) {
    function calcDelta(ex1,ex2,vx1,vw) {
        var vx2=vx1+vw;
        var a;
        a = Math.min(vx1-ex1,vx2-ex2);
        if(a>0)     return -a;
        a = Math.min(ex1-vx1,ex2-vx2);
        if(a>0)     return a;
        return 0;
    }

    var D = YAHOO.util.Dom;

    var r;
    if(e.tagName!=null) r = D.getRegion(e);
    else                r = e;

    var dx = calcDelta(r.left,r.right, document.body.scrollLeft, D.getViewportWidth());
    var dy = calcDelta(r.top, r.bottom,document.body.scrollTop,  D.getViewportHeight());
    window.scrollBy(dx,dy);
}
1654 1655 1656 1657

// used in expandableTextbox.jelly to change a input field into a text area
function expandTextArea(button,id) {
    button.style.display="none";
1658
    var field = button.parentNode.previousSibling.children[0];
1659
    var value = field.value.replace(/ +/g,'\n');
1660 1661 1662 1663
    
    var n = button; 
    while (n.tagName != "TABLE")
    {
K
kohsuke 已提交
1664
        n = n.parentNode;
1665 1666 1667
    }

    n.parentNode.innerHTML = 
1668 1669
        "<textarea rows=8 class='setting-input' name='"+field.name+"'>"+value+"</textarea>";
}
1670 1671 1672

// refresh a part of the HTML specified by the given ID,
// by using the contents fetched from the given URL.
1673
function refreshPart(id,url) {
1674
    var f = function() {
1675
        new Ajax.Request(url, {
1676
            onSuccess: function(rsp) {
1677 1678 1679 1680 1681 1682 1683 1684
                var hist = $(id);
                var p = hist.parentNode;
                var next = hist.nextSibling;
                p.removeChild(hist);

                var div = document.createElement('div');
                div.innerHTML = rsp.responseText;

1685 1686
                var node = div.firstChild;
                p.insertBefore(node, next);
K
kohsuke 已提交
1687

1688
                Behaviour.applySubtree(node);
1689
                layoutUpdateCallback.call();
K
kohsuke 已提交
1690

1691
                if(isRunAsTest) return;
1692
                refreshPart(id,url);
1693 1694
            }
        });
1695 1696 1697 1698 1699
    };
    // if run as test, just do it once and do it now to make sure it's working,
    // but don't repeat.
    if(isRunAsTest) f();
    else    window.setTimeout(f, 5000);
K
kohsuke 已提交
1700 1701 1702 1703 1704 1705
}


/*
    Perform URL encode.
    Taken from http://www.cresc.co.jp/tech/java/URLencoding/JavaScript_URLEncoding.htm
1706
    @deprecated Use standard javascript method "encodeURIComponent" instead
K
kohsuke 已提交
1707 1708 1709 1710 1711 1712 1713 1714 1715 1716 1717 1718 1719 1720 1721 1722 1723 1724 1725 1726 1727 1728 1729 1730 1731 1732 1733 1734 1735 1736 1737 1738 1739 1740 1741 1742
*/
function encode(str){
    var s, u;
    var s0 = "";                // encoded str

    for (var i = 0; i < str.length; i++){   // scan the source
        s = str.charAt(i);
        u = str.charCodeAt(i);          // get unicode of the char

        if (s == " "){s0 += "+";}       // SP should be converted to "+"
        else {
            if ( u == 0x2a || u == 0x2d || u == 0x2e || u == 0x5f || ((u >= 0x30) && (u <= 0x39)) || ((u >= 0x41) && (u <= 0x5a)) || ((u >= 0x61) && (u <= 0x7a))){     // check for escape
                s0 = s0 + s;           // don't escape
            } else {                      // escape
                if ((u >= 0x0) && (u <= 0x7f)){     // single byte format
                    s = "0"+u.toString(16);
                    s0 += "%"+ s.substr(s.length-2);
                } else
                if (u > 0x1fffff){     // quaternary byte format (extended)
                    s0 += "%" + (0xF0 + ((u & 0x1c0000) >> 18)).toString(16);
                    s0 += "%" + (0x80 + ((u & 0x3f000) >> 12)).toString(16);
                    s0 += "%" + (0x80 + ((u & 0xfc0) >> 6)).toString(16);
                    s0 += "%" + (0x80 + (u & 0x3f)).toString(16);
                } else
                if (u > 0x7ff){        // triple byte format
                    s0 += "%" + (0xe0 + ((u & 0xf000) >> 12)).toString(16);
                    s0 += "%" + (0x80 + ((u & 0xfc0) >> 6)).toString(16);
                    s0 += "%" + (0x80 + (u & 0x3f)).toString(16);
                } else {                      // double byte format
                    s0 += "%" + (0xc0 + ((u & 0x7c0) >> 6)).toString(16);
                    s0 += "%" + (0x80 + (u & 0x3f)).toString(16);
                }
            }
        }
    }
    return s0;
1743 1744
}

K
kohsuke 已提交
1745 1746 1747 1748 1749 1750 1751 1752 1753 1754 1755 1756 1757 1758 1759 1760 1761 1762 1763 1764
// when there are multiple form elements of the same name,
// this method returns the input field of the given name that pairs up
// with the specified 'base' input element.
Form.findMatchingInput = function(base, name) {
    // find the FORM element that owns us
    var f = base;
    while (f.tagName != "FORM")
        f = f.parentNode;

    var bases = Form.getInputs(f, null, base.name);
    var targets = Form.getInputs(f, null, name);

    for (var i=0; i<bases.length; i++) {
        if (bases[i] == base)
            return targets[i];
    }

    return null;        // not found
}

1765
// code for supporting repeatable.jelly
K
kohsuke 已提交
1766
var repeatableSupport = {
1767 1768 1769 1770 1771 1772 1773 1774 1775
    // set by the inherited instance to the insertion point DIV
    insertionPoint: null,

    // HTML text of the repeated chunk
    blockHTML: null,

    // containing <div>.
    container: null,

1776 1777 1778
    // block name for structured HTML
    name : null,

1779 1780
    withDragDrop: false,

1781 1782 1783 1784 1785 1786 1787 1788
    // do the initialization
    init : function(container,master,insertionPoint) {
        this.container = $(container);
        this.container.tag = this;
        master = $(master);
        this.blockHTML = master.innerHTML;
        master.parentNode.removeChild(master);
        this.insertionPoint = $(insertionPoint);
1789
        this.name = master.getAttribute("name");
1790
        this.update();
1791
        this.withDragDrop = initContainerDD(container);
1792 1793 1794 1795 1796 1797 1798 1799
    },

    // insert one more block at the insertion position
    expand : function() {
        // importNode isn't supported in IE.
        // nc = document.importNode(node,true);
        var nc = document.createElement("div");
        nc.className = "repeated-chunk";
K
bug fix  
kohsuke 已提交
1800
        nc.setAttribute("name",this.name);
1801 1802
        nc.innerHTML = this.blockHTML;
        this.insertionPoint.parentNode.insertBefore(nc, this.insertionPoint);
1803
        if (this.withDragDrop) prepareDD(nc);
1804

1805
        Behaviour.applySubtree(nc,true);
1806 1807 1808 1809 1810 1811 1812 1813 1814 1815
        this.update();
    },

    // update CSS classes associated with repeated items.
    update : function() {
        var children = [];
        for( var n=this.container.firstChild; n!=null; n=n.nextSibling )
            if(Element.hasClassName(n,"repeated-chunk"))
                children.push(n);

K
kohsuke 已提交
1816 1817 1818
        if(children.length==0) {
            // noop
        } else
1819 1820 1821 1822 1823 1824 1825 1826 1827 1828 1829 1830 1831 1832
        if(children.length==1) {
            children[0].className = "repeated-chunk first last only";
        } else {
            children[0].className = "repeated-chunk first";
            for(var i=1; i<children.length-1; i++)
                children[i].className = "repeated-chunk middle";
            children[children.length-1].className = "repeated-chunk last";
        }
    },

    // these are static methods that don't rely on 'this'

    // called when 'delete' button is clicked
    onDelete : function(n) {
K
Kohsuke Kawaguchi 已提交
1833
        n = findAncestorClass(n,"repeated-chunk");
1834 1835
        var p = n.parentNode;
        p.removeChild(n);
K
Kohsuke Kawaguchi 已提交
1836 1837
        if (p.tag)
            p.tag.update();
1838 1839 1840 1841 1842 1843 1844
    },

    // called when 'add' button is clicked
    onAdd : function(n) {
        while(n.tag==null)
            n = n.parentNode;
        n.tag.expand();
1845 1846 1847 1848 1849 1850 1851 1852
        // Hack to hide tool home when a new tool has some installers.
        var inputs = n.getElementsByTagName('INPUT');
        for (var i = 0; i < inputs.length; i++) {
            var input = inputs[i];
            if (input.name == 'hudson-tools-InstallSourceProperty') {
                updateOptionalBlock(input, false);
            }
        }
1853
    }
1854 1855
};

1856 1857
// prototype object to be duplicated for each radio button group
var radioBlockSupport = {
K
Kohsuke Kawaguchi 已提交
1858
    buttons : null, // set of functions, one for updating one radio block each
1859

1860 1861 1862 1863
    updateButtons : function() {
        for( var i=0; i<this.buttons.length; i++ )
            this.buttons[i]();
    },
1864

1865 1866 1867
    // update one block based on the status of the given radio button
    updateSingleButton : function(radio, blockStart, blockEnd) {
        var show = radio.checked;
1868 1869 1870 1871 1872 1873 1874 1875
        
        if (blockStart.getAttribute('hasHelp') == 'true') {
            n = blockStart.nextSibling;
        } else {
            n = blockStart;
        }
        while((n = n.nextSibling) != blockEnd) {
          n.style.display = show ? "" : "none";
1876 1877
        }
    }
1878
};
1879

1880
function updateBuildHistory(ajaxUrl,nBuild) {
1881
    if(isRunAsTest) return;
1882 1883 1884 1885
    $('buildHistory').headers = ["n",nBuild];

    function updateBuilds() {
        var bh = $('buildHistory');
1886 1887 1888
        if (bh.headers == null) {
            // Yahoo.log("Missing headers in buildHistory element");
        }
K
kohsuke 已提交
1889
        new Ajax.Request(ajaxUrl, {
1890
            requestHeaders: bh.headers,
1891
            onSuccess: function(rsp) {
1892 1893 1894 1895 1896 1897 1898 1899 1900
                var rows = bh.rows;

                //delete rows with transitive data
                while (rows.length > 2 && Element.hasClassName(rows[1], "transitive"))
                    Element.remove(rows[1]);

                // insert new rows
                var div = document.createElement('div');
                div.innerHTML = rsp.responseText;
K
kohsuke 已提交
1901
                Behaviour.applySubtree(div);
1902 1903 1904 1905 1906 1907 1908 1909 1910 1911 1912 1913 1914 1915 1916

                var pivot = rows[0];
                var newRows = div.firstChild.rows;
                for (var i = newRows.length - 1; i >= 0; i--) {
                    pivot.parentNode.insertBefore(newRows[i], pivot.nextSibling);
                }

                // next update
                bh.headers = ["n",rsp.getResponseHeader("n")];
                window.setTimeout(updateBuilds, 5000);
            }
        });
    }
    window.setTimeout(updateBuilds, 5000);
}
1917 1918 1919

// send async request to the given URL (which will send back serialized ListBoxModel object),
// then use the result to fill the list box.
1920 1921 1922 1923 1924 1925
function updateListBox(listBox,url,config) {
    config = config || {};
    config = object(config);
    var originalOnSuccess = config.onSuccess;
    config.onSuccess = function(rsp) {
        var l = $(listBox);
1926 1927 1928
        var currentSelection = l.value;

        // clear the contents
1929 1930
        while(l.length>0)   l.options[0] = null;

1931 1932
        var selectionSet = false; // is the selection forced by the server?
        var possibleIndex = null; // if there's a new option that matches the current value, remember its index
1933 1934 1935
        var opts = eval('('+rsp.responseText+')').values;
        for( var i=0; i<opts.length; i++ ) {
            l.options[i] = new Option(opts[i].name,opts[i].value);
1936
            if(opts[i].selected) {
1937
                l.selectedIndex = i;
1938 1939 1940 1941
                selectionSet = true;
            }
            if (opts[i].value==currentSelection)
                possibleIndex = i;
1942
        }
1943 1944 1945 1946 1947

        // if no value is explicitly selected by the server, try to select the same value
        if (!selectionSet && possibleIndex!=null)
            l.selectedIndex = possibleIndex;

1948 1949 1950 1951
        if (originalOnSuccess!=undefined)
            originalOnSuccess(rsp);
    },
    config.onFailure = function(rsp) {
1952 1953 1954
        // deleting values can result in the data loss, so let's not do that
//        var l = $(listBox);
//        l.options[0] = null;
1955 1956 1957
    }

    new Ajax.Request(url, config);
1958 1959
}

1960 1961 1962 1963 1964
// get the cascaded computed style value. 'a' is the style name like 'backgroundColor'
function getStyle(e,a){
  if(document.defaultView && document.defaultView.getComputedStyle)
    return document.defaultView.getComputedStyle(e,null).getPropertyValue(a.replace(/([A-Z])/g, "-$1"));
  if(e.currentStyle)
K
kohsuke 已提交
1965 1966
    return e.currentStyle[a];
  return null;
1967 1968 1969
};

// set up logic behind the search box
1970
function createSearchBox(searchURL) {
1971 1972 1973 1974 1975 1976
    var ds = new YAHOO.util.XHRDataSource(searchURL+"suggest");
    ds.responseType = YAHOO.util.XHRDataSource.TYPE_JSON;
    ds.responseSchema = {
        resultsList: "suggestions",
        fields: ["name"]
    };
1977 1978
    var ac = new YAHOO.widget.AutoComplete("search-box","search-box-completion",ds);
    ac.typeAhead = false;
L
lvotypko 已提交
1979
    ac.autoHighlight = false;
1980 1981 1982 1983 1984 1985 1986 1987 1988 1989 1990 1991 1992 1993 1994 1995 1996 1997 1998 1999 2000 2001 2002 2003 2004 2005 2006 2007 2008 2009 2010 2011 2012 2013 2014 2015 2016

    var box   = $("search-box");
    var sizer = $("search-box-sizer");
    var comp  = $("search-box-completion");
    var minW  = $("search-box-minWidth");

    Behaviour.addLoadEvent(function(){
        // make sure all three components have the same font settings
        function copyFontStyle(s,d) {
            var ds = d.style;
            ds.fontFamily = getStyle(s,"fontFamily");
            ds.fontSize = getStyle(s,"fontSize");
            ds.fontStyle = getStyle(s,"fontStyle");
            ds.fontWeight = getStyle(s,"fontWeight");
        }

        copyFontStyle(box,sizer);
        copyFontStyle(box,minW);
    });

    // update positions and sizes of the components relevant to search
    function updatePos() {
        function max(a,b) { if(a>b) return a; else return b; }

        sizer.innerHTML = box.value;
        var w = max(sizer.offsetWidth,minW.offsetWidth);
        box.style.width =
        comp.style.width = 
        comp.firstChild.style.width = (w+60)+"px";

        var pos = YAHOO.util.Dom.getXY(box);
        pos[1] += YAHOO.util.Dom.get(box).offsetHeight + 2;
        YAHOO.util.Dom.setXY(comp, pos);
    }

    updatePos();
    box.onkeyup = updatePos;
2017 2018 2019
}


2020 2021 2022
/**
 * Finds the DOM node of the given DOM node that acts as a parent in the form submission.
 *
2023 2024 2025 2026
 * @param {HTMLElement} e
 *      The node whose parent we are looking for.
 * @param {HTMLFormElement} form
 *      The form element that owns 'e'. Passed in as a performance improvement. Can be null.
2027 2028 2029
 * @return null
 *      if the given element shouldn't be a part of the final submission.
 */
2030 2031 2032
function findFormParent(e,form,static) {
    static = static || false;

2033 2034 2035 2036 2037 2038 2039 2040 2041
    if (form==null) // caller can pass in null to have this method compute the owning form
        form = findAncestor(e,"FORM");

    while(e!=form) {
        // this is used to create a group where no single containing parent node exists,
        // like <optionalBlock>
        var nameRef = e.getAttribute("nameRef");
        if(nameRef!=null)
            e = $(nameRef);
2042 2043
        else
            e = e.parentNode;
2044

2045
        if(!static && e.getAttribute("field-disabled")!=null)
2046 2047 2048
            return null;  // this field shouldn't contribute to the final result

        var name = e.getAttribute("name");
2049
        if(name!=null && name.length>0) {
2050
            if(e.tagName=="INPUT" && !static && !xor(e.checked,Element.hasClassName(e,"negative")))
2051 2052 2053 2054 2055 2056 2057 2058 2059
                return null;  // field is not active

            return e;
        }
    }

    return form;
}

2060 2061 2062 2063 2064 2065 2066 2067 2068 2069 2070 2071 2072
// compute the form field name from the control name
function shortenName(name) {
    // [abc.def.ghi] -> abc.def.ghi
    if(name.startsWith('['))
        return name.substring(1,name.length-1);

    // abc.def.ghi -> ghi
    var idx = name.lastIndexOf('.');
    if(idx>=0)  name = name.substring(idx+1);
    return name;
}


2073

2074 2075
//
// structured form submission handling
K
Kohsuke Kawaguchi 已提交
2076
//   see http://wiki.jenkins-ci.org/display/JENKINS/Structured+Form+Submission
2077
function buildFormTree(form) {
K
kohsuke 已提交
2078 2079 2080 2081 2082 2083 2084 2085 2086
    try {
        // I initially tried to use an associative array with DOM elemnets as keys
        // but that doesn't seem to work neither on IE nor Firefox.
        // so I switch back to adding a dynamic property on DOM.
        form.formDom = {}; // root object

        var doms = []; // DOMs that we added 'formDom' for.
        doms.push(form);

2087 2088
        function addProperty(parent,name,value) {
            name = shortenName(name);
K
kohsuke 已提交
2089 2090 2091 2092 2093 2094 2095
            if(parent[name]!=null) {
                if(parent[name].push==null) // is this array?
                    parent[name] = [ parent[name] ];
                parent[name].push(value);
            } else {
                parent[name] = value;
            }
2096 2097
        }

K
kohsuke 已提交
2098 2099 2100
        // find the grouping parent node, which will have @name.
        // then return the corresponding object in the map
        function findParent(e) {
2101 2102 2103 2104 2105 2106 2107 2108
            var p = findFormParent(e,form);
            if (p==null)    return {};

            var m = p.formDom;
            if(m==null) {
                // this is a new grouping node
                doms.push(p);
                p.formDom = m = {};
2109
                addProperty(findParent(p), p.getAttribute("name"), m);
2110
            }
2111
            return m;
K
kohsuke 已提交
2112
        }
2113

K
kohsuke 已提交
2114
        var jsonElement = null;
2115

K
kohsuke 已提交
2116 2117 2118 2119 2120
        for( var i=0; i<form.elements.length; i++ ) {
            var e = form.elements[i];
            if(e.name=="json") {
                jsonElement = e;
                continue;
2121
            }
2122
            if(e.tagName=="FIELDSET")
K
kohsuke 已提交
2123
                continue;
2124 2125 2126 2127 2128 2129 2130 2131 2132 2133 2134
            if(e.tagName=="SELECT" && e.multiple) {
                var values = [];
                for( var o=0; o<e.options.length; o++ ) {
                    var opt = e.options.item(o);
                    if(opt.selected)
                        values.push(opt.value);
                }
                addProperty(findParent(e),e.name,values);
                continue;
            }
                
K
kohsuke 已提交
2135 2136 2137 2138 2139 2140 2141 2142
            var p;
            var type = e.getAttribute("type");
            if(type==null)  type="";
            switch(type.toLowerCase()) {
            case "button":
            case "submit":
                break;
            case "checkbox":
2143
                p = findParent(e);
K
kohsuke 已提交
2144
                var checked = xor(e.checked,Element.hasClassName(e,"negative"));
2145
                if(!e.groupingNode) {
2146 2147 2148 2149
                    v = e.getAttribute("json");
                    if (v) {
                        // if the special attribute is present, we'll either set the value or not. useful for an array of checkboxes
                        // we can't use @value because IE6 sets the value to be "on" if it's left unspecified.
2150 2151 2152 2153 2154 2155
                        if (checked)
                            addProperty(p, e.name, v);
                    } else {// otherwise it'll bind to boolean
                        addProperty(p, e.name, checked);
                    }
                } else {
K
kohsuke 已提交
2156
                    if(checked)
K
kohsuke 已提交
2157 2158
                        addProperty(p, e.name, e.formDom = {});
                }
2159
                break;
2160 2161 2162 2163 2164 2165 2166 2167 2168 2169 2170 2171 2172 2173 2174 2175 2176 2177
            case "file":
                // to support structured form submission with file uploads,
                // rename form field names to unique ones, and leave this name mapping information
                // in JSON. this behavior is backward incompatible, so only do
                // this when
                p = findParent(e);
                if(e.getAttribute("jsonAware")!=null) {
                    var on = e.getAttribute("originalName");
                    if(on!=null) {
                        addProperty(p,on,e.name);
                    } else {
                        var uniqName = "file"+(iota++);
                        addProperty(p,e.name,uniqName);
                        e.setAttribute("originalName",e.name);
                        e.name = uniqName;
                    }
                }
                // switch to multipart/form-data to support file submission
K
See...  
kohsuke 已提交
2178 2179
                // @enctype is the standard, but IE needs @encoding.
                form.enctype = form.encoding = "multipart/form-data";
2180
                break;
K
kohsuke 已提交
2181 2182
            case "radio":
                if(!e.checked)  break;
2183
                while (e.name.substring(0,8)=='removeme')
2184
                    e.name = e.name.substring(e.name.indexOf('_',8)+1);
K
kohsuke 已提交
2185 2186 2187 2188 2189
                if(e.groupingNode) {
                    p = findParent(e);
                    addProperty(p, e.name, e.formDom = { value: e.value });
                    break;
                }
2190

K
kohsuke 已提交
2191 2192 2193 2194 2195 2196
                // otherwise fall through
            default:
                p = findParent(e);
                addProperty(p, e.name, e.value);
                break;
            }
2197 2198
        }

K
kohsuke 已提交
2199
        jsonElement.value = Object.toJSON(form.formDom);
2200

K
kohsuke 已提交
2201 2202 2203
        // clean up
        for( i=0; i<doms.length; i++ )
            doms[i].formDom = null;
2204

2205
        return true;
K
kohsuke 已提交
2206
    } catch(e) {
2207 2208
        alert(e+'\n(form not submitted)');
        return false;
K
kohsuke 已提交
2209
    }
K
kohsuke 已提交
2210 2211
}

2212 2213 2214 2215 2216 2217 2218 2219 2220 2221 2222 2223 2224
/**
 * @param {boolean} toggle
 *      When true, will check all checkboxes in the page. When false, unchecks them all.
 */
var toggleCheckboxes = function(toggle) {
    var inputs = document.getElementsByTagName("input");
    for(var i=0; i<inputs.length; i++) {
        if(inputs[i].type === "checkbox") {
            inputs[i].checked = toggle;
        }
    }
};

K
kohsuke 已提交
2225 2226
// this used to be in prototype.js but it must have been removed somewhere between 1.4.0 to 1.5.1
String.prototype.trim = function() {
K
kohsuke 已提交
2227 2228 2229 2230 2231 2232 2233 2234
    var temp = this;
    var obj = /^(\s*)([\W\w]*)(\b\s*$)/;
    if (obj.test(temp))
        temp = temp.replace(obj, '$2');
    obj = /  /g;
    while (temp.match(obj))
        temp = temp.replace(obj, " ");
    return temp;
K
kohsuke 已提交
2235
}
2236 2237 2238 2239 2240 2241 2242 2243 2244 2245 2246 2247 2248 2249 2250 2251 2252 2253 2254 2255 2256 2257 2258 2259 2260 2261 2262 2263 2264 2265 2266 2267 2268 2269 2270 2271 2272 2273 2274 2275 2276 2277 2278 2279 2280 2281 2282 2283



var hoverNotification = (function() {
    var msgBox;
    var body;

    // animation effect that automatically hide the message box
    var effect = function(overlay, dur) {
        var o = YAHOO.widget.ContainerEffect.FADE(overlay, dur);
        o.animateInCompleteEvent.subscribe(function() {
            window.setTimeout(function() {
                msgBox.hide()
            }, 1500);
        });
        return o;
    }

    function init() {
        if(msgBox!=null)  return;   // already initialized

        var div = document.createElement("DIV");
        document.body.appendChild(div);
        div.innerHTML = "<div id=hoverNotification><div class=bd></div></div>";
        body = $('hoverNotification');
        
        msgBox = new YAHOO.widget.Overlay(body, {
          visible:false,
          width:"10em",
          zIndex:1000,
          effect:{
            effect:effect,
            duration:0.25
          }
        });
        msgBox.render();
    }

    return function(title,anchor) {
        init();
        body.innerHTML = title;
        var xy = YAHOO.util.Dom.getXY(anchor);
        xy[0] += 48;
        xy[1] += anchor.offsetHeight;
        msgBox.cfg.setProperty("xy",xy);
        msgBox.show();
    };
})();
2284 2285

/*
2286
    Drag&Drop implementation for heterogeneous/repeatable lists.
2287
 */
2288 2289 2290 2291 2292 2293 2294 2295 2296 2297 2298 2299 2300 2301 2302 2303 2304 2305 2306 2307
function initContainerDD(e) {
    if (!Element.hasClassName(e,"with-drag-drop")) return false;

    for (e=e.firstChild; e!=null; e=e.nextSibling) {
        if (Element.hasClassName(e,"repeated-chunk"))
            prepareDD(e);
    }
    return true;
}
function prepareDD(e) {
    var h = e;
    // locate a handle
    while (h!=null && !Element.hasClassName(h,"dd-handle"))
        h = h.firstChild ? h.firstChild : h.nextSibling;
    if (h!=null) {
        var dd = new DragDrop(e);
        dd.setHandleElId(h);
    }
}

2308 2309 2310 2311 2312 2313 2314 2315 2316 2317 2318 2319 2320 2321 2322 2323 2324 2325 2326 2327 2328 2329 2330 2331 2332 2333 2334 2335 2336 2337 2338 2339 2340 2341 2342 2343 2344 2345 2346 2347 2348 2349 2350 2351 2352 2353 2354 2355 2356 2357 2358 2359 2360 2361 2362 2363 2364 2365 2366 2367 2368 2369 2370 2371 2372 2373 2374 2375 2376 2377 2378 2379 2380
var DragDrop = function(id, sGroup, config) {
    DragDrop.superclass.constructor.apply(this, arguments);
};

(function() {
    var Dom = YAHOO.util.Dom;
    var Event = YAHOO.util.Event;
    var DDM = YAHOO.util.DragDropMgr;

    YAHOO.extend(DragDrop, YAHOO.util.DDProxy, {
        startDrag: function(x, y) {
            var el = this.getEl();

            this.resetConstraints();
            this.setXConstraint(0,0);    // D&D is for Y-axis only

            // set Y constraint to be within the container
            var totalHeight = el.parentNode.offsetHeight;
            var blockHeight = el.offsetHeight;
            this.setYConstraint(el.offsetTop, totalHeight-blockHeight-el.offsetTop);

            el.style.visibility = "hidden";

            this.goingUp = false;
            this.lastY = 0;
        },

        endDrag: function(e) {
            var srcEl = this.getEl();
            var proxy = this.getDragEl();

            // Show the proxy element and animate it to the src element's location
            Dom.setStyle(proxy, "visibility", "");
            var a = new YAHOO.util.Motion(
                proxy, {
                    points: {
                        to: Dom.getXY(srcEl)
                    }
                },
                0.2,
                YAHOO.util.Easing.easeOut
            )
            var proxyid = proxy.id;
            var thisid = this.id;

            // Hide the proxy and show the source element when finished with the animation
            a.onComplete.subscribe(function() {
                    Dom.setStyle(proxyid, "visibility", "hidden");
                    Dom.setStyle(thisid, "visibility", "");
                });
            a.animate();
        },

        onDrag: function(e) {

            // Keep track of the direction of the drag for use during onDragOver
            var y = Event.getPageY(e);

            if (y < this.lastY) {
                this.goingUp = true;
            } else if (y > this.lastY) {
                this.goingUp = false;
            }

            this.lastY = y;
        },

        onDragOver: function(e, id) {
            var srcEl = this.getEl();
            var destEl = Dom.get(id);

            // We are only concerned with list items, we ignore the dragover
            // notifications for the list.
2381 2382 2383
            if (destEl.nodeName == "DIV" && Dom.hasClass(destEl,"repeated-chunk")
                    // Nested lists.. ensure we don't drag out of this list or into a nested one:
                    && destEl.parentNode==srcEl.parentNode) {
2384 2385 2386 2387 2388 2389 2390 2391 2392
                var p = destEl.parentNode;

                // if going up, insert above the target element
                p.insertBefore(srcEl, this.goingUp?destEl:destEl.nextSibling);

                DDM.refreshCache();
            }
        }
    });
2393 2394
})();

2395 2396 2397 2398 2399 2400 2401 2402 2403 2404 2405 2406 2407 2408 2409 2410 2411 2412 2413 2414 2415 2416 2417 2418 2419 2420 2421 2422 2423 2424 2425 2426 2427 2428 2429 2430 2431
/**
 * Loads the script specified by the URL.
 *
 * @param href
 *      The URL of the script to load.
 * @param callback
 *      If specified, this function will be invoked after the script is loaded.
 * @see http://stackoverflow.com/questions/4845762/onload-handler-for-script-tag-in-internet-explorer
 */
function loadScript(href,callback) {
    var head = document.getElementsByTagName("head")[0] || document.documentElement;
    var script = document.createElement("script");
    script.src = href;

    if (callback) {
        // Handle Script loading
        var done = false;

        // Attach handlers for all browsers
        script.onload = script.onreadystatechange = function() {
            if ( !done && (!this.readyState ||
                    this.readyState === "loaded" || this.readyState === "complete") ) {
                done = true;
                callback();

                // Handle memory leak in IE
                script.onload = script.onreadystatechange = null;
                if ( head && script.parentNode ) {
                    head.removeChild( script );
                }
            }
        };
    }

    // Use insertBefore instead of appendChild  to circumvent an IE6 bug.
    // This arises when a base node is used (#2709 and #4378).
    head.insertBefore( script, head.firstChild );
2432 2433
}

K
kohsuke 已提交
2434 2435 2436 2437 2438 2439 2440 2441 2442
var downloadService = {
    continuations: {},

    download : function(id,url,info, postBack,completionHandler) {
        this.continuations[id] = {postBack:postBack,completionHandler:completionHandler};
        loadScript(url+"?"+Hash.toQueryString(info));
    },

    post : function(id,data) {
2443 2444 2445 2446 2447
        if (data==undefined) {
            // default to id in data
            data = id;
            id = data.id;
        }
K
kohsuke 已提交
2448
        var o = this.continuations[id];
2449
        // send the payload back in the body. We used to send this in as a form submission, but that hits the form size check in Jetty.
K
kohsuke 已提交
2450
        new Ajax.Request(o.postBack, {
2451 2452 2453
            contentType:"application/json",
            encoding:"UTF-8",
            postBody:Object.toJSON(data),
K
kohsuke 已提交
2454 2455 2456
            onSuccess: function() {
                if(o.completionHandler!=null)
                    o.completionHandler();
2457 2458
                else if(downloadService.completionHandler!=null)
                    downloadService.completionHandler();
K
kohsuke 已提交
2459 2460 2461 2462 2463
            }
        });
    }
};

2464 2465
// update center service. to remain compatible with earlier version of Hudson, aliased.
var updateCenter = downloadService;
2466 2467 2468 2469 2470 2471 2472 2473 2474 2475 2476 2477

/*
redirects to a page once the page is ready.

    @param url
        Specifies the URL to redirect the user.
*/
function applySafeRedirector(url) {
    var i=0;
    new PeriodicalExecuter(function() {
      i = (i+1)%4;
      var s = "";
K
kohsuke 已提交
2478 2479
      var j=0;
      for( j=0; j<i; j++ )
2480
        s+='.';
K
kohsuke 已提交
2481 2482 2483 2484 2485 2486
      // put the rest of dots as hidden so that the layout doesn't change
      // depending on the # of dots.
      s+="<span style='visibility:hidden'>";
      for( ; j<4; j++ )
        s+='.';
      s+="</span>";
2487 2488 2489 2490 2491 2492 2493 2494 2495 2496 2497 2498 2499 2500 2501 2502
      $('progress').innerHTML = s;
    },1);

    window.setTimeout(function() {
      var statusChecker = arguments.callee;
        new Ajax.Request(url, {
            method: "get",
            onFailure: function(rsp) {
                if(rsp.status==503) {
                  // redirect as long as we are still loading
                  window.setTimeout(statusChecker,5000);
                } else {
                  window.location.replace(url);
                }
            },
            onSuccess: function(rsp) {
2503 2504 2505 2506 2507 2508
                if(rsp.status!=200) {
                    // if connection fails, somehow Prototype thinks it's a success
                    window.setTimeout(statusChecker,5000);
                } else {
                    window.location.replace(url);
                }
2509 2510 2511
            }
        });
    }, 5000);
2512
}
K
kohsuke 已提交
2513 2514 2515 2516 2517 2518 2519 2520 2521

// logic behind <f:validateButton />
function validateButton(checkUrl,paramList,button) {
  button = button._button;

  var parameters = {};

  paramList.split(',').each(function(name) {
      var p = findPreviousFormItem(button,name);
2522 2523 2524 2525
      if(p!=null) {
        if(p.type=="checkbox")  parameters[name] = p.checked;
        else                    parameters[name] = p.value;
      }
K
kohsuke 已提交
2526 2527 2528 2529 2530 2531 2532 2533 2534 2535
  });

  var spinner = Element.up(button,"DIV").nextSibling;
  var target = spinner.nextSibling;
  spinner.style.display="block";

  new Ajax.Request(checkUrl, {
      parameters: parameters,
      onComplete: function(rsp) {
          spinner.style.display="none";
2536 2537 2538 2539 2540
          var i;
          target.innerHTML = rsp.status==200 ? rsp.responseText
                : '<a href="" onclick="document.getElementById(\'valerr' + (i=iota++)
                + '\').style.display=\'block\';return false">ERROR</a><div id="valerr'
                + i + '" style="display:none">' + rsp.responseText + '</div>';
2541
          Behaviour.applySubtree(target);
2542
          layoutUpdateCallback.call();
2543 2544 2545 2546 2547 2548 2549
          var s = rsp.getResponseHeader("script");
          if(s!=null)
            try {
              eval(s);
            } catch(e) {
              window.alert("failed to evaluate "+s+"\n"+e.message);
            }
K
kohsuke 已提交
2550 2551 2552
      }
  });
}
2553 2554

// create a combobox.
2555 2556 2557
// @param idOrField
//      ID of the <input type=text> element that becomes a combobox, or the field itself.
//      Passing an ID is @deprecated since 1.350; use <input class="combobox"/> instead.
2558 2559
// @param valueFunction
//      Function that returns all the candidates as an array
2560
function createComboBox(idOrField,valueFunction) {
2561
    var candidates = valueFunction();
2562 2563 2564 2565 2566
    var creator = function() {
        if (typeof idOrField == "string")
          idOrField = document.getElementById(idOrField);
        if (!idOrField) return;
        new ComboBox(idOrField, function(value /*, comboBox*/) {
2567 2568 2569 2570 2571 2572 2573 2574 2575 2576 2577 2578
          var items = new Array();
          if (value.length > 0) { // if no value, we'll not provide anything
            value = value.toLowerCase();
            for (var i = 0; i<candidates.length; i++) {
              if (candidates[i].toLowerCase().indexOf(value) >= 0) {
                items.push(candidates[i]);
                if(items.length>20)
                  break; // 20 items in the list should be enough
              }
            }
          }
          return items; // equiv to: comboBox.setItems(items);
2579 2580 2581 2582
        });
    };
    // If an ID given, create when page has loaded (backward compatibility); otherwise now.
    if (typeof idOrField == "string") Behaviour.addLoadEvent(creator); else creator();
2583
}
2584 2585


2586 2587 2588 2589
// Exception in code during the AJAX processing should be reported,
// so that our users can find them more easily.
Ajax.Request.prototype.dispatchException = function(e) {
    throw e;
2590
}
2591 2592 2593 2594 2595 2596 2597 2598 2599 2600 2601

// event callback when layouts/visibility are updated and elements might have moved around
var layoutUpdateCallback = {
    callbacks : [],
    add : function (f) {
        this.callbacks.push(f);
    },
    call : function() {
        for (var i = 0, length = this.callbacks.length; i < length; i++)
            this.callbacks[i]();
    }
2602 2603
}

2604 2605 2606 2607
// Notification bar
// ==============================
// this control displays a single line message at the top of the page, like StackOverflow does
// see ui-samples for more details
2608 2609 2610 2611 2612 2613
var notificationBar = {
    OPACITY : 0.8,
    DELAY : 3000,   // milliseconds to auto-close the notification
    div : null,     // the main 'notification-bar' DIV
    token : null,   // timer for cancelling auto-close

2614 2615 2616 2617 2618 2619 2620 2621 2622 2623 2624 2625
    OK : {// standard option values for typical OK notification
        icon: "accept.png",
        backgroundColor: "#8ae234"
    },
    WARNING : {// likewise, for warning
        icon: "yellow.png",
        backgroundColor: "#fce94f"
    },
    ERROR : {// likewise, for error
        icon: "red.png",
        backgroundColor: "#ef2929",
        sticky: true
K
Kohsuke Kawaguchi 已提交
2626 2627
    },

2628 2629 2630 2631 2632
    init : function() {
        if (this.div==null) {
            this.div = document.createElement("div");
            YAHOO.util.Dom.setStyle(this.div,"opacity",0);
            this.div.id="notification-bar";
2633
            this.div.style.backgroundColor="#fff";
2634 2635 2636 2637 2638 2639 2640 2641 2642 2643 2644 2645 2646 2647 2648 2649 2650
            document.body.insertBefore(this.div, document.body.firstChild);

            var self = this;
            this.div.onclick = function() {
                self.hide();
            };
        }
    },
    // cancel pending auto-hide timeout
    clearTimeout : function() {
        if (this.token)
            window.clearTimeout(this.token);
        this.token = null;
    },
    // hide the current notification bar, if it's displayed
    hide : function () {
        this.clearTimeout();
2651
        var self = this;
2652 2653 2654
        var out = new YAHOO.util.ColorAnim(this.div, {
            opacity: { to:0 },
            backgroundColor: {to:"#fff"}
2655 2656 2657 2658 2659
        }, 0.3, YAHOO.util.Easing.easeIn);
        out.onComplete.subscribe(function() {
            self.div.style.display = "none";
        })
        out.animate();
2660 2661 2662 2663 2664 2665 2666
    },
    // show a notification bar
    show : function (text,options) {
        options = options || {}

        this.init();
        this.div.style.height = this.div.style.lineHeight = options.height || "40px";
2667
        this.div.style.display = "block";
2668 2669 2670 2671 2672

        if (options.icon)
            text = "<img src='"+rootURL+"/images/24x24/"+options.icon+"'> "+text;
        this.div.innerHTML = text;

2673 2674 2675
        new YAHOO.util.ColorAnim(this.div, {
            opacity: { to:this.OPACITY },
            backgroundColor : { to: options.backgroundColor || "#fff" }
2676 2677 2678 2679
        }, 1, YAHOO.util.Easing.easeOut).animate();

        this.clearTimeout();
        var self = this;
2680 2681
        if (!options.sticky)
            this.token = window.setTimeout(function(){self.hide();},this.DELAY);
2682 2683
    }
};