Files
jenkins/war/resources/scripts/hudson-behavior.js
T
kohsuke 720be7d7a1 Merged revisions 23183,23185,23245,23256,23266,23279-23284,23286 via svnmerge from
https://www.dev.java.net/svn/hudson/branches/multiple-update-sources

........
  r23183 | abayer | 2009-10-25 11:53:17 -0700 (Sun, 25 Oct 2009) | 1 line
  
  First chunk of work towards supporting multiple update centers - infrastructure in place for it, and default update center treated as only one of multiple - have to use slightly modified update-center.json though
........
  r23185 | abayer | 2009-10-25 12:25:49 -0700 (Sun, 25 Oct 2009) | 1 line
  
  a number of tweaks - among other things, now works properly with additional update centers defined in hudson.model.UpdateCenter.xml - try using http://andrewbayer.com/images/hello-world/helloWorld-update-center.json as second update center
........
  r23245 | abayer | 2009-10-27 13:45:25 -0700 (Tue, 27 Oct 2009) | 1 line
  
  Modified to support existing default update-center.json
........
  r23256 | kohsuke | 2009-10-27 18:30:15 -0700 (Tue, 27 Oct 2009) | 7 lines
  
  - moved data binding of update-center.json to UpdateSource.
  - moved some of the UpdateSource property into JSON, to improve the user experience when adding an update source.
    the user should just type in one URL, and everything else should happen automatically.
  - it doesn't make sense for UpdateSource to rely on UpdateCenterConfiguration, since behaviors cannot be
    modified per UpdateSource basis. Instead, leaving it in UpdateCenter allows us to maintain backward compatible
    behaviors with the existing custom UpdateCenterConfiguration implementation.
........
  r23266 | kohsuke | 2009-10-27 19:29:24 -0700 (Tue, 27 Oct 2009) | 1 line
  
  serve id inside JSON.
........
  r23279 | kohsuke | 2009-10-28 10:44:00 -0700 (Wed, 28 Oct 2009) | 1 line
  
  Do not special-case "default" ID. Receive hudson.war updates from wherever that provides one.
........
  r23280 | kohsuke | 2009-10-28 10:50:57 -0700 (Wed, 28 Oct 2009) | 1 line
  
  formatting change.
........
  r23281 | kohsuke | 2009-10-28 11:30:40 -0700 (Wed, 28 Oct 2009) | 1 line
  
  typo
........
  r23282 | kohsuke | 2009-10-28 11:45:06 -0700 (Wed, 28 Oct 2009) | 2 lines
  
  - added the UI to remove sites.
  - persistence of UpdateSource happens more automatically now.
........
  r23283 | kohsuke | 2009-10-28 11:50:04 -0700 (Wed, 28 Oct 2009) | 1 line
  
  UpdateSource -> UpdateSite to align terminology with Eclipse.
........
  r23284 | kohsuke | 2009-10-28 11:55:03 -0700 (Wed, 28 Oct 2009) | 5 lines
  
  Actually, for now, just having a programmatic modification to UpdateSite would satisfy the primary use case.
  
  And I'd like to upgrade YUI to a more recent version so that we can use its DataTable for UI. Plus the single update center model encourages the community to bring the code into the Hudson project, which is something we'd like to keep.
  
  So all in all, I'm postponing the UI work.
........
  r23286 | kohsuke | 2009-10-28 13:07:19 -0700 (Wed, 28 Oct 2009) | 1 line
  
  Renaming and bug fixes.
........


git-svn-id: https://hudson.dev.java.net/svn/hudson/trunk/hudson/main@23326 71c3de6d-444a-0410-be80-ed276b4c234a
2009-10-29 21:27:46 +00:00

1677 lines
54 KiB
JavaScript

