PageRenderTime 64ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 0ms

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

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