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

http://github.com/jenkinsci/jenkins · JavaScript · 2576 lines · 1851 code · 309 blank · 416 comment · 433 complexity · e53387816c49d792b597d3f308933e57 MD5 · raw file

  1. /*
  2. * The MIT License
  3. *
  4. * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi,
  5. * Daniel Dyer, Yahoo! Inc., Alan Harder, InfraDNA, Inc.
  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 Jenkins
  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. if (window.isRunAsTest) {
  32. // Disable postMessage when running in test mode (HtmlUnit).
  33. window.postMessage = false;
  34. }
  35. // create a new object whose prototype is the given object
  36. function object(o) {
  37. function F() {}
  38. F.prototype = o;
  39. return new F();
  40. }
  41. function TryEach(fn) {
  42. return function(name) {
  43. try {
  44. fn(name);
  45. } catch (e) {
  46. console.error(e);
  47. }
  48. }
  49. }
  50. /**
  51. * A function that returns false if the page is known to be invisible.
  52. */
  53. var isPageVisible = (function(){
  54. // @see https://developer.mozilla.org/en/DOM/Using_the_Page_Visibility_API
  55. // Set the name of the hidden property and the change event for visibility
  56. var hidden, visibilityChange;
  57. if (typeof document.hidden !== "undefined") {
  58. hidden = "hidden";
  59. visibilityChange = "visibilitychange";
  60. } else if (typeof document.mozHidden !== "undefined") {
  61. hidden = "mozHidden";
  62. visibilityChange = "mozvisibilitychange";
  63. } else if (typeof document.msHidden !== "undefined") {
  64. hidden = "msHidden";
  65. visibilityChange = "msvisibilitychange";
  66. } else if (typeof document.webkitHidden !== "undefined") {
  67. hidden = "webkitHidden";
  68. visibilityChange = "webkitvisibilitychange";
  69. }
  70. // By default, visibility set to true
  71. var pageIsVisible = true;
  72. // If the page is hidden, prevent any polling
  73. // if the page is shown, restore pollings
  74. function onVisibilityChange() {
  75. pageIsVisible = !document[hidden];
  76. }
  77. // Warn if the browser doesn't support addEventListener or the Page Visibility API
  78. if (typeof document.addEventListener !== "undefined" && typeof hidden !== "undefined") {
  79. // Init the value to the real state of the page
  80. pageIsVisible = !document[hidden];
  81. // Handle page visibility change
  82. document.addEventListener(visibilityChange, onVisibilityChange, false);
  83. }
  84. return function() {
  85. return pageIsVisible;
  86. }
  87. })();
  88. // id generator
  89. var iota = 0;
  90. // crumb information
  91. var crumb = {
  92. fieldName: null,
  93. value: null,
  94. init: function(crumbField, crumbValue) {
  95. if (crumbField=="") return; // layout.jelly passes in "" whereas it means null.
  96. this.fieldName = crumbField;
  97. this.value = crumbValue;
  98. },
  99. /**
  100. * Adds the crumb value into the given hash or array and returns it.
  101. */
  102. wrap: function(headers) {
  103. if (this.fieldName!=null) {
  104. if (headers instanceof Array)
  105. // TODO prototype.js only seems to interpret object
  106. headers.push(this.fieldName, this.value);
  107. else
  108. headers[this.fieldName]=this.value;
  109. }
  110. // TODO return value unused
  111. return headers;
  112. },
  113. /**
  114. * Puts a hidden input field to the form so that the form submission will have the crumb value
  115. */
  116. appendToForm : function(form) {
  117. if(this.fieldName==null) return; // noop
  118. var div = document.createElement("div");
  119. div.innerHTML = "<input type=hidden name='"+this.fieldName+"' value='"+this.value+"'>";
  120. form.appendChild(div);
  121. if (form.enctype == "multipart/form-data") {
  122. if (form.action.indexOf("?") != -1) {
  123. form.action = form.action+"&"+this.fieldName+"="+this.value;
  124. } else {
  125. form.action = form.action+"?"+this.fieldName+"="+this.value;
  126. }
  127. }
  128. }
  129. };
  130. (function initializeCrumb() {
  131. var extensionsAvailable = document.head.getAttribute('data-extensions-available');
  132. if (extensionsAvailable === 'true') {
  133. var crumbHeaderName = document.head.getAttribute('data-crumb-header');
  134. var crumbValue = document.head.getAttribute('data-crumb-value');
  135. if (crumbHeaderName && crumbValue) {
  136. crumb.init(crumbHeaderName, crumbValue);
  137. }
  138. }
  139. // else, the instance is starting, restarting, etc.
  140. })();
  141. var isRunAsTest = undefined;
  142. // Be careful, this variable does not include the absolute root URL as in Java part of Jenkins,
  143. // but the contextPath only, like /jenkins
  144. var rootURL = 'not-defined-yet';
  145. var resURL = 'not-defined-yet';
  146. (function initializeUnitTestAndURLs() {
  147. var dataUnitTest = document.head.getAttribute('data-unit-test');
  148. if (dataUnitTest !== null) {
  149. isRunAsTest = dataUnitTest === 'true';
  150. }
  151. var dataRootURL = document.head.getAttribute('data-rooturl');
  152. if (dataRootURL !== null) {
  153. rootURL = dataRootURL;
  154. }
  155. var dataResURL = document.head.getAttribute('data-resurl');
  156. if (dataResURL !== null) {
  157. resURL = dataResURL;
  158. }
  159. })();
  160. (function initializeYUIDebugLogReader(){
  161. Behaviour.addLoadEvent(function(){
  162. var logReaderElement = document.getElementById('yui-logreader');
  163. if (logReaderElement !== null) {
  164. var logReader = new YAHOO.widget.LogReader('yui-logreader');
  165. logReader.collapse();
  166. }
  167. });
  168. })();
  169. // Form check code
  170. //========================================================
  171. var FormChecker = {
  172. // pending requests
  173. queue : [],
  174. // conceptually boolean, but doing so create concurrency problem.
  175. // that is, during unit tests, the AJAX.send works synchronously, so
  176. // the onComplete happens before the send method returns. On a real environment,
  177. // more likely it's the other way around. So setting a boolean flag to true or false
  178. // won't work.
  179. inProgress : 0,
  180. /**
  181. * Schedules a form field check. Executions are serialized to reduce the bandwidth impact.
  182. *
  183. * @param url
  184. * Remote doXYZ URL that performs the check. Query string should include the field value.
  185. * @param method
  186. * HTTP method. GET or POST. I haven't confirmed specifics, but some browsers seem to cache GET requests.
  187. * @param target
  188. * HTML element whose innerHTML will be overwritten when the check is completed.
  189. */
  190. delayedCheck : function(url, method, target) {
  191. if(url==null || method==null || target==null)
  192. return; // don't know whether we should throw an exception or ignore this. some broken plugins have illegal parameters
  193. this.queue.push({url:url, method:method, target:target});
  194. this.schedule();
  195. },
  196. sendRequest : function(url, params) {
  197. if (params.method != "get") {
  198. var idx = url.indexOf('?');
  199. params.parameters = url.substring(idx + 1);
  200. url = url.substring(0, idx);
  201. }
  202. new Ajax.Request(url, params);
  203. },
  204. schedule : function() {
  205. if (this.inProgress>0) return;
  206. if (this.queue.length == 0) return;
  207. var next = this.queue.shift();
  208. this.sendRequest(next.url, {
  209. method : next.method,
  210. onComplete : function(x) {
  211. applyErrorMessage(next.target, x);
  212. FormChecker.inProgress--;
  213. FormChecker.schedule();
  214. layoutUpdateCallback.call();
  215. }
  216. });
  217. this.inProgress++;
  218. }
  219. }
  220. /**
  221. * Find the sibling (in the sense of the structured form submission) form item of the given name,
  222. * and returns that DOM node.
  223. *
  224. * @param {HTMLElement} e
  225. * @param {string} name
  226. * Name of the control to find. Can include "../../" etc in the prefix.
  227. * See @RelativePath.
  228. *
  229. * We assume that the name is normalized and doesn't contain any redundant component.
  230. * That is, ".." can only appear as prefix, and "foo/../bar" is not OK (because it can be reduced to "bar")
  231. */
  232. function findNearBy(e,name) {
  233. while (name.startsWith("../")) {
  234. name = name.substring(3);
  235. e = findFormParent(e,null,true);
  236. }
  237. // name="foo/bar/zot" -> prefixes=["bar","foo"] & name="zot"
  238. var prefixes = name.split("/");
  239. name = prefixes.pop();
  240. prefixes = prefixes.reverse();
  241. // does 'e' itself match the criteria?
  242. // as some plugins use the field name as a parameter value, instead of 'value'
  243. var p = findFormItem(e,name,function(e,filter) {
  244. return filter(e) ? e : null;
  245. });
  246. if (p!=null && prefixes.length==0) return p;
  247. var owner = findFormParent(e,null,true);
  248. function locate(iterator,e) {// keep finding elements until we find the good match
  249. while (true) {
  250. e = iterator(e,name);
  251. if (e==null) return null;
  252. // make sure this candidate element 'e' is in the right point in the hierarchy
  253. var p = e;
  254. for (var i=0; i<prefixes.length; i++) {
  255. p = findFormParent(p,null,true);
  256. if (p.getAttribute("name")!=prefixes[i])
  257. return null;
  258. }
  259. if (findFormParent(p,null,true)==owner)
  260. return e;
  261. }
  262. }
  263. return locate(findPreviousFormItem,e) || locate(findNextFormItem,e);
  264. }
  265. function controlValue(e) {
  266. if (e==null) return null;
  267. // compute the form validation value to be sent to the server
  268. var type = e.getAttribute("type");
  269. if(type!=null && type.toLowerCase()=="checkbox")
  270. return e.checked;
  271. return e.value;
  272. }
  273. function toValue(e) {
  274. return encodeURIComponent(controlValue(e));
  275. }
  276. /**
  277. * Builds a query string in a fluent API pattern.
  278. * @param {HTMLElement} owner
  279. * The 'this' control.
  280. */
  281. function qs(owner) {
  282. return {
  283. params : "",
  284. append : function(s) {
  285. if (this.params.length==0) this.params+='?';
  286. else this.params+='&';
  287. this.params += s;
  288. return this;
  289. },
  290. nearBy : function(name) {
  291. var e = findNearBy(owner,name);
  292. if (e==null) return this; // skip
  293. return this.append(Path.tail(name)+'='+toValue(e));
  294. },
  295. addThis : function() {
  296. return this.append("value="+toValue(owner));
  297. },
  298. toString : function() {
  299. return this.params;
  300. }
  301. };
  302. }
  303. // find the nearest ancestor node that has the given tag name
  304. function findAncestor(e, tagName) {
  305. do {
  306. e = e.parentNode;
  307. } while (e != null && e.tagName != tagName);
  308. return e;
  309. }
  310. function findAncestorClass(e, cssClass) {
  311. do {
  312. e = e.parentNode;
  313. } while (e != null && !Element.hasClassName(e,cssClass));
  314. return e;
  315. }
  316. function isTR(tr, nodeClass) {
  317. return tr.tagName == 'TR' || tr.classList.contains(nodeClass || 'tr') || tr.classList.contains('jenkins-form-item');
  318. }
  319. function findFollowingTR(node, className, nodeClass) {
  320. // identify the parent TR
  321. var tr = node;
  322. while (!isTR(tr, nodeClass)) {
  323. tr = tr.parentNode;
  324. if (!(tr instanceof Element))
  325. return null;
  326. }
  327. // then next TR that matches the CSS
  328. do {
  329. // Supports plugins with custom variants of <f:entry> that call
  330. // findFollowingTR(element, 'validation-error-area') and haven't migrated
  331. // to use querySelector
  332. if (className === 'validation-error-area' || className === 'help-area') {
  333. var queryChildren = tr.getElementsByClassName(className);
  334. if (queryChildren.length > 0 && (isTR(queryChildren[0]) || Element.hasClassName(queryChildren[0], className) ))
  335. return queryChildren[0];
  336. }
  337. tr = $(tr).next();
  338. } while (tr != null && (!isTR(tr) || !Element.hasClassName(tr,className)));
  339. return tr;
  340. }
  341. function findInFollowingTR(input, className) {
  342. var node = findFollowingTR(input, className);
  343. if (node.tagName == 'TR') {
  344. node = node.firstElementChild.nextSibling;
  345. } else {
  346. node = node.firstElementChild;
  347. }
  348. return node;
  349. }
  350. function find(src,filter,traversalF) {
  351. while(src!=null) {
  352. src = traversalF(src);
  353. if(src!=null && filter(src))
  354. return src;
  355. }
  356. return null;
  357. }
  358. /**
  359. * Traverses a form in the reverse document order starting from the given element (but excluding it),
  360. * until the given filter matches, or run out of an element.
  361. */
  362. function findPrevious(src,filter) {
  363. return find(src,filter,function (e) {
  364. var p = e.previousSibling;
  365. if(p==null) return e.parentNode;
  366. while(p.lastElementChild!=null)
  367. p = p.lastElementChild;
  368. return p;
  369. });
  370. }
  371. function findNext(src,filter) {
  372. return find(src,filter,function (e) {
  373. var n = e.nextSibling;
  374. if(n==null) return e.parentNode;
  375. while(n.firstElementChild!=null)
  376. n = n.firstElementChild;
  377. return n;
  378. });
  379. }
  380. function findFormItem(src,name,directionF) {
  381. var name2 = "_."+name; // handles <textbox field="..." /> notation silently
  382. return directionF(src,function(e){
  383. if (e.tagName == "INPUT" && e.type=="radio" && e.checked==true) {
  384. var r = 0;
  385. while (e.name.substring(r,r+8)=='removeme') //radio buttons have must be unique in repeatable blocks so name is prefixed
  386. r = e.name.indexOf('_',r+8)+1;
  387. return name == e.name.substring(r);
  388. }
  389. return (e.tagName=="INPUT" || e.tagName=="TEXTAREA" || e.tagName=="SELECT") && (e.name==name || e.name==name2); });
  390. }
  391. /**
  392. * Traverses a form in the reverse document order and finds an INPUT element that matches the given name.
  393. */
  394. function findPreviousFormItem(src,name) {
  395. return findFormItem(src,name,findPrevious);
  396. }
  397. function findNextFormItem(src,name) {
  398. return findFormItem(src,name,findNext);
  399. }
  400. // This method seems unused in the ecosystem, only grails-plugin was using it but it's blacklisted now
  401. /**
  402. * Parse HTML into DOM.
  403. */
  404. function parseHtml(html) {
  405. var c = document.createElement("div");
  406. c.innerHTML = html;
  407. return c.firstElementChild;
  408. }
  409. /**
  410. * Evaluates the script in global context.
  411. */
  412. function geval(script) {
  413. // execScript chokes on "" but eval doesn't, so we need to reject it first.
  414. if (script==null || script=="") return;
  415. // see http://perfectionkills.com/global-eval-what-are-the-options/
  416. // note that execScript cannot return value
  417. (this.execScript || eval)(script);
  418. }
  419. /**
  420. * Emulate the firing of an event.
  421. *
  422. * @param {HTMLElement} element
  423. * The element that will fire the event
  424. * @param {String} event
  425. * like 'change', 'blur', etc.
  426. */
  427. function fireEvent(element,event){
  428. if (document.createEvent) {
  429. // dispatch for firefox + others
  430. var evt = document.createEvent("HTMLEvents");
  431. evt.initEvent(event, true, true ); // event type,bubbling,cancelable
  432. return !element.dispatchEvent(evt);
  433. } else {
  434. // dispatch for IE
  435. var evt = document.createEventObject();
  436. return element.fireEvent('on'+event,evt)
  437. }
  438. }
  439. // shared tooltip object
  440. var tooltip;
  441. // Behavior rules
  442. //========================================================
  443. // using tag names in CSS selector makes the processing faster
  444. function registerValidator(e) {
  445. // Retrieve the validation error area
  446. var tr = findFollowingTR(e, "validation-error-area");
  447. if (!tr) {
  448. console.warn("Couldn't find the expected parent element (.setting-main) for element", e)
  449. return;
  450. }
  451. // find the validation-error-area
  452. e.targetElement = tr.firstElementChild.nextSibling;
  453. e.targetUrl = function() {
  454. var url = this.getAttribute("checkUrl");
  455. var depends = this.getAttribute("checkDependsOn");
  456. if (depends==null) {// legacy behaviour where checkUrl is a JavaScript
  457. try {
  458. return eval(url); // need access to 'this', so no 'geval'
  459. } catch (e) {
  460. if (window.console!=null) console.warn("Legacy checkUrl '" + url + "' is not valid JavaScript: "+e);
  461. if (window.YUI!=null) YUI.log("Legacy checkUrl '" + url + "' is not valid JavaScript: "+e,"warn");
  462. return url; // return plain url as fallback
  463. }
  464. } else {
  465. var q = qs(this).addThis();
  466. if (depends.length>0)
  467. depends.split(" ").each(TryEach(function (n) {
  468. q.nearBy(n);
  469. }));
  470. return url+ q.toString();
  471. }
  472. };
  473. var method = e.getAttribute("checkMethod") || "post";
  474. var url = e.targetUrl();
  475. try {
  476. FormChecker.delayedCheck(url, method, e.targetElement);
  477. } catch (x) {
  478. // this happens if the checkUrl refers to a non-existing element.
  479. // don't let this kill off the entire JavaScript
  480. YAHOO.log("Failed to register validation method: "+e.getAttribute("checkUrl")+" : "+e);
  481. return;
  482. }
  483. var checker = function() {
  484. var target = this.targetElement;
  485. FormChecker.sendRequest(this.targetUrl(), {
  486. method : method,
  487. onComplete : function(x) {
  488. if (x.status == 200) {
  489. // All FormValidation responses are 200
  490. target.innerHTML = x.responseText;
  491. } else {
  492. // Content is taken from FormValidation#_errorWithMarkup
  493. // TODO Add i18n support
  494. target.innerHTML = "<div class='error'>An internal error occurred during form field validation (HTTP " + x.status + "). Please reload the page and if the problem persists, ask the administrator for help.</div>";
  495. }
  496. Behaviour.applySubtree(target);
  497. }
  498. });
  499. }
  500. var oldOnchange = e.onchange;
  501. if(typeof oldOnchange=="function") {
  502. e.onchange = function() { checker.call(this); oldOnchange.call(this); }
  503. } else
  504. e.onchange = checker;
  505. var v = e.getAttribute("checkDependsOn");
  506. if (v) {
  507. v.split(" ").each(TryEach(function (name) {
  508. var c = findNearBy(e,name);
  509. if (c==null) {
  510. if (window.console!=null) console.warn("Unable to find nearby "+name);
  511. if (window.YUI!=null) YUI.log("Unable to find a nearby control of the name "+name,"warn")
  512. return;
  513. }
  514. $(c).observe("change",checker.bind(e));
  515. }));
  516. }
  517. e = null; // avoid memory leak
  518. }
  519. function registerRegexpValidator(e,regexp,message) {
  520. var tr = findFollowingTR(e, "validation-error-area");
  521. if (!tr) {
  522. console.warn("Couldn't find the expected parent element (.setting-main) for element", e)
  523. return;
  524. }
  525. // find the validation-error-area
  526. e.targetElement = tr.firstElementChild.nextSibling;
  527. var checkMessage = e.getAttribute('checkMessage');
  528. if (checkMessage) message = checkMessage;
  529. var oldOnchange = e.onchange;
  530. e.onchange = function() {
  531. var set = oldOnchange != null ? oldOnchange.call(this) : false;
  532. if (this.value.match(regexp)) {
  533. if (!set) this.targetElement.innerHTML = "<div/>";
  534. } else {
  535. this.targetElement.innerHTML = "<div class=error>" + message + "</div>";
  536. set = true;
  537. }
  538. return set;
  539. }
  540. e.onchange.call(e);
  541. e = null; // avoid memory leak
  542. }
  543. /**
  544. * Add a validator for number fields which contains 'min', 'max' attribute
  545. * @param e Input element
  546. */
  547. function registerMinMaxValidator(e) {
  548. var tr = findFollowingTR(e, "validation-error-area");
  549. if (!tr) {
  550. console.warn("Couldn't find the expected parent element (.setting-main) for element", e)
  551. return;
  552. }
  553. // find the validation-error-area
  554. e.targetElement = tr.firstElementChild.nextSibling;
  555. var checkMessage = e.getAttribute('checkMessage');
  556. if (checkMessage) message = checkMessage;
  557. var oldOnchange = e.onchange;
  558. e.onchange = function() {
  559. var set = oldOnchange != null ? oldOnchange.call(this) : false;
  560. const min = this.getAttribute('min');
  561. const max = this.getAttribute('max');
  562. function isInteger(str) {
  563. return str.match(/^-?\d*$/) !== null;
  564. }
  565. if (isInteger(this.value)) { // Ensure the value is an integer
  566. if ((min !== null && isInteger(min)) && (max !== null && isInteger(max))) { // Both min and max attributes are available
  567. if (min <= max) { // Add the validator if min <= max
  568. if (parseInt(min) > parseInt(this.value) || parseInt(this.value) > parseInt(max)) { // The value is out of range
  569. this.targetElement.innerHTML = "<div class=error>This value should be between " + min + " and " + max + "</div>";
  570. set = true;
  571. } else {
  572. if (!set) this.targetElement.innerHTML = "<div/>"; // The value is valid
  573. }
  574. }
  575. } else if ((min !== null && isInteger(min)) && (max === null || !isInteger(max))) { // There is only 'min' available
  576. if (parseInt(min) > parseInt(this.value)) {
  577. this.targetElement.innerHTML = "<div class=error>This value should be larger than " + min + "</div>";
  578. set = true;
  579. } else {
  580. if (!set) this.targetElement.innerHTML = "<div/>";
  581. }
  582. } else if ((min === null || !isInteger(min)) && (max !== null && isInteger(max))) { // There is only 'max' available
  583. if (parseInt(max) < parseInt(this.value)) {
  584. this.targetElement.innerHTML = "<div class=error>This value should be less than " + max + "</div>";
  585. set = true;
  586. } else {
  587. if (!set) this.targetElement.innerHTML = "<div/>";
  588. }
  589. }
  590. }
  591. return set;
  592. }
  593. e.onchange.call(e);
  594. e = null; // avoid memory leak
  595. }
  596. /**
  597. * Prevent user input 'e' or 'E' in <f:number>
  598. * @param event Input event
  599. */
  600. function preventInputEe(event) {
  601. if (event.which === 69 || event.which === 101) {
  602. event.preventDefault();
  603. }
  604. }
  605. /**
  606. * Wraps a <button> into YUI button.
  607. *
  608. * @param e
  609. * button element
  610. * @param onclick
  611. * onclick handler
  612. * @return
  613. * YUI Button widget.
  614. */
  615. function makeButton(e,onclick) {
  616. var h = e.onclick;
  617. var clsName = e.className;
  618. var n = e.name;
  619. var attributes = {};
  620. // YUI Button class interprets value attribute of <input> as HTML
  621. // similar to how the child nodes of a <button> are treated as HTML.
  622. // in standard HTML, we wouldn't expect the former case, yet here we are!
  623. if (e.tagName === 'INPUT') {
  624. attributes.label = e.value.escapeHTML();
  625. }
  626. var btn = new YAHOO.widget.Button(e, attributes);
  627. if(onclick!=null)
  628. btn.addListener("click",onclick);
  629. if(h!=null)
  630. btn.addListener("click",h);
  631. var be = btn.get("element");
  632. var classesSeparatedByWhitespace = clsName.split(' ');
  633. for (var i = 0; i < classesSeparatedByWhitespace.length; i++) {
  634. var singleClass = classesSeparatedByWhitespace[i];
  635. if (singleClass) {
  636. be.classList.add(singleClass);
  637. }
  638. }
  639. if(n) // copy the name
  640. be.setAttribute("name",n);
  641. // keep the data-* attributes from the source
  642. var length = e.attributes.length;
  643. for (var i = 0; i < length; i++) {
  644. var attribute = e.attributes[i];
  645. var attributeName = attribute.name;
  646. if (attributeName.startsWith('data-')) {
  647. btn._button.setAttribute(attributeName, attribute.value);
  648. }
  649. }
  650. return btn;
  651. }
  652. /*
  653. If we are inside 'to-be-removed' class, some HTML altering behaviors interact badly, because
  654. the behavior re-executes when the removed master copy gets reinserted later.
  655. */
  656. function isInsideRemovable(e) {
  657. return Element.ancestors(e).find(function(f){return f.hasClassName("to-be-removed");});
  658. }
  659. /**
  660. * Render the template captured by &lt;l:renderOnDemand> at the element 'e' and replace 'e' by the content.
  661. *
  662. * @param {HTMLElement} e
  663. * The place holder element to be lazy-rendered.
  664. * @param {boolean} noBehaviour
  665. * if specified, skip the application of behaviour rule.
  666. */
  667. function renderOnDemand(e,callback,noBehaviour) {
  668. if (!e || !Element.hasClassName(e,"render-on-demand")) return;
  669. var proxy = eval(e.getAttribute("proxy"));
  670. proxy.render(function (t) {
  671. var contextTagName = e.parentNode.tagName;
  672. var c;
  673. if (contextTagName=="TBODY") {
  674. c = document.createElement("DIV");
  675. c.innerHTML = "<TABLE><TBODY>"+t.responseText+"</TBODY></TABLE>";
  676. c = c./*JENKINS-15494*/lastElementChild.firstElementChild;
  677. } else {
  678. c = document.createElement(contextTagName);
  679. c.innerHTML = t.responseText;
  680. }
  681. var elements = [];
  682. while (c.firstElementChild!=null) {
  683. var n = c.firstElementChild;
  684. e.parentNode.insertBefore(n,e);
  685. if (n.nodeType==1 && !noBehaviour)
  686. elements.push(n);
  687. }
  688. Element.remove(e);
  689. evalInnerHtmlScripts(t.responseText,function() {
  690. Behaviour.applySubtree(elements,true);
  691. if (callback) callback(t);
  692. });
  693. });
  694. }
  695. /**
  696. * Finds all the script tags
  697. */
  698. function evalInnerHtmlScripts(text,callback) {
  699. var q = [];
  700. var matchAll = new RegExp('<script([^>]*)>([\\S\\s]*?)<\/script>', 'img');
  701. var matchOne = new RegExp('<script([^>]*)>([\\S\\s]*?)<\/script>', 'im');
  702. var srcAttr = new RegExp('src=[\'\"]([^\'\"]+)[\'\"]','i');
  703. (text.match(matchAll)||[]).map(function(s) {
  704. var m = s.match(srcAttr);
  705. if (m) {
  706. q.push(function(cont) {
  707. loadScript(m[1],cont);
  708. });
  709. } else {
  710. q.push(function(cont) {
  711. geval(s.match(matchOne)[2]);
  712. cont();
  713. });
  714. }
  715. });
  716. q.push(callback);
  717. sequencer(q);
  718. }
  719. /**
  720. * Take an array of (typically async) functions and run them in a sequence.
  721. * Each of the function in the array takes one 'continuation' parameter, and upon the completion
  722. * of the function it needs to invoke "continuation()" to signal the execution of the next function.
  723. */
  724. function sequencer(fs) {
  725. var nullFunction = function() {}
  726. function next() {
  727. if (fs.length>0) {
  728. (fs.shift()||nullFunction)(next);
  729. }
  730. }
  731. return next();
  732. }
  733. function progressBarOnClick() {
  734. var href = this.getAttribute("href");
  735. if(href!=null) window.location = href;
  736. }
  737. function expandButton(e) {
  738. var link = e.target;
  739. while(!Element.hasClassName(link,"advancedLink"))
  740. link = link.parentNode;
  741. link.style.display = "none";
  742. $(link).next().style.display="block";
  743. layoutUpdateCallback.call();
  744. }
  745. function labelAttachPreviousOnClick() {
  746. var e = $(this).previous();
  747. while (e!=null) {
  748. if (e.tagName=="INPUT") {
  749. e.click();
  750. break;
  751. }
  752. e = e.previous();
  753. }
  754. }
  755. function helpButtonOnClick() {
  756. var tr = findFollowingTR(this, "help-area", "help-sibling") ||
  757. findFollowingTR(this, "help-area", "setting-help") ||
  758. findFollowingTR(this, "help-area");
  759. var div = $(tr).down();
  760. if (!div.hasClassName("help"))
  761. div = div.next().down();
  762. if (div.style.display != "block") {
  763. div.style.display = "block";
  764. // make it visible
  765. new Ajax.Request(this.getAttribute("helpURL"), {
  766. method : 'get',
  767. onSuccess : function(x) {
  768. var from = x.getResponseHeader("X-Plugin-From");
  769. div.innerHTML = x.responseText+(from?"<div class='from-plugin'>"+from+"</div>":"");
  770. layoutUpdateCallback.call();
  771. },
  772. onFailure : function(x) {
  773. div.innerHTML = "<b>ERROR</b>: Failed to load help file: " + x.statusText;
  774. layoutUpdateCallback.call();
  775. }
  776. });
  777. } else {
  778. div.style.display = "none";
  779. layoutUpdateCallback.call();
  780. }
  781. return false;
  782. }
  783. function isGeckoCommandKey() {
  784. return Prototype.Browser.Gecko && event.keyCode == 224
  785. }
  786. function isOperaCommandKey() {
  787. return Prototype.Browser.Opera && event.keyCode == 17
  788. }
  789. function isWebKitCommandKey() {
  790. return Prototype.Browser.WebKit && (event.keyCode == 91 || event.keyCode == 93)
  791. }
  792. function isCommandKey() {
  793. return isGeckoCommandKey() || isOperaCommandKey() || isWebKitCommandKey();
  794. }
  795. function isReturnKeyDown() {
  796. return event.type == 'keydown' && event.keyCode == Event.KEY_RETURN;
  797. }
  798. function getParentForm(element) {
  799. if (element == null) throw 'not found a parent form';
  800. if (element instanceof HTMLFormElement) return element;
  801. return getParentForm(element.parentNode);
  802. }
  803. // figure out the corresponding end marker
  804. function findEnd(e) {
  805. for( var depth=0; ; e=$(e).next()) {
  806. if(Element.hasClassName(e,"rowvg-start")) depth++;
  807. if(Element.hasClassName(e,"rowvg-end")) depth--;
  808. if(depth==0) return e;
  809. }
  810. }
  811. function makeOuterVisible(b) {
  812. this.outerVisible = b;
  813. this.updateVisibility();
  814. }
  815. function makeInnerVisible(b) {
  816. this.innerVisible = b;
  817. this.updateVisibility();
  818. }
  819. function updateVisibility() {
  820. var display = (this.outerVisible && this.innerVisible) ? "" : "none";
  821. for (var e=this.start; e!=this.end; e=$(e).next()) {
  822. if (e.rowVisibilityGroup && e!=this.start) {
  823. e.rowVisibilityGroup.makeOuterVisible(this.innerVisible);
  824. e = e.rowVisibilityGroup.end; // the above call updates visibility up to e.rowVisibilityGroup.end inclusive
  825. } else {
  826. e.style.display = display;
  827. }
  828. }
  829. layoutUpdateCallback.call();
  830. }
  831. function rowvgStartEachRow(recursive,f) {
  832. if (recursive) {
  833. for (var e=this.start; e!=this.end; e=$(e).next())
  834. f(e);
  835. } else {
  836. throw "not implemented yet";
  837. }
  838. }
  839. (function () {
  840. var p = 20;
  841. Behaviour.specify("BODY", "body", ++p, function() {
  842. tooltip = new YAHOO.widget.Tooltip("tt", {context:[], zindex:999});
  843. });
  844. Behaviour.specify("TABLE.sortable", "table-sortable", ++p, function(e) {// sortable table
  845. e.sortable = new Sortable.Sortable(e);
  846. });
  847. Behaviour.specify("TABLE.progress-bar", "table-progress-bar", ++p, function(e) { // progressBar.jelly
  848. e.onclick = progressBarOnClick;
  849. });
  850. Behaviour.specify("INPUT.expand-button", "input-expand-button", ++p, function(e) {
  851. makeButton(e, expandButton);
  852. });
  853. // <label> that doesn't use ID, so that it can be copied in <repeatable>
  854. Behaviour.specify("LABEL.attach-previous", "label-attach-previous", ++p, function(e) {
  855. e.onclick = labelAttachPreviousOnClick;
  856. });
  857. // form fields that are validated via AJAX call to the server
  858. // elements with this class should have two attributes 'checkUrl' that evaluates to the server URL.
  859. Behaviour.specify("INPUT.validated", "input-validated", ++p, registerValidator);
  860. Behaviour.specify("SELECT.validated", "select-validated", ++p, registerValidator);
  861. Behaviour.specify("TEXTAREA.validated", "textarea-validated", ++p, registerValidator);
  862. // validate required form values
  863. Behaviour.specify("INPUT.required", "input-required", ++p, function(e) { registerRegexpValidator(e,/./,"Field is required"); });
  864. // validate form values to be an integer
  865. Behaviour.specify("INPUT.number", "input-number", ++p, function(e) {
  866. e.addEventListener('keypress', preventInputEe)
  867. registerMinMaxValidator(e);
  868. registerRegexpValidator(e,/^((\-?\d+)|)$/,"Not an integer");
  869. });
  870. Behaviour.specify("INPUT.number-required", "input-number-required", ++p, function(e) {
  871. e.addEventListener('keypress', preventInputEe)
  872. registerMinMaxValidator(e);
  873. registerRegexpValidator(e,/^\-?(\d+)$/,"Not an integer");
  874. });
  875. Behaviour.specify("INPUT.non-negative-number-required", "input-non-negative-number-required", ++p, function(e) {
  876. e.addEventListener('keypress', preventInputEe)
  877. registerMinMaxValidator(e);
  878. registerRegexpValidator(e,/^\d+$/,"Not a non-negative integer");
  879. });
  880. Behaviour.specify("INPUT.positive-number", "input-positive-number", ++p, function(e) {
  881. e.addEventListener('keypress', preventInputEe)
  882. registerMinMaxValidator(e);
  883. registerRegexpValidator(e,/^(\d*[1-9]\d*|)$/,"Not a positive integer");
  884. });
  885. Behaviour.specify("INPUT.positive-number-required", "input-positive-number-required", ++p, function(e) {
  886. e.addEventListener('keypress', preventInputEe)
  887. registerMinMaxValidator(e);
  888. registerRegexpValidator(e,/^[1-9]\d*$/,"Not a positive integer");
  889. });
  890. Behaviour.specify("INPUT.auto-complete", "input-auto-complete", ++p, function(e) {// form field with auto-completion support
  891. // insert the auto-completion container
  892. var div = document.createElement("DIV");
  893. e.parentNode.insertBefore(div,$(e).next()||null);
  894. e.style.position = "relative"; // or else by default it's absolutely positioned, making "width:100%" break
  895. var ds = new YAHOO.util.XHRDataSource(e.getAttribute("autoCompleteUrl"));
  896. ds.responseType = YAHOO.util.XHRDataSource.TYPE_JSON;
  897. ds.responseSchema = {
  898. resultsList: "suggestions",
  899. fields: ["name"]
  900. };
  901. // Instantiate the AutoComplete
  902. var ac = new YAHOO.widget.AutoComplete(e, div, ds);
  903. ac.generateRequest = function(query) {
  904. return "?value=" + query;
  905. };
  906. ac.autoHighlight = false;
  907. ac.prehighlightClassName = "yui-ac-prehighlight";
  908. ac.animSpeed = 0;
  909. ac.formatResult = ac.formatEscapedResult;
  910. ac.useShadow = true;
  911. ac.autoSnapContainer = true;
  912. ac.delimChar = e.getAttribute("autoCompleteDelimChar");
  913. ac.doBeforeExpandContainer = function(textbox,container) {// adjust the width every time we show it
  914. container.style.width=textbox.clientWidth+"px";
  915. var Dom = YAHOO.util.Dom;
  916. Dom.setXY(container, [Dom.getX(textbox), Dom.getY(textbox) + textbox.offsetHeight] );
  917. return true;
  918. }
  919. });
  920. Behaviour.specify("A.jenkins-help-button", "a-jenkins-help-button", ++p, function(e) {
  921. e.onclick = helpButtonOnClick;
  922. e.tabIndex = 9999; // make help link unnavigable from keyboard
  923. e.parentNode.parentNode.addClassName('has-help');
  924. });
  925. // legacy class name
  926. Behaviour.specify("A.help-button", "a-help-button", ++p, function(e) {
  927. e.onclick = helpButtonOnClick;
  928. e.tabIndex = 9999; // make help link unnavigable from keyboard
  929. e.parentNode.parentNode.addClassName('has-help');
  930. });
  931. // Script Console : settings and shortcut key
  932. Behaviour.specify("TEXTAREA.script", "textarea-script", ++p, function(e) {
  933. (function() {
  934. var cmdKeyDown = false;
  935. var mode = e.getAttribute("script-mode") || "text/x-groovy";
  936. var readOnly = eval(e.getAttribute("script-readOnly")) || false;
  937. var w = CodeMirror.fromTextArea(e,{
  938. mode: mode,
  939. lineNumbers: true,
  940. matchBrackets: true,
  941. readOnly: readOnly,
  942. onKeyEvent: function (editor, event){
  943. function saveAndSubmit() {
  944. editor.save();
  945. getParentForm(e).submit();
  946. event.stop();
  947. }
  948. // Mac (Command + Enter)
  949. if (navigator.userAgent.indexOf('Mac') > -1) {
  950. if (event.type == 'keydown' && isCommandKey()) {
  951. cmdKeyDown = true;
  952. }
  953. if (event.type == 'keyup' && isCommandKey()) {
  954. cmdKeyDown = false;
  955. }
  956. if (cmdKeyDown && isReturnKeyDown()) {
  957. saveAndSubmit();
  958. return true;
  959. }
  960. // Windows, Linux (Ctrl + Enter)
  961. } else {
  962. if (event.ctrlKey && isReturnKeyDown()) {
  963. saveAndSubmit();
  964. return true;
  965. }
  966. }
  967. }
  968. }).getWrapperElement();
  969. w.setAttribute("style","border:1px solid black; margin-top: 1em; margin-bottom: 1em")
  970. })();
  971. });
  972. // deferred client-side clickable map.
  973. // this is useful where the generation of <map> element is time consuming
  974. Behaviour.specify("IMG[lazymap]", "img-lazymap-", ++p, function(e) {
  975. new Ajax.Request(
  976. e.getAttribute("lazymap"),
  977. {
  978. method : 'get',
  979. onSuccess : function(x) {
  980. var div = document.createElement("div");
  981. document.body.appendChild(div);
  982. div.innerHTML = x.responseText;
  983. var id = "map" + (iota++);
  984. div.firstElementChild.setAttribute("name", id);
  985. e.setAttribute("usemap", "#" + id);
  986. }
  987. });
  988. });
  989. // resizable text area
  990. Behaviour.specify("TEXTAREA", "textarea", ++p, function(textarea) {
  991. if(Element.hasClassName(textarea,"rich-editor")) {
  992. // rich HTML editor
  993. try {
  994. var editor = new YAHOO.widget.Editor(textarea, {
  995. dompath: true,
  996. animate: true,
  997. handleSubmit: true
  998. });
  999. // probably due to the timing issue, we need to let the editor know
  1000. // that DOM is ready
  1001. editor.DOMReady=true;
  1002. editor.fireQueue();
  1003. editor.render();
  1004. layoutUpdateCallback.call();
  1005. } catch(e) {
  1006. alert(e);
  1007. }
  1008. return;
  1009. }
  1010. // CodeMirror inserts a wrapper element next to the textarea.
  1011. // textarea.nextSibling may not be the handle.
  1012. var handles = findElementsBySelector(textarea.parentNode, ".textarea-handle");
  1013. if(handles.length != 1) return;
  1014. var handle = handles[0];
  1015. var Event = YAHOO.util.Event;
  1016. function getCodemirrorScrollerOrTextarea(){
  1017. return textarea.codemirrorObject ? textarea.codemirrorObject.getScrollerElement() : textarea;
  1018. }
  1019. handle.onmousedown = function(ev) {
  1020. ev = Event.getEvent(ev);
  1021. var s = getCodemirrorScrollerOrTextarea();
  1022. var offset = s.offsetHeight-Event.getPageY(ev);
  1023. s.style.opacity = 0.5;
  1024. document.onmousemove = function(ev) {
  1025. ev = Event.getEvent(ev);
  1026. function max(a,b) { if(a<b) return b; else return a; }
  1027. s.style.height = max(32, offset + Event.getPageY(ev)) + 'px';
  1028. layoutUpdateCallback.call();
  1029. return false;
  1030. };
  1031. document.onmouseup = function() {
  1032. document.onmousemove = null;
  1033. document.onmouseup = null;
  1034. var s = getCodemirrorScrollerOrTextarea();
  1035. s.style.opacity = 1;
  1036. }
  1037. };
  1038. handle.ondblclick = function() {
  1039. var s = getCodemirrorScrollerOrTextarea();
  1040. s.style.height = "1px"; // To get actual height of the textbox, shrink it and show its scrollbar
  1041. s.style.height = s.scrollHeight + 'px';
  1042. }
  1043. });
  1044. // structured form submission
  1045. Behaviour.specify("FORM", "form", ++p, function(form) {
  1046. crumb.appendToForm(form);
  1047. if(Element.hasClassName(form, "no-json"))
  1048. return;
  1049. // add the hidden 'json' input field, which receives the form structure in JSON
  1050. var div = document.createElement("div");
  1051. div.innerHTML = "<input type=hidden name=json value=init>";
  1052. form.appendChild(div);
  1053. var oldOnsubmit = form.onsubmit;
  1054. if (typeof oldOnsubmit == "function") {
  1055. form.onsubmit = function() { return buildFormTree(this) && oldOnsubmit.call(this); }
  1056. } else {
  1057. form.onsubmit = function() { return buildFormTree(this); };
  1058. }
  1059. form = null; // memory leak prevention
  1060. });
  1061. // hook up tooltip.
  1062. // add nodismiss="" if you'd like to display the tooltip forever as long as the mouse is on the element.
  1063. Behaviour.specify("[tooltip]", "-tooltip-", ++p, function(e) {
  1064. applyTooltip(e,e.getAttribute("tooltip"));
  1065. });
  1066. Behaviour.specify("INPUT.submit-button", "input-submit-button", ++p, function(e) {
  1067. makeButton(e);
  1068. });
  1069. Behaviour.specify("INPUT.yui-button", "input-yui-button", ++p, function(e) {
  1070. makeButton(e);
  1071. });
  1072. Behaviour.specify("TR.optional-block-start,DIV.tr.optional-block-start", "tr-optional-block-start-div-tr-optional-block-start", ++p, function(e) { // see optionalBlock.jelly
  1073. // set start.ref to checkbox in preparation of row-set-end processing
  1074. var checkbox = e.down().down();
  1075. e.setAttribute("ref", checkbox.id = "cb"+(iota++));
  1076. });
  1077. // see RowVisibilityGroupTest
  1078. Behaviour.specify("TR.rowvg-start,DIV.tr.rowvg-start", "tr-rowvg-start-div-tr-rowvg-start", ++p, function(e) {
  1079. e.rowVisibilityGroup = {
  1080. outerVisible: true,
  1081. innerVisible: true,
  1082. /**
  1083. * TR that marks the beginning of this visibility group.
  1084. */
  1085. start: e,
  1086. /**
  1087. * TR that marks the end of this visibility group.
  1088. */
  1089. end: findEnd(e),
  1090. /**
  1091. * Considers the visibility of the row group from the point of view of outside.
  1092. * If you think of a row group like a logical DOM node, this is akin to its .style.display.
  1093. */
  1094. makeOuterVisible: makeOuterVisible,
  1095. /**
  1096. * Considers the visibility of the rows in this row group. Since all the rows in a rowvg
  1097. * shares the single visibility, this just needs to be one boolean, as opposed to many.
  1098. *
  1099. * If you think of a row group like a logical DOM node, this is akin to its children's .style.display.
  1100. */
  1101. makeInnerVisible: makeInnerVisible,
  1102. /**
  1103. * Based on innerVisible and outerVisible, update the relevant rows' actual CSS display attribute.
  1104. */
  1105. updateVisibility: updateVisibility,
  1106. /**
  1107. * Enumerate each row and pass that to the given function.
  1108. *
  1109. * @param {boolean} recursive
  1110. * If true, this visits all the rows from nested visibility groups.
  1111. */
  1112. eachRow: rowvgStartEachRow
  1113. };
  1114. });
  1115. Behaviour.specify("TR.row-set-end,DIV.tr.row-set-end", "tr-row-set-end-div-tr-row-set-end", ++p, function(e) { // see rowSet.jelly and optionalBlock.jelly
  1116. // figure out the corresponding start block
  1117. e = $(e);
  1118. var end = e;
  1119. for( var depth=0; ; e=e.previous()) {
  1120. if(e.hasClassName("row-set-end")) depth++;
  1121. if(e.hasClassName("row-set-start")) depth--;
  1122. if(depth==0) break;
  1123. }
  1124. var start = e;
  1125. // @ref on start refers to the ID of the element that controls the JSON object created from these rows
  1126. // if we don't find it, turn the start node into the governing node (thus the end result is that you
  1127. // created an intermediate JSON object that's always on.)
  1128. var ref = start.getAttribute("ref");
  1129. if(ref==null)
  1130. start.id = ref = "rowSetStart"+(iota++);
  1131. applyNameRef(start,end,ref);
  1132. });
  1133. Behaviour.specify("TR.optional-block-start,DIV.tr.optional-block-start", "tr-optional-block-start-div-tr-optional-block-start-2", ++p, function(e) { // see optionalBlock.jelly
  1134. // this is suffixed by a pointless string so that two processing for optional-block-start
  1135. // can sandwich row-set-end
  1136. // this requires "TR.row-set-end" to mark rows
  1137. var checkbox = e.down().down();
  1138. updateOptionalBlock(checkbox,false);
  1139. });
  1140. // image that shows [+] or [-], with hover effect.
  1141. // oncollapsed and onexpanded will be called when the button is triggered.
  1142. Behaviour.specify("IMG.fold-control", "img-fold-control", ++p, function(e) {
  1143. function changeTo(e,img) {
  1144. var src = e.src;
  1145. e.src = src.substring(0,src.lastIndexOf('/'))+"/"+e.getAttribute("state")+img;
  1146. }
  1147. e.onmouseover = function() {
  1148. changeTo(this,"-hover.png");
  1149. };
  1150. e.onmouseout = function() {
  1151. changeTo(this,".png");
  1152. };
  1153. e.parentNode.onclick = function(event) {
  1154. var e = this.firstElementChild;
  1155. var s = e.getAttribute("state");
  1156. if(s=="plus") {
  1157. e.setAttribute("state","minus");
  1158. if(e.onexpanded) e.onexpanded();
  1159. } else {
  1160. e.setAttribute("state","plus");
  1161. if(e.oncollapsed) e.oncollapsed();
  1162. }
  1163. changeTo(e,"-hover.png");
  1164. YAHOO.util.Event.stopEvent(event);
  1165. return false;
  1166. };
  1167. e = null; // memory leak prevention
  1168. });
  1169. // editableComboBox.jelly
  1170. Behaviour.specify("INPUT.combobox", "input-combobox", ++p, function(c) {
  1171. // Next element after <input class="combobox"/> should be <div class="combobox-values">
  1172. var vdiv = $(c).next();
  1173. if (vdiv.hasClassName("combobox-values")) {
  1174. createComboBox(c, function() {
  1175. return vdiv.childElements().collect(function(value) {
  1176. return value.getAttribute('value');
  1177. });
  1178. });
  1179. }
  1180. });
  1181. // dropdownList.jelly
  1182. Behaviour.specify("SELECT.dropdownList", "select-dropdownlist", ++p, function(e) {
  1183. if(isInsideRemovable(e)) return;
  1184. var subForms = [];
  1185. var start = findInFollowingTR(e, 'dropdownList-container'), end;
  1186. do { start = start.firstElementChild; } while (start && !isTR(start));
  1187. if (start && !Element.hasClassName(start,'dropdownList-start'))
  1188. start = findFollowingTR(start, 'dropdownList-start');
  1189. while (start != null) {
  1190. subForms.push(start);
  1191. start = findFollowingTR(start, 'dropdownList-start');
  1192. }
  1193. // control visibility
  1194. function updateDropDownList() {
  1195. for (var i = 0; i < subForms.length; i++) {
  1196. var show = e.selectedIndex == i;
  1197. var f = $(subForms[i]);
  1198. if (show) renderOnDemand(f.next());
  1199. f.rowVisibilityGroup.makeInnerVisible(show);
  1200. // TODO: this is actually incorrect in the general case if nested vg uses field-disabled
  1201. // so far dropdownList doesn't create such a situation.
  1202. f.rowVisibilityGroup.eachRow(true, show?function(e) {
  1203. e.removeAttribute("field-disabled");
  1204. } : function(e) {
  1205. e.setAttribute("field-disabled","true");
  1206. });
  1207. }
  1208. }
  1209. e.onchange = updateDropDownList;
  1210. updateDropDownList();
  1211. });
  1212. Behaviour.specify("A.showDetails", "a-showdetails", ++p, function(e) {
  1213. e.onclick = function() {
  1214. this.style.display = 'none';
  1215. $(this).next().style.display = 'block';
  1216. layoutUpdateCallback.call();
  1217. return false;
  1218. };
  1219. e = null; // avoid memory leak
  1220. });
  1221. Behaviour.specify("DIV.behavior-loading", "div-behavior-loading", ++p, function(e) {
  1222. e.style.display = 'none';
  1223. });
  1224. Behaviour.specify(".button-with-dropdown", "-button-with-dropdown", ++p, function (e) {
  1225. new YAHOO.widget.Button(e, { type: "menu", menu: $(e).next() });
  1226. });
  1227. Behaviour.specify(".track-mouse", "-track-mouse", ++p, function (element) {
  1228. var DOM = YAHOO.util.Dom;
  1229. $(element).observe("mouseenter",function () {
  1230. element.addClassName("mouseover");
  1231. var mousemoveTracker = function (event) {
  1232. var elementRegion = DOM.getRegion(element);
  1233. if (event.x < elementRegion.left || event.x > elementRegion.right ||
  1234. event.y < elementRegion.top || event.y > elementRegion.bottom) {
  1235. element.removeClassName("mouseover");
  1236. Element.stopObserving(document, "mousemove", mousemoveTracker);
  1237. }
  1238. };
  1239. Element.observe(document, "mousemove", mousemoveTracker);
  1240. });
  1241. });
  1242. /*
  1243. Use on div tag to make it sticky visible on the bottom of the page.
  1244. When page scrolls it remains in the bottom of the page
  1245. Convenient on "OK" button and etc for a long form page
  1246. */
  1247. Behaviour.specify("#bottom-sticker", "-bottom-sticker", ++p, function(sticker) {
  1248. var DOM = YAHOO.util.Dom;
  1249. var shadow = document.createElement("div");
  1250. sticker.parentNode.insertBefore(shadow,sticker);
  1251. var edge = document.createElement("div");
  1252. edge.className = "bottom-sticker-edge";
  1253. sticker.insertBefore(edge,sticker.firstElementChild);
  1254. function adjustSticker() {
  1255. shadow.style.height = sticker.offsetHeight + "px";
  1256. var viewport = DOM.getClientRegion();
  1257. var pos = DOM.getRegion(shadow);
  1258. sticker.style.position = "fixed";
  1259. var bottomPos = Math.max(0, viewport.bottom - pos.bottom);
  1260. sticker.style.bottom = bottomPos + "px"
  1261. sticker.style.left = Math.max(0,pos.left-viewport.left) + "px"
  1262. }
  1263. // react to layout change
  1264. Element.observe(window,"scroll",adjustSticker);
  1265. Element.observe(window,"resize",adjustSticker);
  1266. // initial positioning
  1267. Element.observe(window,"load",adjustSticker);
  1268. Event.observe(window, 'jenkins:bottom-sticker-adjust', adjustSticker);
  1269. adjustSticker();
  1270. layoutUpdateCallback.add(adjustSticker);
  1271. });
  1272. Behaviour.specify("#top-sticker", "-top-sticker", ++p, function(sticker) {// legacy
  1273. this[".top-sticker"](sticker);
  1274. });
  1275. /**
  1276. * @param {HTMLElement} sticker
  1277. */
  1278. Behaviour.specify(".top-sticker", "-top-sticker-2", ++p, function(sticker) {
  1279. var DOM = YAHOO.util.Dom;
  1280. var shadow = document.createElement("div");
  1281. sticker.parentNode.insertBefore(shadow,sticker);
  1282. var edge = document.createElement("div");
  1283. edge.className = "top-sticker-edge";
  1284. sticker.insertBefore(edge,sticker.firstElementChild);
  1285. var initialBreadcrumbPosition = DOM.getRegion(shadow);
  1286. function adjustSticker() {
  1287. shadow.style.height = sticker.offsetHeight + "px";
  1288. var viewport = DOM.getClientRegion();
  1289. var pos = DOM.getRegion(shadow);
  1290. sticker.style.position = "fixed";
  1291. if(pos.top <= initialBreadcrumbPosition.top) {
  1292. sticker.style.top = Math.max(0, pos.top-viewport.top) + "px"
  1293. }
  1294. sticker.style.left = Math.max(0,pos.left-viewport.left) + "px"
  1295. }
  1296. // react to layout change
  1297. Element.observe(window,"scroll",adjustSticker);
  1298. Element.observe(window,"resize",adjustSticker);
  1299. // initial positioning
  1300. Element.observe(window,"load",adjustSticker);
  1301. adjustSticker();
  1302. });
  1303. /**
  1304. * Function that provides compatibility to the checkboxes without title on an f:entry
  1305. *
  1306. * When a checkbox is generated by setting the title on the f:entry like
  1307. * <f:entry field="rebaseBeforePush"title="${%Rebase Before Push}">
  1308. * <f:checkbox />
  1309. * </f:entry>
  1310. * This function will copy the title from the .setting-name field to the checkbox label.
  1311. * It will also move the help button.
  1312. *
  1313. * @param {HTMLLabelElement} label
  1314. */
  1315. Behaviour.specify('label.js-checkbox-label-empty', 'form-fallbacks', 1000, function(label) {
  1316. var labelParent = label.parentElement;
  1317. if (!labelParent.classList.contains('setting-main')) return;
  1318. function findSettingName(formGroup) {
  1319. for (var i=0; i<formGroup.childNodes.length; i++) {
  1320. var child = formGroup.childNodes[i];
  1321. if (child.classList.contains('jenkins-form-label') || child.classList.contains('setting-name')) return child;
  1322. }
  1323. }
  1324. var settingName = findSettingName(labelParent.parentNode);
  1325. if (settingName == undefined) return
  1326. var jenkinsHelpButton = settingName.querySelector('.jenkins-help-button');
  1327. var helpLink = jenkinsHelpButton !== null ? jenkinsHelpButton : settingName.querySelector('.setting-help');
  1328. if (helpLink) {
  1329. labelParent.classList.add('help-sibling');
  1330. labelParent.appendChild(helpLink);
  1331. }
  1332. labelParent.parentNode.removeChild(settingName);
  1333. // Copy setting-name text and append it to the checkbox label
  1334. var labelText = settingName.innerText;
  1335. var spanTag = document.createElement('span')
  1336. spanTag.innerHTML = labelText
  1337. label.appendChild(spanTag)
  1338. });
  1339. })();
  1340. var hudsonRules = {}; // legacy name
  1341. // now empty, but plugins can stuff things in here later:
  1342. Behaviour.register(hudsonRules);
  1343. function applyTooltip(e,text) {
  1344. // copied from YAHOO.widget.Tooltip.prototype.configContext to efficiently add a new element
  1345. // event registration via YAHOO.util.Event.addListener leaks memory, so do it by ourselves here
  1346. e.onmouseover = function(ev) {
  1347. var delay = this.getAttribute("nodismiss")!=null ? 99999999 : 5000;
  1348. tooltip.cfg.setProperty("autodismissdelay",delay);
  1349. return tooltip.onContextMouseOver.call(this,YAHOO.util.Event.getEvent(ev),tooltip);
  1350. }
  1351. e.onmousemove = function(ev) { return tooltip.onContextMouseMove.call(this,YAHOO.util.Event.getEvent(ev),tooltip); }
  1352. e.onmouseout = function(ev) { return tooltip.onContextMouseOut .call(this,YAHOO.util.Event.getEvent(ev),tooltip); }
  1353. e.title = text;
  1354. e = null; // avoid memory leak
  1355. }
  1356. var Path = {
  1357. tail : function(p) {
  1358. var idx = p.lastIndexOf("/");
  1359. if (idx<0) return p;
  1360. return p.substring(idx+1);
  1361. }
  1362. };
  1363. /**
  1364. * Install change handlers based on the 'fillDependsOn' attribute.
  1365. */
  1366. function refillOnChange(e,onChange) {
  1367. var deps = [];
  1368. function h() {
  1369. var params = {};
  1370. deps.each(TryEach(function (d) {
  1371. params[d.name] = controlValue(d.control);
  1372. }));
  1373. onChange(params);
  1374. }
  1375. var v = e.getAttribute("fillDependsOn");
  1376. if (v!=null) {
  1377. v.split(" ").each(TryEach(function (name) {
  1378. var c = findNearBy(e,name);
  1379. if (c==null) {
  1380. if (window.console!=null) console.warn("Unable to find nearby "+name);
  1381. if (window.YUI!=null) YUI.log("Unable to find a nearby control of the name "+name,"warn")
  1382. return;
  1383. }
  1384. $(c).observe("change",h);
  1385. deps.push({name:Path.tail(name),control:c});
  1386. }));
  1387. }
  1388. h(); // initial fill
  1389. }
  1390. function xor(a,b) {
  1391. // convert both values to boolean by '!' and then do a!=b
  1392. return !a != !b;
  1393. }
  1394. // used by editableDescription.jelly to replace the description field with a form
  1395. function replaceDescription() {
  1396. var d = document.getElementById("description");
  1397. $(d).down().next().innerHTML = "<div class='jenkins-spinner'></div>";
  1398. new Ajax.Request(
  1399. "./descriptionForm",
  1400. {
  1401. onComplete : function(x) {
  1402. d.innerHTML = x.responseText;
  1403. evalInnerHtmlScripts(x.responseText,function() {
  1404. Behaviour.applySubtree(d);
  1405. d.getElementsByTagName("TEXTAREA")[0].focus();
  1406. });
  1407. layoutUpdateCallback.call();
  1408. }
  1409. }
  1410. );
  1411. return false;
  1412. }
  1413. /**
  1414. * Indicates that form fields from rows [s,e) should be grouped into a JSON object,
  1415. * and attached under the element identified by the specified id.
  1416. */
  1417. function applyNameRef(s,e,id) {
  1418. $(id).groupingNode = true;
  1419. // s contains the node itself
  1420. applyNameRefHelper(s,e,id);
  1421. }
  1422. function applyNameRefHelper(s,e,id) {
  1423. if (s===null)
  1424. return;
  1425. for(var x=$(s).next(); x!=e; x=x.next()) {
  1426. // to handle nested <f:rowSet> correctly, don't overwrite the existing value
  1427. if(x.getAttribute("nameRef")==null) {
  1428. x.setAttribute("nameRef",id);
  1429. if (x.hasClassName('tr'))
  1430. applyNameRefHelper(x.firstElementChild,null,id);
  1431. }
  1432. }
  1433. }
  1434. // used by optionalBlock.jelly to update the form status
  1435. // @param c checkbox element
  1436. function updateOptionalBlock(c,scroll) {
  1437. // find the start TR
  1438. var s = $(c);
  1439. while(!s.hasClassName("optional-block-start"))
  1440. s = s.up();
  1441. // find the beginning of the rowvg
  1442. var vg =s;
  1443. while (!vg.hasClassName("rowvg-start"))
  1444. vg = vg.next();
  1445. var checked = xor(c.checked,Element.hasClassName(c,"negative"));
  1446. vg.rowVisibilityGroup.makeInnerVisible(checked);
  1447. if(checked && scroll) {
  1448. var D = YAHOO.util.Dom;
  1449. var r = D.getRegion(s);
  1450. r = r.union(D.getRegion(vg.rowVisibilityGroup.end));
  1451. scrollIntoView(r);
  1452. }
  1453. if (c.name == 'hudson-tools-InstallSourceProperty') {
  1454. // Hack to hide tool home when "Install automatically" is checked.
  1455. var homeField = findPreviousFormItem(c, 'home');
  1456. if (homeField != null && homeField.value == '') {
  1457. var tr = findAncestor(homeField, 'TR') || findAncestorClass(homeField, 'tr');
  1458. if (tr != null) {
  1459. tr.style.display = c.checked ? 'none' : '';
  1460. layoutUpdateCallback.call();
  1461. }
  1462. }
  1463. }
  1464. }
  1465. //
  1466. // Auto-scroll support for progressive log output.
  1467. // See http://radio.javaranch.com/pascarello/2006/08/17/1155837038219.html
  1468. //
  1469. function AutoScroller(scrollContainer) {
  1470. // get the height of the viewport.
  1471. // See http://www.howtocreate.co.uk/tutorials/javascript/browserwindow
  1472. function getViewportHeight() {
  1473. if (typeof( window.innerWidth ) == 'number') {
  1474. //Non-IE
  1475. return window.innerHeight;
  1476. } else if (document.documentElement && ( document.documentElement.clientWidth || document.documentElement.clientHeight )) {
  1477. //IE 6+ in 'standards compliant mode'
  1478. return document.documentElement.clientHeight;
  1479. } else if (document.body && ( document.body.clientWidth || document.body.clientHeight )) {
  1480. //IE 4 compatible
  1481. return document.body.clientHeight;
  1482. }
  1483. return null;
  1484. }
  1485. return {
  1486. bottomThreshold : 25,
  1487. scrollContainer: scrollContainer,
  1488. getCurrentHeight : function() {
  1489. var scrollDiv = $(this.scrollContainer);
  1490. if (scrollDiv.scrollHeight > 0)
  1491. return scrollDiv.scrollHeight;
  1492. else
  1493. if (scrollDiv.offsetHeight > 0)
  1494. return scrollDiv.offsetHeight;
  1495. return null; // huh?
  1496. },
  1497. // return true if we are in the "stick to bottom" mode
  1498. isSticking : function() {
  1499. var scrollDiv = $(this.scrollContainer);
  1500. var currentHeight = this.getCurrentHeight();
  1501. // when used with the BODY tag, the height needs to be the viewport height, instead of
  1502. // the element height.
  1503. //var height = ((scrollDiv.style.pixelHeight) ? scrollDiv.style.pixelHeight : scrollDiv.offsetHeight);
  1504. var height = getViewportHeight();
  1505. var scrollPos = Math.max(scrollDiv.scrollTop, document.documentElement.scrollTop);
  1506. var diff = currentHeight - scrollPos - height;
  1507. // window.alert("currentHeight=" + currentHeight + ",scrollTop=" + scrollDiv.scrollTop + ",height=" + height);
  1508. return diff < this.bottomThreshold;
  1509. },
  1510. scrollToBottom : function() {
  1511. var scrollDiv = $(this.scrollContainer);
  1512. var currentHeight = this.getCurrentHeight();
  1513. if(document.documentElement) document.documentElement.scrollTop = currentHeight
  1514. scrollDiv.scrollTop = currentHeight;
  1515. }
  1516. };
  1517. }
  1518. // scroll the current window to display the given element or the region.
  1519. function scrollIntoView(e) {
  1520. function calcDelta(ex1,ex2,vx1,vw) {
  1521. var vx2=vx1+vw;
  1522. var a;
  1523. a = Math.min(vx1-ex1,vx2-ex2);
  1524. if(a>0) return -a;
  1525. a = Math.min(ex1-vx1,ex2-vx2);
  1526. if(a>0) return a;
  1527. return 0;
  1528. }
  1529. var D = YAHOO.util.Dom;
  1530. var r;
  1531. if(e.tagName!=null) r = D.getRegion(e);
  1532. else r = e;
  1533. var dx = calcDelta(r.left,r.right, document.body.scrollLeft, D.getViewportWidth());
  1534. var dy = calcDelta(r.top, r.bottom,document.body.scrollTop, D.getViewportHeight());
  1535. window.scrollBy(dx,dy);
  1536. }
  1537. // used in expandableTextbox.jelly to change a input field into a text area
  1538. function expandTextArea(button,id) {
  1539. button.style.display="none";
  1540. var field = button.parentNode.previousSibling.children[0];
  1541. var value = field.value.replace(/ +/g,'\n');
  1542. var n = button;
  1543. while (!n.classList.contains("expanding-input") && n.tagName != "TABLE")
  1544. {
  1545. n = n.parentNode;
  1546. }
  1547. var parent = n.parentNode;
  1548. parent.innerHTML = "<textarea rows=8 class='setting-input'></textarea>";
  1549. var textArea = parent.childNodes[0];
  1550. textArea.name = field.name;
  1551. textArea.innerText = value;
  1552. layoutUpdateCallback.call();
  1553. }
  1554. // refresh a part of the HTML specified by the given ID,
  1555. // by using the contents fetched from the given URL.
  1556. function refreshPart(id,url) {
  1557. var intervalID = null;
  1558. var f = function() {
  1559. if(isPageVisible()) {
  1560. new Ajax.Request(url, {
  1561. onSuccess: function(rsp) {
  1562. var hist = $(id);
  1563. if (hist == null) {
  1564. console.log("There's no element that has ID of " + id);
  1565. if (intervalID !== null)
  1566. window.clearInterval(intervalID);
  1567. return;
  1568. }
  1569. if (!rsp.responseText) {
  1570. console.log("Failed to retrieve response for ID " + id + ", perhaps Jenkins is unavailable");
  1571. return;
  1572. }
  1573. var p = hist.up();
  1574. var div = document.createElement('div');
  1575. div.innerHTML = rsp.responseText;
  1576. var node = $(div).firstDescendant();
  1577. p.replaceChild(node, hist);
  1578. Behaviour.applySubtree(node);
  1579. layoutUpdateCallback.call();
  1580. }
  1581. });
  1582. }
  1583. };
  1584. // if run as test, just do it once and do it now to make sure it's working,
  1585. // but don't repeat.
  1586. if(isRunAsTest) f();
  1587. else intervalID = window.setInterval(f, 5000);
  1588. }
  1589. /*
  1590. Perform URL encode.
  1591. Taken from http://www.cresc.co.jp/tech/java/URLencoding/JavaScript_URLEncoding.htm
  1592. @deprecated Use standard javascript method "encodeURIComponent" instead
  1593. */
  1594. function encode(str){
  1595. var s, u;
  1596. var s0 = ""; // encoded str
  1597. for (var i = 0; i < str.length; i++){ // scan the source
  1598. s = str.charAt(i);
  1599. u = str.charCodeAt(i); // get unicode of the char
  1600. if (s == " "){s0 += "+";} // SP should be converted to "+"
  1601. else {
  1602. if ( u == 0x2a || u == 0x2d || u == 0x2e || u == 0x5f || ((u >= 0x30) && (u <= 0x39)) || ((u >= 0x41) && (u <= 0x5a)) || ((u >= 0x61) && (u <= 0x7a))){ // check for escape
  1603. s0 = s0 + s; // don't escape
  1604. } else { // escape
  1605. if ((u >= 0x0) && (u <= 0x7f)){ // single byte format
  1606. s = "0"+u.toString(16);
  1607. s0 += "%"+ s.substr(s.length-2);
  1608. } else
  1609. if (u > 0x1fffff){ // quaternary byte format (extended)
  1610. s0 += "%" + (0xF0 + ((u & 0x1c0000) >> 18)).toString(16);
  1611. s0 += "%" + (0x80 + ((u & 0x3f000) >> 12)).toString(16);
  1612. s0 += "%" + (0x80 + ((u & 0xfc0) >> 6)).toString(16);
  1613. s0 += "%" + (0x80 + (u & 0x3f)).toString(16);
  1614. } else
  1615. if (u > 0x7ff){ // triple byte format
  1616. s0 += "%" + (0xe0 + ((u & 0xf000) >> 12)).toString(16);
  1617. s0 += "%" + (0x80 + ((u & 0xfc0) >> 6)).toString(16);
  1618. s0 += "%" + (0x80 + (u & 0x3f)).toString(16);
  1619. } else { // double byte format
  1620. s0 += "%" + (0xc0 + ((u & 0x7c0) >> 6)).toString(16);
  1621. s0 += "%" + (0x80 + (u & 0x3f)).toString(16);
  1622. }
  1623. }
  1624. }
  1625. }
  1626. return s0;
  1627. }
  1628. // when there are multiple form elements of the same name,
  1629. // this method returns the input field of the given name that pairs up
  1630. // with the specified 'base' input element.
  1631. Form.findMatchingInput = function(base, name) {
  1632. // find the FORM element that owns us
  1633. var f = base;
  1634. while (f.tagName != "FORM")
  1635. f = f.parentNode;
  1636. var bases = Form.getInputs(f, null, base.name);
  1637. var targets = Form.getInputs(f, null, name);
  1638. for (var i=0; i<bases.length; i++) {
  1639. if (bases[i] == base)
  1640. return targets[i];
  1641. }
  1642. return null; // not found
  1643. }
  1644. function onBuildHistoryChange(handler) {
  1645. Event.observe(window, 'jenkins:buildHistoryChanged', handler);
  1646. }
  1647. function fireBuildHistoryChanged() {
  1648. Event.fire(window, 'jenkins:buildHistoryChanged');
  1649. }
  1650. function toQueryString(params) {
  1651. var query = '';
  1652. if (params) {
  1653. for (var paramName in params) {
  1654. if (params.hasOwnProperty(paramName)) {
  1655. if (query === '') {
  1656. query = '?';
  1657. } else {
  1658. query += '&';
  1659. }
  1660. query += paramName + '=' + encodeURIComponent(params[paramName]);
  1661. }
  1662. }
  1663. }
  1664. return query;
  1665. }
  1666. function getElementOverflowParams(element) {
  1667. // First we force it to wrap so we can get those dimension.
  1668. // Then we force it to "nowrap", so we can get those dimension.
  1669. // We can then compare the two sets, which will indicate if
  1670. // wrapping is potentially happening, or not.
  1671. // Force it to wrap.
  1672. element.classList.add('force-wrap');
  1673. var wrappedClientWidth = element.clientWidth;
  1674. var wrappedClientHeight = element.clientHeight;
  1675. element.classList.remove('force-wrap');
  1676. // Force it to nowrap. Return the comparisons.
  1677. element.classList.add('force-nowrap');
  1678. var nowrapClientHeight = element.clientHeight;
  1679. try {
  1680. var overflowParams = {
  1681. element: element,
  1682. clientWidth: wrappedClientWidth,
  1683. scrollWidth: element.scrollWidth,
  1684. isOverflowed: wrappedClientHeight > nowrapClientHeight
  1685. };
  1686. return overflowParams;
  1687. } finally {
  1688. element.classList.remove('force-nowrap');
  1689. }
  1690. }
  1691. // get the cascaded computed style value. 'a' is the style name like 'backgroundColor'
  1692. function getStyle(e,a){
  1693. if(document.defaultView && document.defaultView.getComputedStyle)
  1694. return document.defaultView.getComputedStyle(e,null).getPropertyValue(a.replace(/([A-Z])/g, "-$1"));
  1695. if(e.currentStyle)
  1696. return e.currentStyle[a];
  1697. return null;
  1698. }
  1699. function ElementResizeTracker() {
  1700. this.trackedElements = [];
  1701. if(isRunAsTest) {
  1702. return;
  1703. }
  1704. var thisTracker = this;
  1705. function checkForResize() {
  1706. for (var i = 0; i < thisTracker.trackedElements.length; i++) {
  1707. var element = thisTracker.trackedElements[i];
  1708. var currDims = Element.getDimensions(element);
  1709. var lastDims = element.lastDimensions;
  1710. if (currDims.width !== lastDims.width || currDims.height !== lastDims.height) {
  1711. Event.fire(element, 'jenkins:resize');
  1712. }
  1713. element.lastDimensions = currDims;
  1714. }
  1715. }
  1716. Event.observe(window, 'jenkins:resizeCheck', checkForResize);
  1717. function checkForResizeLoop() {
  1718. checkForResize();
  1719. setTimeout(checkForResizeLoop, 200);
  1720. }
  1721. checkForResizeLoop();
  1722. }
  1723. ElementResizeTracker.prototype.addElement = function(element) {
  1724. for (var i = 0; i < this.trackedElements.length; i++) {
  1725. if (this.trackedElements[i] === element) {
  1726. // we're already tracking it so no need to add it.
  1727. return;
  1728. }
  1729. }
  1730. this.trackedElements.push(element);
  1731. }
  1732. ElementResizeTracker.prototype.onResize = function(element, handler) {
  1733. element.lastDimensions = Element.getDimensions(element);
  1734. Event.observe(element, 'jenkins:resize', handler);
  1735. this.addElement(element);
  1736. }
  1737. ElementResizeTracker.fireResizeCheck = function() {
  1738. Event.fire(window, 'jenkins:resizeCheck');
  1739. }
  1740. var elementResizeTracker = new ElementResizeTracker();
  1741. /**
  1742. * Makes sure the given element is within the viewport.
  1743. *
  1744. * @param {HTMLElement} e
  1745. * The element to bring into the viewport.
  1746. */
  1747. function ensureVisible(e) {
  1748. var viewport = YAHOO.util.Dom.getClientRegion();
  1749. var pos = YAHOO.util.Dom.getRegion(e);
  1750. var Y = viewport.top;
  1751. var H = viewport.height;
  1752. function handleStickers(name,f) {
  1753. var e = $(name);
  1754. if (e) f(e);
  1755. document.getElementsBySelector("."+name).each(TryEach(f));
  1756. }
  1757. // if there are any stickers around, subtract them from the viewport
  1758. handleStickers("top-sticker",function (t) {
  1759. t = t.clientHeight;
  1760. Y+=t; H-=t;
  1761. });
  1762. handleStickers("bottom-sticker",function (b) {
  1763. b = b.clientHeight;
  1764. H-=b;
  1765. });
  1766. var y = pos.top;
  1767. var h = pos.height;
  1768. var d = (y+h)-(Y+H);
  1769. if (d>0) {
  1770. document.body.scrollTop += d;
  1771. } else {
  1772. var d = Y-y;
  1773. if (d>0) document.body.scrollTop -= d;
  1774. }
  1775. }
  1776. // set up logic behind the search box
  1777. function createSearchBox(searchURL) {
  1778. var ds = new YAHOO.util.XHRDataSource(searchURL+"suggest");
  1779. ds.responseType = YAHOO.util.XHRDataSource.TYPE_JSON;
  1780. ds.responseSchema = {
  1781. resultsList: "suggestions",
  1782. fields: ["name"]
  1783. };
  1784. var ac = new YAHOO.widget.AutoComplete("search-box","search-box-completion",ds);
  1785. ac.typeAhead = false;
  1786. ac.autoHighlight = false;
  1787. ac.formatResult = ac.formatEscapedResult;
  1788. ac.maxResultsDisplayed = 25;
  1789. var box = $("search-box");
  1790. var sizer = $("search-box-sizer");
  1791. var comp = $("search-box-completion");
  1792. Behaviour.addLoadEvent(function(){
  1793. // copy font style of box to sizer
  1794. var ds = sizer.style;
  1795. ds.fontFamily = getStyle(box, "fontFamily");
  1796. ds.fontSize = getStyle(box, "fontSize");
  1797. ds.fontStyle = getStyle(box, "fontStyle");
  1798. ds.fontWeight = getStyle(box, "fontWeight");
  1799. });
  1800. // update positions and sizes of the components relevant to search
  1801. function updatePos() {
  1802. sizer.innerHTML = box.value.escapeHTML();
  1803. var cssWidth, offsetWidth = sizer.offsetWidth;
  1804. if (offsetWidth > 0) {
  1805. cssWidth = offsetWidth + "px";
  1806. } else { // sizer hidden on small screen, make sure resizing looks OK
  1807. cssWidth = getStyle(sizer, "minWidth");
  1808. }
  1809. box.style.width =
  1810. comp.firstElementChild.style.minWidth = "calc(60px + " + cssWidth + ")";
  1811. var pos = YAHOO.util.Dom.getXY(box);
  1812. pos[1] += YAHOO.util.Dom.get(box).offsetHeight + 2;
  1813. YAHOO.util.Dom.setXY(comp, pos);
  1814. }
  1815. updatePos();
  1816. box.addEventListener("input", updatePos);
  1817. }
  1818. /**
  1819. * Finds the DOM node of the given DOM node that acts as a parent in the form submission.
  1820. *
  1821. * @param {HTMLElement} e
  1822. * The node whose parent we are looking for.
  1823. * @param {HTMLFormElement} form
  1824. * The form element that owns 'e'. Passed in as a performance improvement. Can be null.
  1825. * @return null
  1826. * if the given element shouldn't be a part of the final submission.
  1827. */
  1828. function findFormParent(e,form,isStatic) {
  1829. isStatic = isStatic || false;
  1830. if (form==null) // caller can pass in null to have this method compute the owning form
  1831. form = findAncestor(e,"FORM");
  1832. while(e!=form) {
  1833. // this is used to create a group where no single containing parent node exists,
  1834. // like <optionalBlock>
  1835. var nameRef = e.getAttribute("nameRef");
  1836. if(nameRef!=null)
  1837. e = $(nameRef);
  1838. else
  1839. e = e.parentNode;
  1840. if(!isStatic && e.getAttribute("field-disabled")!=null)
  1841. return null; // this field shouldn't contribute to the final result
  1842. var name = e.getAttribute("name");
  1843. if(name!=null && name.length>0) {
  1844. if(e.tagName=="INPUT" && !isStatic && !xor(e.checked,Element.hasClassName(e,"negative")))
  1845. return null; // field is not active
  1846. return e;
  1847. }
  1848. }
  1849. return form;
  1850. }
  1851. // compute the form field name from the control name
  1852. function shortenName(name) {
  1853. // [abc.def.ghi] -> abc.def.ghi
  1854. if(name.startsWith('['))
  1855. return name.substring(1,name.length-1);
  1856. // abc.def.ghi -> ghi
  1857. var idx = name.lastIndexOf('.');
  1858. if(idx>=0) name = name.substring(idx+1);
  1859. return name;
  1860. }
  1861. //
  1862. // structured form submission handling
  1863. // see https://www.jenkins.io/redirect/developer/structured-form-submission
  1864. function buildFormTree(form) {
  1865. try {
  1866. // I initially tried to use an associative array with DOM elements as keys
  1867. // but that doesn't seem to work neither on IE nor Firefox.
  1868. // so I switch back to adding a dynamic property on DOM.
  1869. form.formDom = {}; // root object
  1870. var doms = []; // DOMs that we added 'formDom' for.
  1871. doms.push(form);
  1872. function addProperty(parent,name,value) {
  1873. name = shortenName(name);
  1874. if(parent[name]!=null) {
  1875. if(parent[name].push==null) // is this array?
  1876. parent[name] = [ parent[name] ];
  1877. parent[name].push(value);
  1878. } else {
  1879. parent[name] = value;
  1880. }
  1881. }
  1882. // find the grouping parent node, which will have @name.
  1883. // then return the corresponding object in the map
  1884. function findParent(e) {
  1885. var p = findFormParent(e,form);
  1886. if (p==null) return {};
  1887. var m = p.formDom;
  1888. if(m==null) {
  1889. // this is a new grouping node
  1890. doms.push(p);
  1891. p.formDom = m = {};
  1892. addProperty(findParent(p), p.getAttribute("name"), m);
  1893. }
  1894. return m;
  1895. }
  1896. var jsonElement = null;
  1897. for( var i=0; i<form.elements.length; i++ ) {
  1898. var e = form.elements[i];
  1899. if(e.name=="json") {
  1900. jsonElement = e;
  1901. continue;
  1902. }
  1903. if(e.tagName=="FIELDSET")
  1904. continue;
  1905. if(e.tagName=="SELECT" && e.multiple) {
  1906. var values = [];
  1907. for( var o=0; o<e.options.length; o++ ) {
  1908. var opt = e.options.item(o);
  1909. if(opt.selected)
  1910. values.push(opt.value);
  1911. }
  1912. addProperty(findParent(e),e.name,values);
  1913. continue;
  1914. }
  1915. var p;
  1916. var r;
  1917. var type = e.getAttribute("type");
  1918. if(type==null) type="";
  1919. switch(type.toLowerCase()) {
  1920. case "button":
  1921. case "submit":
  1922. break;
  1923. case "checkbox":
  1924. p = findParent(e);
  1925. var checked = xor(e.checked,Element.hasClassName(e,"negative"));
  1926. if(!e.groupingNode) {
  1927. v = e.getAttribute("json");
  1928. if (v) {
  1929. // if the special attribute is present, we'll either set the value or not. useful for an array of checkboxes
  1930. // we can't use @value because IE6 sets the value to be "on" if it's left unspecified.
  1931. if (checked)
  1932. addProperty(p, e.name, v);
  1933. } else {// otherwise it'll bind to boolean
  1934. addProperty(p, e.name, checked);
  1935. }
  1936. } else {
  1937. if(checked)
  1938. addProperty(p, e.name, e.formDom = {});
  1939. }
  1940. break;
  1941. case "file":
  1942. // to support structured form submission with file uploads,
  1943. // rename form field names to unique ones, and leave this name mapping information
  1944. // in JSON. this behavior is backward incompatible, so only do
  1945. // this when
  1946. p = findParent(e);
  1947. if(e.getAttribute("jsonAware")!=null) {
  1948. var on = e.getAttribute("originalName");
  1949. if(on!=null) {
  1950. addProperty(p,on,e.name);
  1951. } else {
  1952. var uniqName = "file"+(iota++);
  1953. addProperty(p,e.name,uniqName);
  1954. e.setAttribute("originalName",e.name);
  1955. e.name = uniqName;
  1956. }
  1957. }
  1958. // switch to multipart/form-data to support file submission
  1959. // @enctype is the standard, but IE needs @encoding.
  1960. form.enctype = form.encoding = "multipart/form-data";
  1961. crumb.appendToForm(form);
  1962. break;
  1963. case "radio":
  1964. if(!e.checked) break;
  1965. r=0;
  1966. while (e.name.substring(r,r+8)=='removeme')
  1967. r = e.name.indexOf('_',r+8)+1;
  1968. p = findParent(e);
  1969. if(e.groupingNode) {
  1970. addProperty(p, e.name.substring(r), e.formDom = { value: e.value });
  1971. } else {
  1972. addProperty(p, e.name.substring(r), e.value);
  1973. }
  1974. break;
  1975. case "password":
  1976. p = findParent(e);
  1977. addProperty(p, e.name, e.value);
  1978. // must be kept in sync with RedactSecretJsonForTraceSanitizer.REDACT_KEY
  1979. addProperty(p, "$redact", shortenName(e.name));
  1980. break;
  1981. default:
  1982. p = findParent(e);
  1983. addProperty(p, e.name, e.value);
  1984. if (e.hasClassName("complex-password-field")) {
  1985. addProperty(p, "$redact", shortenName(e.name));
  1986. }
  1987. break;
  1988. }
  1989. }
  1990. jsonElement.value = Object.toJSON(form.formDom);
  1991. // clean up
  1992. for( i=0; i<doms.length; i++ )
  1993. doms[i].formDom = null;
  1994. return true;
  1995. } catch(e) {
  1996. alert(e+'\n(form not submitted)');
  1997. return false;
  1998. }
  1999. }
  2000. /**
  2001. * @param {boolean} toggle
  2002. * When true, will check all checkboxes in the page. When false, unchecks them all.
  2003. */
  2004. var toggleCheckboxes = function(toggle) {
  2005. var inputs = document.getElementsByTagName("input");
  2006. for(var i=0; i<inputs.length; i++) {
  2007. if(inputs[i].type === "checkbox") {
  2008. inputs[i].checked = toggle;
  2009. }
  2010. }
  2011. };
  2012. var hoverNotification = (function() {
  2013. var msgBox;
  2014. var body;
  2015. // animation effect that automatically hide the message box
  2016. var effect = function(overlay, dur) {
  2017. var o = YAHOO.widget.ContainerEffect.FADE(overlay, dur);
  2018. o.animateInCompleteEvent.subscribe(function() {
  2019. window.setTimeout(function() {
  2020. msgBox.hide()
  2021. }, 1500);
  2022. });
  2023. return o;
  2024. }
  2025. function init() {
  2026. if(msgBox!=null) return; // already initialized
  2027. var div = document.createElement("DIV");
  2028. document.body.appendChild(div);
  2029. div.innerHTML = "<div id=hoverNotification class='jenkins-tooltip'><div class=bd></div></div>";
  2030. body = $('hoverNotification');
  2031. msgBox = new YAHOO.widget.Overlay(body, {
  2032. visible:false,
  2033. zIndex:1000,
  2034. effect:{
  2035. effect:effect,
  2036. duration:0.25
  2037. }
  2038. });
  2039. msgBox.render();
  2040. }
  2041. return function(title, anchor, offset) {
  2042. if (typeof offset === 'undefined') {
  2043. offset = 48;
  2044. }
  2045. init();
  2046. body.innerHTML = title;
  2047. var xy = YAHOO.util.Dom.getXY(anchor);
  2048. xy[0] += offset;
  2049. xy[1] += anchor.offsetHeight;
  2050. msgBox.cfg.setProperty("xy",xy);
  2051. msgBox.show();
  2052. };
  2053. })();
  2054. /**
  2055. * Loads the script specified by the URL.
  2056. *
  2057. * @param href
  2058. * The URL of the script to load.
  2059. * @param callback
  2060. * If specified, this function will be invoked after the script is loaded.
  2061. * @see http://stackoverflow.com/questions/4845762/onload-handler-for-script-tag-in-internet-explorer
  2062. */
  2063. function loadScript(href,callback) {
  2064. var head = document.getElementsByTagName("head")[0] || document.documentElement;
  2065. var script = document.createElement("script");
  2066. script.src = href;
  2067. if (callback) {
  2068. // Handle Script loading
  2069. var done = false;
  2070. // Attach handlers for all browsers
  2071. script.onload = script.onreadystatechange = function() {
  2072. if ( !done && (!this.readyState ||
  2073. this.readyState === "loaded" || this.readyState === "complete") ) {
  2074. done = true;
  2075. callback();
  2076. // Handle memory leak in IE
  2077. script.onload = script.onreadystatechange = null;
  2078. if ( head && script.parentNode ) {
  2079. head.removeChild( script );
  2080. }
  2081. }
  2082. };
  2083. }
  2084. // Use insertBefore instead of appendChild to circumvent an IE6 bug.
  2085. // This arises when a base node is used (#2709 and #4378).
  2086. head.insertBefore( script, head.firstElementChild );
  2087. }
  2088. // logic behind <f:validateButton />
  2089. function safeValidateButton(yuiButton) {
  2090. var button = yuiButton._button;
  2091. var descriptorUrl = button.getAttribute('data-validate-button-descriptor-url');
  2092. var method = button.getAttribute('data-validate-button-method');
  2093. var checkUrl = descriptorUrl + "/" + method;
  2094. // optional, by default = empty string
  2095. var paramList = button.getAttribute('data-validate-button-with') || '';
  2096. validateButton(checkUrl, paramList, yuiButton);
  2097. }
  2098. // this method should not be called directly, only get called by safeValidateButton
  2099. // kept "public" for legacy compatibility
  2100. function validateButton(checkUrl,paramList,button) {
  2101. button = button._button;
  2102. var parameters = {};
  2103. paramList.split(',').each(function(name) {
  2104. var p = findPreviousFormItem(button,name);
  2105. if(p!=null) {
  2106. if(p.type=="checkbox") parameters[name] = p.checked;
  2107. else parameters[name] = p.value;
  2108. }
  2109. });
  2110. var spinner = $(button).up("DIV").next();
  2111. var target = spinner.next();
  2112. spinner.style.display="block";
  2113. new Ajax.Request(checkUrl, {
  2114. parameters: parameters,
  2115. onComplete: function(rsp) {
  2116. spinner.style.display="none";
  2117. applyErrorMessage(target, rsp);
  2118. layoutUpdateCallback.call();
  2119. var s = rsp.getResponseHeader("script");
  2120. try {
  2121. geval(s);
  2122. } catch(e) {
  2123. window.alert("failed to evaluate "+s+"\n"+e.message);
  2124. }
  2125. }
  2126. });
  2127. }
  2128. function applyErrorMessage(elt, rsp) {
  2129. if (rsp.status == 200) {
  2130. elt.innerHTML = rsp.responseText;
  2131. } else {
  2132. var id = 'valerr' + (iota++);
  2133. elt.innerHTML = '<a href="" onclick="document.getElementById(\'' + id
  2134. + '\').style.display=\'block\';return false">ERROR</a><div id="'
  2135. + id + '" style="display:none">' + rsp.responseText + '</div>';
  2136. var error = document.getElementById('error-description'); // cf. oops.jelly
  2137. if (error) {
  2138. var div = document.getElementById(id);
  2139. while (div.firstElementChild) {
  2140. div.removeChild(div.firstElementChild);
  2141. }
  2142. div.appendChild(error);
  2143. }
  2144. }
  2145. Behaviour.applySubtree(elt);
  2146. }
  2147. // create a combobox.
  2148. // @param idOrField
  2149. // ID of the <input type=text> element that becomes a combobox, or the field itself.
  2150. // Passing an ID is @deprecated since 1.350; use <input class="combobox"/> instead.
  2151. // @param valueFunction
  2152. // Function that returns all the candidates as an array
  2153. function createComboBox(idOrField,valueFunction) {
  2154. var candidates = valueFunction();
  2155. var creator = function() {
  2156. if (typeof idOrField == "string")
  2157. idOrField = document.getElementById(idOrField);
  2158. if (!idOrField) return;
  2159. new ComboBox(idOrField, function(value /*, comboBox*/) {
  2160. var items = new Array();
  2161. if (value.length > 0) { // if no value, we'll not provide anything
  2162. value = value.toLowerCase();
  2163. for (var i = 0; i<candidates.length; i++) {
  2164. if (candidates[i].toLowerCase().indexOf(value) >= 0) {
  2165. items.push(candidates[i]);
  2166. if(items.length>20)
  2167. break; // 20 items in the list should be enough
  2168. }
  2169. }
  2170. }
  2171. return items; // equiv to: comboBox.setItems(items);
  2172. });
  2173. };
  2174. // If an ID given, create when page has loaded (backward compatibility); otherwise now.
  2175. if (typeof idOrField == "string") Behaviour.addLoadEvent(creator); else creator();
  2176. }
  2177. // Exception in code during the AJAX processing should be reported,
  2178. // so that our users can find them more easily.
  2179. Ajax.Request.prototype.dispatchException = function(e) {
  2180. throw e;
  2181. }
  2182. // event callback when layouts/visibility are updated and elements might have moved around
  2183. var layoutUpdateCallback = {
  2184. callbacks : [],
  2185. add : function (f) {
  2186. this.callbacks.push(f);
  2187. },
  2188. call : function() {
  2189. for (var i = 0, length = this.callbacks.length; i < length; i++)
  2190. this.callbacks[i]();
  2191. }
  2192. }
  2193. // Notification bar
  2194. // ==============================
  2195. // this control displays a single line message at the top of the page, like StackOverflow does
  2196. // see ui-samples for more details
  2197. var notificationBar = {
  2198. OPACITY : 1,
  2199. DELAY : 3000, // milliseconds to auto-close the notification
  2200. div : null, // the main 'notification-bar' DIV
  2201. token : null, // timer for cancelling auto-close
  2202. defaultIcon: "svg-sprite-action-symbol.svg#ic_info_24px",
  2203. defaultAlertClass: "notif-alert-default",
  2204. OK : {// standard option values for typical OK notification
  2205. icon: "svg-sprite-action-symbol.svg#ic_check_circle_24px",
  2206. alertClass: "notif-alert-success",
  2207. },
  2208. WARNING : {// likewise, for warning
  2209. icon: "svg-sprite-action-symbol.svg#ic_report_problem_24px",
  2210. alertClass: "notif-alert-warn",
  2211. },
  2212. ERROR : {// likewise, for error
  2213. icon: "svg-sprite-action-symbol.svg#ic_highlight_off_24px",
  2214. alertClass: "notif-alert-err",
  2215. sticky: true
  2216. },
  2217. init : function() {
  2218. if (this.div==null) {
  2219. this.div = document.createElement("div");
  2220. YAHOO.util.Dom.setStyle(this.div,"opacity",0);
  2221. this.div.id="notification-bar";
  2222. document.body.insertBefore(this.div, document.body.firstElementChild);
  2223. var self = this;
  2224. this.div.onclick = function() {
  2225. self.hide();
  2226. };
  2227. } else {
  2228. this.div.innerHTML = "";
  2229. }
  2230. },
  2231. // cancel pending auto-hide timeout
  2232. clearTimeout : function() {
  2233. if (this.token)
  2234. window.clearTimeout(this.token);
  2235. this.token = null;
  2236. },
  2237. // hide the current notification bar, if it's displayed
  2238. hide : function () {
  2239. this.clearTimeout();
  2240. this.div.classList.remove("notif-alert-show");
  2241. this.div.classList.add("notif-alert-clear");
  2242. },
  2243. // show a notification bar
  2244. show : function (text,options) {
  2245. options = options || {};
  2246. this.init();
  2247. var icon = this.div.appendChild(document.createElement("div"));
  2248. icon.style.display = "inline-block";
  2249. if (options.iconColor || this.defaultIconColor) {
  2250. icon.style.color = options.iconColor || this.defaultIconColor;
  2251. }
  2252. var svg = icon.appendChild(document.createElementNS("http://www.w3.org/2000/svg", "svg"));
  2253. svg.setAttribute("viewBox", "0 0 24 24");
  2254. svg.setAttribute("focusable", "false");
  2255. svg.setAttribute("class", "svg-icon");
  2256. var use = svg.appendChild(document.createElementNS("http://www.w3.org/2000/svg","use"));
  2257. use.setAttribute("href", rootURL + "/images/material-icons/" + (options.icon || this.defaultIcon));
  2258. var message = this.div.appendChild(document.createElement("span"));
  2259. message.appendChild(document.createTextNode(text));
  2260. this.div.className=options.alertClass || this.defaultAlertClass;
  2261. this.div.classList.add("notif-alert-show");
  2262. this.clearTimeout();
  2263. var self = this;
  2264. if (!options.sticky)
  2265. this.token = window.setTimeout(function(){self.hide();},this.DELAY);
  2266. }
  2267. };