/*
* The MIT License
*
* Copyright (c) 2004-2009, Sun Microsystems, Inc., Kohsuke Kawaguchi, Daniel Dyer, Yahoo! Inc.
*
* 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.
*/
//
//
// JavaScript for Hudson
// See http://www.ibm.com/developerworks/web/library/wa-memleak/?ca=dgr-lnxw97JavascriptLeaks
// for memory leak patterns and how to prevent them.
//
// create a new object whose prototype is the given object
function object(o) {
function F() {}
F.prototype = o;
return new F();
}
// id generator
var iota = 0;
// crumb information
var crumb = {
fieldName: null,
value: null,
init: function(crumbField, crumbValue) {
this.fieldName = crumbField;
this.value = crumbValue;
},
/**
* Adds the crumb value into the given hash and returns the hash.
*/
wrap: function(hash) {
if(this.fieldName!=null)
hash[this.fieldName]=this.value;
return hash;
},
/**
* 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);
}
}
// Form check code
//========================================================
var FormChecker = {
// pending requests
queue : [],
// 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,
/**
* 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.
*/
delayedCheck : function(url, method, target) {
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
this.queue.push({url:url, method:method, target:target});
this.schedule();
},
sendRequest : function(url, params) {
if (params.method == "post") {
var idx = url.indexOf('?');
params.parameters = url.substring(idx + 1);
url = url.substring(0, idx);
}
new Ajax.Request(url, params);
},
schedule : function() {
if (this.inProgress>0) return;
if (this.queue.length == 0) return;
var next = this.queue.shift();
this.sendRequest(next.url, {
method : next.method,
onComplete : function(x) {
next.target.innerHTML = x.responseText;
FormChecker.inProgress--;
FormChecker.schedule();
}
});
this.inProgress++;
}
}
function toValue(e) {
// compute the form validation value to be sent to the server
var type = e.getAttribute("type");
if(type!=null && type.toLowerCase()=="checkbox")
return e.checked;
return encodeURIComponent(e.value);
}
// find the nearest ancestor node that has the given tag name
function findAncestor(e, tagName) {
do {
e = e.parentNode;
} while (e != null && e.tagName != tagName);
return e;
}
function findFollowingTR(input, className) {
// identify the parent TR
var tr = input;
while (tr.tagName != "TR")
tr = tr.parentNode;
// then next TR that matches the CSS
do {
tr = tr.nextSibling;
} while (tr.tagName != "TR" || tr.className != className);
return tr;
}
/**
* 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) {
function prev(e) {
var p = e.previousSibling;
if(p==null) return e.parentNode;
while(p.lastChild!=null)
p = p.lastChild;
return p;
}
while(src!=null) {
src = prev(src);
if(src==null) break;
if(filter(src))
return src;
}
return null;
}
/**
* Traverses a form in the reverse document order and finds an INPUT element that matches the given name.
*/
function findPreviousFormItem(src,name) {
var name2 = "_."+name; // handles <textbox field="..." /> notation silently
return findPrevious(src,function(e){ return (e.tagName=="INPUT" || e.tagName=="TEXTAREA") && (e.name==name || e.name==name2); });
}
// shared tooltip object
var tooltip;
// Behavior rules
//========================================================
// using tag names in CSS selector makes the processing faster
function registerValidator(e) {
e.targetElement = findFollowingTR(e, "validation-error-area").firstChild.nextSibling;
e.targetUrl = function() {
return eval(this.getAttribute("checkUrl"));
};
var method = e.getAttribute("checkMethod");
if (!method) method = "get";
FormChecker.delayedCheck(e.targetUrl(), method, e.targetElement);
var checker = function() {
var target = this.targetElement;
FormChecker.sendRequest(this.targetUrl(), {
method : method,
onComplete : function(x) {
target.innerHTML = x.responseText;
}
});
}
var oldOnchange = e.onchange;
if(typeof oldOnchange=="function") {
e.onchange = function() { checker.call(this); oldOnchange.call(this); }
} else
e.onchange = checker;
e.onblur = checker;
e = null; // avoid memory leak
}
function registerRegexpValidator(e,regexp,message) {
e.targetElement = findFollowingTR(e, "validation-error-area").firstChild.nextSibling;
var checkMessage = e.getAttribute('checkMessage');
if (checkMessage) message = checkMessage;
var oldOnchange = e.onchange;
e.onchange = function() {
var set = oldOnchange != null ? oldOnchange.call(this) : false;
if (this.value.match(regexp)) {
if (!set) this.targetElement.innerHTML = "";
} else {
this.targetElement.innerHTML = "<div class=error>" + message + "</div>";
set = true;
}
return set;
}
e.onchange.call(e);
e = null; // avoid memory leak
}
/**
* Wraps a <button> into YUI button.
*
* @param e
* button element
* @param onclick
* onclick handler
*/
function makeButton(e,onclick) {
var h = e.onclick;
var clsName = e.className;
var n = e.name;
var btn = new YAHOO.widget.Button(e,{});
if(onclick!=null)
btn.addListener("click",onclick);
if(h!=null)
btn.addListener("click",h);
var be = btn.get("element");
Element.addClassName(be,clsName);
if(n!=null) // copy the name
be.setAttribute("name",n);
return btn;
}
/*
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");});
}
var hudsonRules = {
"BODY" : function() {
tooltip = new YAHOO.widget.Tooltip("tt", {context:[], zindex:999});
},
// 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) {
if(isInsideRemovable(e)) return;
// components for the add button
var menu = document.createElement("SELECT");
var btns = findElementsBySelector(e,"INPUT.hetero-list-add"),
btn = btns[btns.length-1]; // In case nested content also uses hetero-list
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");
var tooltip = n.getAttribute("tooltip");
menu.options[i] = new Option(n.getAttribute("title"),""+i);
templates.push({html:n.innerHTML, name:name, tooltip:tooltip});
}
Element.remove(prototypes);
// D&D support
function prepareDD(e) {
var dd = new DragDrop(e);
var h = e;
// locate a handle
while(!Element.hasClassName(h,"dd-handle"))
h = h.firstChild;
dd.setHandleElId(h);
}
var withDragDrop = Element.hasClassName(e,"with-drag-drop");
if(withDragDrop) {
for(e=e.firstChild; e!=null; e=e.nextSibling) {
if(Element.hasClassName(e,"repeated-chunk"))
prepareDD(e);
}
}
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;
insertionPoint.parentNode.insertBefore(nc, insertionPoint);
if(withDragDrop) prepareDD(nc);
Behaviour.applySubtree(nc);
});
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);
}
});
},
"DIV.repeated-container" : function(e) {
if(isInsideRemovable(e)) return;
// 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);
},
"TABLE.sortable" : function(e) {// sortable table
ts_makeSortable(e);
},
"TABLE.progress-bar" : function(e) {// sortable table
e.onclick = function() {
var href = this.getAttribute("href");
if(href!=null) window.location = href;
}
e = null; // avoid memory leak
},
"INPUT.advancedButton" : function(e) {
makeButton(e,function(e) {
var link = e.target;
while(!Element.hasClassName(link,"advancedLink"))
link = link.parentNode;
link.style.display = "none"; // hide the button
var container = link.nextSibling.firstChild; // TABLE -> TBODY
var tr = link;
while (tr.tagName != "TR")
tr = tr.parentNode;
// move the contents of the advanced portion into the main table
var nameRef = tr.getAttribute("nameref");
while (container.lastChild != null) {
var row = container.lastChild;
if(nameRef!=null && row.getAttribute("nameref")==null)
row.setAttribute("nameref",nameRef); // to handle inner rowSets, don't override existing values
tr.parentNode.insertBefore(row, tr.nextSibling);
}
});
e = null; // avoid memory leak
},
"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
},
// 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 = "";
Element.removeClassName(this, "defaulted");
}
}
e.onblur = function() {
if (this.value == "") {
this.value = defaultValue;
Element.addClassName(this, "defaulted");
}
}
e = null; // avoid memory leak
},
// <label> that doesn't use ID, so that it can be copied in <repeatable>
"LABEL.attach-previous" : function(e) {
e.onclick = function() {
var e = this;
while(e.tagName!="INPUT")
e=e.previousSibling;
e.click();
}
e = null;
},
// 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,
// validate required form values
"INPUT.required" : function(e) { registerRegexpValidator(e,/./,"Field is required"); },
// validate form values to be a number
"INPUT.number" : function(e) { registerRegexpValidator(e,/^(\d+|)$/,"Not a number"); },
"INPUT.positive-number" : function(e) {
registerRegexpValidator(e,/^(\d*[1-9]\d*|)$/,"Not a positive number");
},
"A.help-button" : function(e) {
e.onclick = function() {
var tr = findFollowingTR(this, "help-area");
var div = tr.firstChild.nextSibling.firstChild;
if (div.style.display != "block") {
div.style.display = "block";
// make it visible
new Ajax.Request(this.getAttribute("helpURL"), {
method : 'get',
onComplete : function(x) {
div.innerHTML = x.responseText;
}
});
} else {
div.style.display = "none";
}
return false;
}
e = null; // avoid memory leak
},
// 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',
onSuccess : function(x) {
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);
}
});
},
// button to add a new repeatable block
"INPUT.repeatable-add" : function(e) {
makeButton(e,function(e) {
repeatableSupport.onAdd(e.target);
});
e = null; // avoid memory leak
},
"INPUT.repeatable-delete" : function(e) {
makeButton(e,function(e) {
repeatableSupport.onDelete(e.target);
});
e = null; // avoid memory leak
},
// resizable text area
"TEXTAREA" : function(textarea) {
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;
}
var handle = textarea.nextSibling;
if(handle==null || handle.className!="textarea-handle") return;
var Event = YAHOO.util.Event;
handle.onmousedown = function(ev) {
ev = Event.getEvent(ev);
var offset = textarea.offsetHeight-Event.getPageY(ev);
textarea.style.opacity = 0.5;
document.onmousemove = function(ev) {
ev = Event.getEvent(ev);
function max(a,b) { if(a<b) return b; else return a; }
textarea.style.height = max(32, offset + Event.getPageY(ev)) + 'px';
return false;
};
document.onmouseup = function() {
document.onmousemove = null;
document.onmouseup = null;
textarea.style.opacity = 1;
}
};
handle.ondblclick = function() {
textarea.style.height = "";
textarea.rows = textarea.value.split("\n").length;
}
},
// structured form submission
"FORM" : function(form) {
crumb.appendToForm(form);
if(Element.hasClassName("no-json"))
return;
// add the hidden 'json' input field, which receives the form structure in JSON
var div = document.createElement("div");
div.innerHTML = "<input type=hidden name=json value=init>";
form.appendChild(div);
form.onsubmit = function() { buildFormTree(this); };
form = null; // memory leak prevention
},
// hook up tooltip.
// add nodismiss="" if you'd like to display the tooltip forever as long as the mouse is on the element.
"[tooltip]" : function(e) {
applyTooltip(e,e.getAttribute("tooltip"));
},
"INPUT.submit-button" : function(e) {
makeButton(e);
},
"INPUT.yui-button" : function(e) {
makeButton(e);
},
"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++));
},
"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;
var ref = start.getAttribute("ref");
if(ref==null)
start.id = ref = "rowSetStart"+(iota++);
applyNameRef(start,end,ref);
},
"BODY TR.optional-block-start": function(e) { // see optionalBlock.jelly
// this is prefixed by a pointless BODY so that two processing for optional-block-start
// can sandwitch row-set-end
// this requires "TR.row-set-end" to mark rows
var checkbox = e.firstChild.firstChild;
updateOptionalBlock(checkbox,false);
},
// 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
}
};
function applyTooltip(e,text) {
// copied from YAHOO.widget.Tooltip.prototype.configContext to efficiently add a new element
// event registration via YAHOO.util.Event.addListener leaks memory, so do it by ourselves here
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);
}
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); }
e.title = text;
e = null; // avoid memory leak
}
Behaviour.register(hudsonRules);
function xor(a,b) {
// convert both values to boolean by '!' and then do a!=b
return !a != !b;
}
// used by editableDescription.jelly to replace the description field with a form
function replaceDescription() {
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;
Behaviour.applySubtree(d);
d.getElementsByTagName("TEXTAREA")[0].focus();
}
}
);
return false;
}
function applyNameRef(s,e,id) {
$(id).groupingNode = true;
// s contains the node itself
for(var x=s.nextSibling; x!=e; x=x.nextSibling) {
// to handle nested <f:rowSet> correctly, don't overwrite the existing value
if(x.getAttribute("nameRef")==null)
x.setAttribute("nameRef",id);
}
}
// used by optionalBlock.jelly to update the form status
// @param c checkbox element
function updateOptionalBlock(c,scroll) {
// find the start TR
var s = c;
while(!Element.hasClassName(s, "optional-block-start"))
s = s.parentNode;
var tbl = s.parentNode;
var i = false;
var o = false;
var checked = xor(c.checked,Element.hasClassName(c,"negative"));
var lastRow = null;
for (var j = 0; tbl.rows[j]; j++) {
var n = tbl.rows[j];
if (i && Element.hasClassName(n, "optional-block-end"))
o = true;
if (i && !o) {
if (checked) {
n.style.display = "";
lastRow = n;
} else
n.style.display = "none";
}
if (n==s) {
if (n.getAttribute('hasHelp') == 'true')
j++;
i = true;
}
}
if(checked && scroll) {
var D = YAHOO.util.Dom;
var r = D.getRegion(s);
if(lastRow!=null) r = r.union(D.getRegion(lastRow));
scrollIntoView(r);
}
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' : '';
}
}
}
}
//
// 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
if (objDiv.offsetHeight > 0)
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();
var diff = currentHeight - scrollDiv.scrollTop - height;
// window.alert("currentHeight=" + currentHeight + ",scrollTop=" + scrollDiv.scrollTop + ",height=" + height);
return diff < this.bottomThreshold;
},
scrollToBottom : function() {
var scrollDiv = $(this.scrollContainer);
scrollDiv.scrollTop = this.getCurrentHeight();
}
};
}
// 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);
}
// used in expandableTextbox.jelly to change a input field into a text area
function expandTextArea(button,id) {
button.style.display="none";
var field = document.getElementById(id);
var value = field.value.replace(/ +/g,'\n');
var n = field;
while(n.tagName!="TABLE")
n = n.parentNode;
n.parentNode.innerHTML =
"<textarea rows=8 class='setting-input' name='"+field.name+"'>"+value+"</textarea>";
}
// refresh a part of the HTML specified by the given ID,
// by using the contents fetched from the given URL.
function refreshPart(id,url) {
var f = function() {
new Ajax.Request(url, {
onSuccess: function(rsp) {
var hist = $(id);
var p = hist.parentNode;
var next = hist.nextSibling;
p.removeChild(hist);
var div = document.createElement('div');
div.innerHTML = rsp.responseText;
var node = div.firstChild;
p.insertBefore(node, next);
Behaviour.applySubtree(node);
if(isRunAsTest) return;
refreshPart(id,url);
}
});
};
// 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);
}
/*
Perform URL encode.
Taken from http://www.cresc.co.jp/tech/java/URLencoding/JavaScript_URLEncoding.htm
@deprecated Use standard javascript method "encodeURIComponent" instead
*/
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;
}
// 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
}
// used witih <dropdownList> and <dropdownListBlock> to control visibility
function updateDropDownList(sel) {
for (var i = 0; i < sel.subForms.length; i++) {
var show = sel.selectedIndex == i;
var f = sel.subForms[i];
var td = f.start;
while (true) {
td.style.display = (show ? "" : "none");
if(show)
td.removeAttribute("field-disabled");
else // buildFormData uses this attribute and ignores the contents
td.setAttribute("field-disabled","true");
if (td == f.end) break;
td = td.nextSibling;
}
}
}
// code for supporting repeatable.jelly
var repeatableSupport = {
// set by the inherited instance to the insertion point DIV
insertionPoint: null,
// HTML text of the repeated chunk
blockHTML: null,
// containing <div>.
container: null,
// block name for structured HTML
name : null,
// 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);
this.name = master.getAttribute("name");
this.update();
},
// 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";
nc.setAttribute("name",this.name);
nc.innerHTML = this.blockHTML;
this.insertionPoint.parentNode.insertBefore(nc, this.insertionPoint);
Behaviour.applySubtree(nc);
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);
if(children.length==0) {
// noop
} else
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) {
while (!Element.hasClassName(n,"repeated-chunk"))
n = n.parentNode;
var p = n.parentNode;
p.removeChild(n);
p.tag.update();
},
// called when 'add' button is clicked
onAdd : function(n) {
while(n.tag==null)
n = n.parentNode;
n.tag.expand();
// 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);
}
}
}
};
// Used by radioBlock.jelly to wire up expandable radio block
function addRadioBlock(id) {
// prototype object to be duplicated for each radio button group
var radioBlockSupport = {
buttons : null,
updateButtons : function() {
for( var i=0; i<this.buttons.length; i++ )
this.buttons[i]();
},
// update one block based on the status of the given radio button
updateSingleButton : function(radio, blockStart, blockEnd) {
var tbl = blockStart.parentNode;
var i = false;
var o = false;
var show = radio.checked;
for (var j = 0; tbl.rows[j]; j++) {
var n = tbl.rows[j];
if (n == blockEnd)
o = true;
if (i && !o) {
if (show)
n.style.display = "";
else
n.style.display = "none";
}
if (n == blockStart) {
i = true;
if (n.getAttribute('hasHelp') == 'true')
j++;
}
}
}
};
// 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 r = document.getElementById('Rb' + id);
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 = document.getElementById("rb_s"+id);
var e = document.getElementById("rb_e"+id);
var u = function() {
g.updateSingleButton(r,s,e);
};
applyNameRef(s,e,'Rb'+id);
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(); };
}
function updateBuildHistory(ajaxUrl,nBuild) {
if(isRunAsTest) return;
$('buildHistory').headers = ["n",nBuild];
function updateBuilds() {
var bh = $('buildHistory');
new Ajax.Request(ajaxUrl, {
requestHeaders: bh.headers,
onSuccess: function(rsp) {
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;
Behaviour.applySubtree(div);
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);
}
// send async request to the given URL (which will send back serialized ListBoxModel object),
// then use the result to fill the list box.
function updateListBox(listBox,url) {
new Ajax.Request(url, {
onSuccess: function(rsp) {
var l = $(listBox);
while(l.length>0) l.options[0] = null;
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);
if(opts[i].selected)
l.selectedIndex = i;
}
},
onFailure: function(rsp) {
var l = $(listBox);
l.options[0] = null;
}
});
}
// 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)
return e.currentStyle[a];
return null;
};
// set up logic behind the search box
function createSearchBox(searchURL) {
var ds = new YAHOO.widget.DS_XHR(searchURL+"suggest",["suggestions","name"]);
ds.queryMatchCase = false;
var ac = new YAHOO.widget.AutoComplete("search-box","search-box-completion",ds);
ac.typeAhead = false;
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;
}
//
// structured form submission handling
// see http://hudson.gotdns.com/wiki/display/HUDSON/Structured+Form+Submission
function buildFormTree(form) {
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);
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;
}
function addProperty(parent,name,value) {
name = shortenName(name);
if(parent[name]!=null) {
if(parent[name].push==null) // is this array?
parent[name] = [ parent[name] ];
parent[name].push(value);
} else {
parent[name] = value;
}
}
// find the grouping parent node, which will have @name.
// then return the corresponding object in the map
function findParent(e) {
while(e!=form) {
e = e.parentNode;
// 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);
if(e.getAttribute("field-disabled")!=null)
return {}; // this field shouldn't contribute to the final result
var name = e.getAttribute("name");
if(name!=null) {
if(e.tagName=="INPUT" && !xor(e.checked,Element.hasClassName(e,"negative")))
return {}; // field is not active
var m = e.formDom;
if(m==null) {
// this is a new grouping node
doms.push(e);
e.formDom = m = {};
addProperty(findParent(e), name, m);
}
return m;
}
}
return form.formDom; // guaranteed non-null
}
var jsonElement = null;
for( var i=0; i<form.elements.length; i++ ) {
var e = form.elements[i];
if(e.name=="json") {
jsonElement = e;
continue;
}
if(e.tagName=="FIELDSET")
continue;
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;
}
var p;
var type = e.getAttribute("type");
if(type==null) type="";
switch(type.toLowerCase()) {
case "button":
case "submit":
break;
case "checkbox":
p = findParent(e);
var checked = xor(e.checked,Element.hasClassName(e,"negative"));
if(!e.groupingNode)
addProperty(p, e.name, checked);
else {
if(checked)
addProperty(p, e.name, e.formDom = {});
}
break;
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
// @enctype is the standard, but IE needs @encoding.
form.enctype = form.encoding = "multipart/form-data";
break;
case "radio":
if(!e.checked) break;
if(e.groupingNode) {
p = findParent(e);
addProperty(p, e.name, e.formDom = { value: e.value });
break;
}
// otherwise fall through
default:
p = findParent(e);
addProperty(p, e.name, e.value);
break;
}
}
jsonElement.value = Object.toJSON(form.formDom);
// clean up
for( i=0; i<doms.length; i++ )
doms[i].formDom = null;
return jsonElement.value;
} catch(e) {
alert(e);
}
}
// 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() {
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;
}
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();
};
})();
/*
D&D implementation for heterogeneous list.
*/
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.
if (destEl.nodeName == "DIV" && Dom.hasClass(destEl,"repeated-chunk")) {
var p = destEl.parentNode;
// if going up, insert above the target element
p.insertBefore(srcEl, this.goingUp?destEl:destEl.nextSibling);
DDM.refreshCache();
}
}
});
})();
function loadScript(href) {
var s = document.createElement("script");
s.setAttribute("src",href);
document.getElementsByTagName("HEAD")[0].appendChild(s);
}
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) {
if (data==undefined) {
// default to id in data
data = id;
id = data.id;
}
var o = this.continuations[id];
new Ajax.Request(o.postBack, {
parameters:{json:Object.toJSON(data)},
onSuccess: function() {
if(o.completionHandler!=null)
o.completionHandler();
}
});
}
};
// update center service. to remain compatible with earlier version of Hudson, aliased.
var updateCenter = downloadService;
/*
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 = "";
for( var j=0; j<i; j++ )
s+='.';
$('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) {
if(rsp.status!=200) {
// if connection fails, somehow Prototype thinks it's a success
window.setTimeout(statusChecker,5000);
} else {
window.location.replace(url);
}
}
});
}, 5000);
}
// logic behind <f:validateButton />
function validateButton(checkUrl,paramList,button) {
button = button._button;
var parameters = {};
paramList.split(',').each(function(name) {
var p = findPreviousFormItem(button,name);
if(p!=null) {
if(p.type=="checkbox") parameters[name] = p.checked;
else parameters[name] = p.value;
}
});
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";
target.innerHTML = rsp.responseText;
var s = rsp.getResponseHeader("script");
if(s!=null)
try {
eval(s);
} catch(e) {
window.alert("failed to evaluate "+s+"\n"+e.message);
}
}
});
}
// create a combobox.
// @param id
// ID of the <input type=text> element that becomes a combobox.
// @param valueFunction
// Function that returns all the candidates as an array
function createComboBox(id,valueFunction) {
var candidates = valueFunction();
Behaviour.addLoadEvent(function() {
var callback = function(value /*, comboBox*/) {
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);
};
if (document.getElementById(id) != null) {
new ComboBox(id,callback);
}
});
}