/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

Large files are truncated click here to view the full 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");