/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
- /*
- * The MIT License
- *
- * Copyright (c) 2004-2010, Sun Microsystems, Inc., Kohsuke Kawaguchi,
- * Daniel Dyer, Yahoo! Inc., Alan Harder, InfraDNA, Inc.
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy
- * of this software and associated documentation files (the "Software"), to deal
- * in the Software without restriction, including without limitation the rights
- * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
- * copies of the Software, and to permit persons to whom the Software is
- * furnished to do so, subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in
- * all copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
- * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
- * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
- * THE SOFTWARE.
- */
- //
- //
- // JavaScript for Jenkins
- // See http://www.ibm.com/developerworks/web/library/wa-memleak/?ca=dgr-lnxw97JavascriptLeaks
- // for memory leak patterns and how to prevent them.
- //
- if (window.isRunAsTest) {
- // Disable postMessage when running in test mode (HtmlUnit).
- window.postMessage = false;
- }
- // create a new object whose prototype is the given object
- function object(o) {
- function F() {}
- F.prototype = o;
- return new F();
- }
- function TryEach(fn) {
- return function(name) {
- try {
- fn(name);
- } catch (e) {
- console.error(e);
- }
- }
- }
- /**
- * A function that returns false if the page is known to be invisible.
- */
- var isPageVisible = (function(){
- // @see https://developer.mozilla.org/en/DOM/Using_the_Page_Visibility_API
- // Set the name of the hidden property and the change event for visibility
- var hidden, visibilityChange;
- if (typeof document.hidden !== "undefined") {
- hidden = "hidden";
- visibilityChange = "visibilitychange";
- } else if (typeof document.mozHidden !== "undefined") {
- hidden = "mozHidden";
- visibilityChange = "mozvisibilitychange";
- } else if (typeof document.msHidden !== "undefined") {
- hidden = "msHidden";
- visibilityChange = "msvisibilitychange";
- } else if (typeof document.webkitHidden !== "undefined") {
- hidden = "webkitHidden";
- visibilityChange = "webkitvisibilitychange";
- }
- // By default, visibility set to true
- var pageIsVisible = true;
- // If the page is hidden, prevent any polling
- // if the page is shown, restore pollings
- function onVisibilityChange() {
- pageIsVisible = !document[hidden];
- }
- // Warn if the browser doesn't support addEventListener or the Page Visibility API
- if (typeof document.addEventListener !== "undefined" && typeof hidden !== "undefined") {
- // Init the value to the real state of the page
- pageIsVisible = !document[hidden];
- // Handle page visibility change
- document.addEventListener(visibilityChange, onVisibilityChange, false);
- }
- return function() {
- return pageIsVisible;
- }
- })();
- // id generator
- var iota = 0;
- // crumb information
- var crumb = {
- fieldName: null,
- value: null,
- init: function(crumbField, crumbValue) {
- if (crumbField=="") return; // layout.jelly passes in "" whereas it means null.
- this.fieldName = crumbField;
- this.value = crumbValue;
- },
- /**
- * Adds the crumb value into the given hash or array and returns it.
- */
- wrap: function(headers) {
- if (this.fieldName!=null) {
- if (headers instanceof Array)
- // TODO prototype.js only seems to interpret object
- headers.push(this.fieldName, this.value);
- else
- headers[this.fieldName]=this.value;
- }
- // TODO return value unused
- return headers;
- },
- /**
- * Puts a hidden input field to the form so that the form submission will have the crumb value
- */
- appendToForm : function(form) {
- if(this.fieldName==null) return; // noop
- var div = document.createElement("div");
- div.innerHTML = "<input type=hidden name='"+this.fieldName+"' value='"+this.value+"'>";
- form.appendChild(div);
- if (form.enctype == "multipart/form-data") {
- if (form.action.indexOf("?") != -1) {
- form.action = form.action+"&"+this.fieldName+"="+this.value;
- } else {
- form.action = form.action+"?"+this.fieldName+"="+this.value;
- }
- }
- }
- };
- (function initializeCrumb() {
- var extensionsAvailable = document.head.getAttribute('data-extensions-available');
- if (extensionsAvailable === 'true') {
- var crumbHeaderName = document.head.getAttribute('data-crumb-header');
- var crumbValue = document.head.getAttribute('data-crumb-value');
- if (crumbHeaderName && crumbValue) {
- crumb.init(crumbHeaderName, crumbValue);
- }
- }
- // else, the instance is starting, restarting, etc.
- })();
- var isRunAsTest = undefined;
- // Be careful, this variable does not include the absolute root URL as in Java part of Jenkins,
- // but the contextPath only, like /jenkins
- var rootURL = 'not-defined-yet';
- var resURL = 'not-defined-yet';
- (function initializeUnitTestAndURLs() {
- var dataUnitTest = document.head.getAttribute('data-unit-test');
- if (dataUnitTest !== null) {
- isRunAsTest = dataUnitTest === 'true';
- }
- var dataRootURL = document.head.getAttribute('data-rooturl');
- if (dataRootURL !== null) {
- rootURL = dataRootURL;
- }
- var dataResURL = document.head.getAttribute('data-resurl');
- if (dataResURL !== null) {
- resURL = dataResURL;
- }
- })();
- (function initializeYUIDebugLogReader(){
- Behaviour.addLoadEvent(function(){
- var logReaderElement = document.getElementById('yui-logreader');
- if (logReaderElement !== null) {
- var logReader = new YAHOO.widget.LogReader('yui-logreader');
- logReader.collapse();
- }
- });
- })();
- // Form check code
- //========================================================
- var FormChecker = {
- // pending requests
- queue : [],
- // conceptually boolean, but doing so create concurrency problem.
- // that is, during unit tests, the AJAX.send works synchronously, so
- // the onComplete happens before the send method returns. On a real environment,
- // more likely it's the other way around. So setting a boolean flag to true or false
- // won't work.
- inProgress : 0,
- /**
- * Schedules a form field check. Executions are serialized to reduce the bandwidth impact.
- *
- * @param url
- * Remote doXYZ URL that performs the check. Query string should include the field value.
- * @param method
- * HTTP method. GET or POST. I haven't confirmed specifics, but some browsers seem to cache GET requests.
- * @param target
- * HTML element whose innerHTML will be overwritten when the check is completed.
- */
- delayedCheck : function(url, method, target) {
- if(url==null || method==null || target==null)
- return; // don't know whether we should throw an exception or ignore this. some broken plugins have illegal parameters
- this.queue.push({url:url, method:method, target:target});
- this.schedule();
- },
- sendRequest : function(url, params) {
- if (params.method != "get") {
- var idx = url.indexOf('?');
- params.parameters = url.substring(idx + 1);
- url = url.substring(0, idx);
- }
- new Ajax.Request(url, params);
- },
- schedule : function() {
- if (this.inProgress>0) return;
- if (this.queue.length == 0) return;
- var next = this.queue.shift();
- this.sendRequest(next.url, {
- method : next.method,
- onComplete : function(x) {
- applyErrorMessage(next.target, x);
- FormChecker.inProgress--;
- FormChecker.schedule();
- layoutUpdateCallback.call();
- }
- });
- this.inProgress++;
- }
- }
- /**
- * Find the sibling (in the sense of the structured form submission) form item of the given name,
- * and returns that DOM node.
- *
- * @param {HTMLElement} e
- * @param {string} name
- * Name of the control to find. Can include "../../" etc in the prefix.
- * See @RelativePath.
- *
- * We assume that the name is normalized and doesn't contain any redundant component.
- * That is, ".." can only appear as prefix, and "foo/../bar" is not OK (because it can be reduced to "bar")
- */
- function findNearBy(e,name) {
- while (name.startsWith("../")) {
- name = name.substring(3);
- e = findFormParent(e,null,true);
- }
- // name="foo/bar/zot" -> prefixes=["bar","foo"] & name="zot"
- var prefixes = name.split("/");
- name = prefixes.pop();
- prefixes = prefixes.reverse();
- // does 'e' itself match the criteria?
- // as some plugins use the field name as a parameter value, instead of 'value'
- var p = findFormItem(e,name,function(e,filter) {
- return filter(e) ? e : null;
- });
- if (p!=null && prefixes.length==0) return p;
- var owner = findFormParent(e,null,true);
- function locate(iterator,e) {// keep finding elements until we find the good match
- while (true) {
- e = iterator(e,name);
- if (e==null) return null;
- // make sure this candidate element 'e' is in the right point in the hierarchy
- var p = e;
- for (var i=0; i<prefixes.length; i++) {
- p = findFormParent(p,null,true);
- if (p.getAttribute("name")!=prefixes[i])
- return null;
- }
- if (findFormParent(p,null,true)==owner)
- return e;
- }
- }
- return locate(findPreviousFormItem,e) || locate(findNextFormItem,e);
- }
- function controlValue(e) {
- if (e==null) return null;
- // compute the form validation value to be sent to the server
- var type = e.getAttribute("type");
- if(type!=null && type.toLowerCase()=="checkbox")
- return e.checked;
- return e.value;
- }
- function toValue(e) {
- return encodeURIComponent(controlValue(e));
- }
- /**
- * Builds a query string in a fluent API pattern.
- * @param {HTMLElement} owner
- * The 'this' control.
- */
- function qs(owner) {
- return {
- params : "",
- append : function(s) {
- if (this.params.length==0) this.params+='?';
- else this.params+='&';
- this.params += s;
- return this;
- },
- nearBy : function(name) {
- var e = findNearBy(owner,name);
- if (e==null) return this; // skip
- return this.append(Path.tail(name)+'='+toValue(e));
- },
- addThis : function() {
- return this.append("value="+toValue(owner));
- },
- toString : function() {
- return this.params;
- }
- };
- }
- // find the nearest ancestor node that has the given tag name
- function findAncestor(e, tagName) {
- do {
- e = e.parentNode;
- } while (e != null && e.tagName != tagName);
- return e;
- }
- function findAncestorClass(e, cssClass) {
- do {
- e = e.parentNode;
- } while (e != null && !Element.hasClassName(e,cssClass));
- return e;
- }
- function isTR(tr, nodeClass) {
- return tr.tagName == 'TR' || tr.classList.contains(nodeClass || 'tr') || tr.classList.contains('jenkins-form-item');
- }
- function findFollowingTR(node, className, nodeClass) {
- // identify the parent TR
- var tr = node;
- while (!isTR(tr, nodeClass)) {
- tr = tr.parentNode;
- if (!(tr instanceof Element))
- return null;
- }
- // then next TR that matches the CSS
- do {
- // Supports plugins with custom variants of <f:entry> that call
- // findFollowingTR(element, 'validation-error-area') and haven't migrated
- // to use querySelector
- if (className === 'validation-error-area' || className === 'help-area') {
- var queryChildren = tr.getElementsByClassName(className);
- if (queryChildren.length > 0 && (isTR(queryChildren[0]) || Element.hasClassName(queryChildren[0], className) ))
- return queryChildren[0];
- }
- tr = $(tr).next();
- } while (tr != null && (!isTR(tr) || !Element.hasClassName(tr,className)));
- return tr;
- }
- function findInFollowingTR(input, className) {
- var node = findFollowingTR(input, className);
- if (node.tagName == 'TR') {
- node = node.firstElementChild.nextSibling;
- } else {
- node = node.firstElementChild;
- }
- return node;
- }
- function find(src,filter,traversalF) {
- while(src!=null) {
- src = traversalF(src);
- if(src!=null && filter(src))
- return src;
- }
- return null;
- }
- /**
- * Traverses a form in the reverse document order starting from the given element (but excluding it),
- * until the given filter matches, or run out of an element.
- */
- function findPrevious(src,filter) {
- return find(src,filter,function (e) {
- var p = e.previousSibling;
- if(p==null) return e.parentNode;
- while(p.lastElementChild!=null)
- p = p.lastElementChild;
- return p;
- });
- }
- function findNext(src,filter) {
- return find(src,filter,function (e) {
- var n = e.nextSibling;
- if(n==null) return e.parentNode;
- while(n.firstElementChild!=null)
- n = n.firstElementChild;
- return n;
- });
- }
- function findFormItem(src,name,directionF) {
- var name2 = "_."+name; // handles <textbox field="..." /> notation silently
- return directionF(src,function(e){
- if (e.tagName == "INPUT" && e.type=="radio" && e.checked==true) {
- var r = 0;
- while (e.name.substring(r,r+8)=='removeme') //radio buttons have must be unique in repeatable blocks so name is prefixed
- r = e.name.indexOf('_',r+8)+1;
- return name == e.name.substring(r);
- }
- return (e.tagName=="INPUT" || e.tagName=="TEXTAREA" || e.tagName=="SELECT") && (e.name==name || e.name==name2); });
- }
- /**
- * Traverses a form in the reverse document order and finds an INPUT element that matches the given name.
- */
- function findPreviousFormItem(src,name) {
- return findFormItem(src,name,findPrevious);
- }
- function findNextFormItem(src,name) {
- return findFormItem(src,name,findNext);
- }
- // This method seems unused in the ecosystem, only grails-plugin was using it but it's blacklisted now
- /**
- * Parse HTML into DOM.
- */
- function parseHtml(html) {
- var c = document.createElement("div");
- c.innerHTML = html;
- return c.firstElementChild;
- }
- /**
- * Evaluates the script in global context.
- */
- function geval(script) {
- // execScript chokes on "" but eval doesn't, so we need to reject it first.
- if (script==null || script=="") return;
- // see http://perfectionkills.com/global-eval-what-are-the-options/
- // note that execScript cannot return value
- (this.execScript || eval)(script);
- }
- /**
- * Emulate the firing of an event.
- *
- * @param {HTMLElement} element
- * The element that will fire the event
- * @param {String} event
- * like 'change', 'blur', etc.
- */
- function fireEvent(element,event){
- if (document.createEvent) {
- // dispatch for firefox + others
- var evt = document.createEvent("HTMLEvents");
- evt.initEvent(event, true, true ); // event type,bubbling,cancelable
- return !element.dispatchEvent(evt);
- } else {
- // dispatch for IE
- var evt = document.createEventObject();
- return element.fireEvent('on'+event,evt)
- }
- }
- // shared tooltip object
- var tooltip;
- // Behavior rules
- //========================================================
- // using tag names in CSS selector makes the processing faster
- function registerValidator(e) {
- // Retrieve the validation error area
- var tr = findFollowingTR(e, "validation-error-area");
- if (!tr) {
- console.warn("Couldn't find the expected parent element (.setting-main) for element", e)
- return;
- }
- // find the validation-error-area
- e.targetElement = tr.firstElementChild.nextSibling;
- e.targetUrl = function() {
- var url = this.getAttribute("checkUrl");
- var depends = this.getAttribute("checkDependsOn");
- if (depends==null) {// legacy behaviour where checkUrl is a JavaScript
- try {
- return eval(url); // need access to 'this', so no 'geval'
- } catch (e) {
- if (window.console!=null) console.warn("Legacy checkUrl '" + url + "' is not valid JavaScript: "+e);
- if (window.YUI!=null) YUI.log("Legacy checkUrl '" + url + "' is not valid JavaScript: "+e,"warn");
- return url; // return plain url as fallback
- }
- } else {
- var q = qs(this).addThis();
- if (depends.length>0)
- depends.split(" ").each(TryEach(function (n) {
- q.nearBy(n);
- }));
- return url+ q.toString();
- }
- };
- var method = e.getAttribute("checkMethod") || "post";
- var url = e.targetUrl();
- try {
- FormChecker.delayedCheck(url, method, e.targetElement);
- } catch (x) {
- // this happens if the checkUrl refers to a non-existing element.
- // don't let this kill off the entire JavaScript
- YAHOO.log("Failed to register validation method: "+e.getAttribute("checkUrl")+" : "+e);
- return;
- }
- var checker = function() {
- var target = this.targetElement;
- FormChecker.sendRequest(this.targetUrl(), {
- method : method,
- onComplete : function(x) {
- if (x.status == 200) {
- // All FormValidation responses are 200
- target.innerHTML = x.responseText;
- } else {
- // Content is taken from FormValidation#_errorWithMarkup
- // TODO Add i18n support
- 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>";
- }
- Behaviour.applySubtree(target);
- }
- });
- }
- var oldOnchange = e.onchange;
- if(typeof oldOnchange=="function") {
- e.onchange = function() { checker.call(this); oldOnchange.call(this); }
- } else
- e.onchange = checker;
- var v = e.getAttribute("checkDependsOn");
- if (v) {
- v.split(" ").each(TryEach(function (name) {
- var c = findNearBy(e,name);
- if (c==null) {
- if (window.console!=null) console.warn("Unable to find nearby "+name);
- if (window.YUI!=null) YUI.log("Unable to find a nearby control of the name "+name,"warn")
- return;
- }
- $(c).observe("change",checker.bind(e));
- }));
- }
- e = null; // avoid memory leak
- }
- function registerRegexpValidator(e,regexp,message) {
- var tr = findFollowingTR(e, "validation-error-area");
- if (!tr) {
- console.warn("Couldn't find the expected parent element (.setting-main) for element", e)
- return;
- }
- // find the validation-error-area
- e.targetElement = tr.firstElementChild.nextSibling;
- var checkMessage = e.getAttribute('checkMessage');
- if (checkMessage) message = checkMessage;
- var oldOnchange = e.onchange;
- e.onchange = function() {
- var set = oldOnchange != null ? oldOnchange.call(this) : false;
- if (this.value.match(regexp)) {
- if (!set) this.targetElement.innerHTML = "<div/>";
- } else {
- this.targetElement.innerHTML = "<div class=error>" + message + "</div>";
- set = true;
- }
- return set;
- }
- e.onchange.call(e);
- e = null; // avoid memory leak
- }
- /**
- * Add a validator for number fields which contains 'min', 'max' attribute
- * @param e Input element
- */
- function registerMinMaxValidator(e) {
- var tr = findFollowingTR(e, "validation-error-area");
- if (!tr) {
- console.warn("Couldn't find the expected parent element (.setting-main) for element", e)
- return;
- }
- // find the validation-error-area
- e.targetElement = tr.firstElementChild.nextSibling;
- var checkMessage = e.getAttribute('checkMessage');
- if (checkMessage) message = checkMessage;
- var oldOnchange = e.onchange;
- e.onchange = function() {
- var set = oldOnchange != null ? oldOnchange.call(this) : false;
- const min = this.getAttribute('min');
- const max = this.getAttribute('max');
- function isInteger(str) {
- return str.match(/^-?\d*$/) !== null;
- }
- if (isInteger(this.value)) { // Ensure the value is an integer
- if ((min !== null && isInteger(min)) && (max !== null && isInteger(max))) { // Both min and max attributes are available
- if (min <= max) { // Add the validator if min <= max
- if (parseInt(min) > parseInt(this.value) || parseInt(this.value) > parseInt(max)) { // The value is out of range
- this.targetElement.innerHTML = "<div class=error>This value should be between " + min + " and " + max + "</div>";
- set = true;
- } else {
- if (!set) this.targetElement.innerHTML = "<div/>"; // The value is valid
- }
- }
- } else if ((min !== null && isInteger(min)) && (max === null || !isInteger(max))) { // There is only 'min' available
- if (parseInt(min) > parseInt(this.value)) {
- this.targetElement.innerHTML = "<div class=error>This value should be larger than " + min + "</div>";
- set = true;
- } else {
- if (!set) this.targetElement.innerHTML = "<div/>";
- }
- } else if ((min === null || !isInteger(min)) && (max !== null && isInteger(max))) { // There is only 'max' available
- if (parseInt(max) < parseInt(this.value)) {
- this.targetElement.innerHTML = "<div class=error>This value should be less than " + max + "</div>";
- set = true;
- } else {
- if (!set) this.targetElement.innerHTML = "<div/>";
- }
- }
- }
- return set;
- }
- e.onchange.call(e);
- e = null; // avoid memory leak
- }
- /**
- * Prevent user input 'e' or 'E' in <f:number>
- * @param event Input event
- */
- function preventInputEe(event) {
- if (event.which === 69 || event.which === 101) {
- event.preventDefault();
- }
- }
- /**
- * Wraps a <button> into YUI button.
- *
- * @param e
- * button element
- * @param onclick
- * onclick handler
- * @return
- * YUI Button widget.
- */
- function makeButton(e,onclick) {
- var h = e.onclick;
- var clsName = e.className;
- var n = e.name;
- var attributes = {};
- // YUI Button class interprets value attribute of <input> as HTML
- // similar to how the child nodes of a <button> are treated as HTML.
- // in standard HTML, we wouldn't expect the former case, yet here we are!
- if (e.tagName === 'INPUT') {
- attributes.label = e.value.escapeHTML();
- }
- var btn = new YAHOO.widget.Button(e, attributes);
- if(onclick!=null)
- btn.addListener("click",onclick);
- if(h!=null)
- btn.addListener("click",h);
- var be = btn.get("element");
- var classesSeparatedByWhitespace = clsName.split(' ');
- for (var i = 0; i < classesSeparatedByWhitespace.length; i++) {
- var singleClass = classesSeparatedByWhitespace[i];
- if (singleClass) {
- be.classList.add(singleClass);
- }
- }
- if(n) // copy the name
- be.setAttribute("name",n);
- // keep the data-* attributes from the source
- var length = e.attributes.length;
- for (var i = 0; i < length; i++) {
- var attribute = e.attributes[i];
- var attributeName = attribute.name;
- if (attributeName.startsWith('data-')) {
- btn._button.setAttribute(attributeName, attribute.value);
- }
- }
- return btn;
- }
- /*
- If we are inside 'to-be-removed' class, some HTML altering behaviors interact badly, because
- the behavior re-executes when the removed master copy gets reinserted later.
- */
- function isInsideRemovable(e) {
- return Element.ancestors(e).find(function(f){return f.hasClassName("to-be-removed");});
- }
- /**
- * Render the template captured by <l:renderOnDemand> at the element 'e' and replace 'e' by the content.
- *
- * @param {HTMLElement} e
- * The place holder element to be lazy-rendered.
- * @param {boolean} noBehaviour
- * if specified, skip the application of behaviour rule.
- */
- function renderOnDemand(e,callback,noBehaviour) {
- if (!e || !Element.hasClassName(e,"render-on-demand")) return;
- var proxy = eval(e.getAttribute("proxy"));
- proxy.render(function (t) {
- var contextTagName = e.parentNode.tagName;
- var c;
- if (contextTagName=="TBODY") {
- c = document.createElement("DIV");
- c.innerHTML = "<TABLE><TBODY>"+t.responseText+"</TBODY></TABLE>";
- c = c./*JENKINS-15494*/lastElementChild.firstElementChild;
- } else {
- c = document.createElement(contextTagName);
- c.innerHTML = t.responseText;
- }
- var elements = [];
- while (c.firstElementChild!=null) {
- var n = c.firstElementChild;
- e.parentNode.insertBefore(n,e);
- if (n.nodeType==1 && !noBehaviour)
- elements.push(n);
- }
- Element.remove(e);
- evalInnerHtmlScripts(t.responseText,function() {
- Behaviour.applySubtree(elements,true);
- if (callback) callback(t);
- });
- });
- }
- /**
- * Finds all the script tags
- */
- function evalInnerHtmlScripts(text,callback) {
- var q = [];
- var matchAll = new RegExp('<script([^>]*)>([\\S\\s]*?)<\/script>', 'img');
- var matchOne = new RegExp('<script([^>]*)>([\\S\\s]*?)<\/script>', 'im');
- var srcAttr = new RegExp('src=[\'\"]([^\'\"]+)[\'\"]','i');
- (text.match(matchAll)||[]).map(function(s) {
- var m = s.match(srcAttr);
- if (m) {
- q.push(function(cont) {
- loadScript(m[1],cont);
- });
- } else {
- q.push(function(cont) {
- geval(s.match(matchOne)[2]);
- cont();
- });
- }
- });
- q.push(callback);
- sequencer(q);
- }
- /**
- * Take an array of (typically async) functions and run them in a sequence.
- * Each of the function in the array takes one 'continuation' parameter, and upon the completion
- * of the function it needs to invoke "continuation()" to signal the execution of the next function.
- */
- function sequencer(fs) {
- var nullFunction = function() {}
- function next() {
- if (fs.length>0) {
- (fs.shift()||nullFunction)(next);
- }
- }
- return next();
- }
- function progressBarOnClick() {
- var href = this.getAttribute("href");
- if(href!=null) window.location = href;
- }
- function expandButton(e) {
- var link = e.target;
- while(!Element.hasClassName(link,"advancedLink"))
- link = link.parentNode;
- link.style.display = "none";
- $(link).next().style.display="block";
- layoutUpdateCallback.call();
- }
- function labelAttachPreviousOnClick() {
- var e = $(this).previous();
- while (e!=null) {
- if (e.tagName=="INPUT") {
- e.click();
- break;
- }
- e = e.previous();
- }
- }
- function helpButtonOnClick() {
- var tr = findFollowingTR(this, "help-area", "help-sibling") ||
- findFollowingTR(this, "help-area", "setting-help") ||
- findFollowingTR(this, "help-area");
- var div = $(tr).down();
- if (!div.hasClassName("help"))
- div = div.next().down();
- if (div.style.display != "block") {
- div.style.display = "block";
- // make it visible
- new Ajax.Request(this.getAttribute("helpURL"), {
- method : 'get',
- onSuccess : function(x) {
- var from = x.getResponseHeader("X-Plugin-From");
- div.innerHTML = x.responseText+(from?"<div class='from-plugin'>"+from+"</div>":"");
- layoutUpdateCallback.call();
- },
- onFailure : function(x) {
- div.innerHTML = "<b>ERROR</b>: Failed to load help file: " + x.statusText;
- layoutUpdateCallback.call();
- }
- });
- } else {
- div.style.display = "none";
- layoutUpdateCallback.call();
- }
- return false;
- }
- function isGeckoCommandKey() {
- return Prototype.Browser.Gecko && event.keyCode == 224
- }
- function isOperaCommandKey() {
- return Prototype.Browser.Opera && event.keyCode == 17
- }
- function isWebKitCommandKey() {
- return Prototype.Browser.WebKit && (event.keyCode == 91 || event.keyCode == 93)
- }
- function isCommandKey() {
- return isGeckoCommandKey() || isOperaCommandKey() || isWebKitCommandKey();
- }
- function isReturnKeyDown() {
- return event.type == 'keydown' && event.keyCode == Event.KEY_RETURN;
- }
- function getParentForm(element) {
- if (element == null) throw 'not found a parent form';
- if (element instanceof HTMLFormElement) return element;
- return getParentForm(element.parentNode);
- }
- // figure out the corresponding end marker
- function findEnd(e) {
- for( var depth=0; ; e=$(e).next()) {
- if(Element.hasClassName(e,"rowvg-start")) depth++;
- if(Element.hasClassName(e,"rowvg-end")) depth--;
- if(depth==0) return e;
- }
- }
- function makeOuterVisible(b) {
- this.outerVisible = b;
- this.updateVisibility();
- }
- function makeInnerVisible(b) {
- this.innerVisible = b;
- this.updateVisibility();
- }
- function updateVisibility() {
- var display = (this.outerVisible && this.innerVisible) ? "" : "none";
- for (var e=this.start; e!=this.end; e=$(e).next()) {
- if (e.rowVisibilityGroup && e!=this.start) {
- e.rowVisibilityGroup.makeOuterVisible(this.innerVisible);
- e = e.rowVisibilityGroup.end; // the above call updates visibility up to e.rowVisibilityGroup.end inclusive
- } else {
- e.style.display = display;
- }
- }
- layoutUpdateCallback.call();
- }
- function rowvgStartEachRow(recursive,f) {
- if (recursive) {
- for (var e=this.start; e!=this.end; e=$(e).next())
- f(e);
- } else {
- throw "not implemented yet";
- }
- }
- (function () {
- var p = 20;
- Behaviour.specify("BODY", "body", ++p, function() {
- tooltip = new YAHOO.widget.Tooltip("tt", {context:[], zindex:999});
- });
- Behaviour.specify("TABLE.sortable", "table-sortable", ++p, function(e) {// sortable table
- e.sortable = new Sortable.Sortable(e);
- });
- Behaviour.specify("TABLE.progress-bar", "table-progress-bar", ++p, function(e) { // progressBar.jelly
- e.onclick = progressBarOnClick;
- });
- Behaviour.specify("INPUT.expand-button", "input-expand-button", ++p, function(e) {
- makeButton(e, expandButton);
- });
- // <label> that doesn't use ID, so that it can be copied in <repeatable>
- Behaviour.specify("LABEL.attach-previous", "label-attach-previous", ++p, function(e) {
- e.onclick = labelAttachPreviousOnClick;
- });
- // form fields that are validated via AJAX call to the server
- // elements with this class should have two attributes 'checkUrl' that evaluates to the server URL.
- Behaviour.specify("INPUT.validated", "input-validated", ++p, registerValidator);
- Behaviour.specify("SELECT.validated", "select-validated", ++p, registerValidator);
- Behaviour.specify("TEXTAREA.validated", "textarea-validated", ++p, registerValidator);
- // validate required form values
- Behaviour.specify("INPUT.required", "input-required", ++p, function(e) { registerRegexpValidator(e,/./,"Field is required"); });
- // validate form values to be an integer
- Behaviour.specify("INPUT.number", "input-number", ++p, function(e) {
- e.addEventListener('keypress', preventInputEe)
- registerMinMaxValidator(e);
- registerRegexpValidator(e,/^((\-?\d+)|)$/,"Not an integer");
- });
- Behaviour.specify("INPUT.number-required", "input-number-required", ++p, function(e) {
- e.addEventListener('keypress', preventInputEe)
- registerMinMaxValidator(e);
- registerRegexpValidator(e,/^\-?(\d+)$/,"Not an integer");
- });
- Behaviour.specify("INPUT.non-negative-number-required", "input-non-negative-number-required", ++p, function(e) {
- e.addEventListener('keypress', preventInputEe)
- registerMinMaxValidator(e);
- registerRegexpValidator(e,/^\d+$/,"Not a non-negative integer");
- });
- Behaviour.specify("INPUT.positive-number", "input-positive-number", ++p, function(e) {
- e.addEventListener('keypress', preventInputEe)
- registerMinMaxValidator(e);
- registerRegexpValidator(e,/^(\d*[1-9]\d*|)$/,"Not a positive integer");
- });
- Behaviour.specify("INPUT.positive-number-required", "input-positive-number-required", ++p, function(e) {
- e.addEventListener('keypress', preventInputEe)
- registerMinMaxValidator(e);
- registerRegexpValidator(e,/^[1-9]\d*$/,"Not a positive integer");
- });
- Behaviour.specify("INPUT.auto-complete", "input-auto-complete", ++p, function(e) {// form field with auto-completion support
- // insert the auto-completion container
- var div = document.createElement("DIV");
- e.parentNode.insertBefore(div,$(e).next()||null);
- e.style.position = "relative"; // or else by default it's absolutely positioned, making "width:100%" break
- var ds = new YAHOO.util.XHRDataSource(e.getAttribute("autoCompleteUrl"));
- ds.responseType = YAHOO.util.XHRDataSource.TYPE_JSON;
- ds.responseSchema = {
- resultsList: "suggestions",
- fields: ["name"]
- };
-
- // Instantiate the AutoComplete
- var ac = new YAHOO.widget.AutoComplete(e, div, ds);
- ac.generateRequest = function(query) {
- return "?value=" + query;
- };
- ac.autoHighlight = false;
- ac.prehighlightClassName = "yui-ac-prehighlight";
- ac.animSpeed = 0;
- ac.formatResult = ac.formatEscapedResult;
- ac.useShadow = true;
- ac.autoSnapContainer = true;
- ac.delimChar = e.getAttribute("autoCompleteDelimChar");
- ac.doBeforeExpandContainer = function(textbox,container) {// adjust the width every time we show it
- container.style.width=textbox.clientWidth+"px";
- var Dom = YAHOO.util.Dom;
- Dom.setXY(container, [Dom.getX(textbox), Dom.getY(textbox) + textbox.offsetHeight] );
- return true;
- }
- });
- Behaviour.specify("A.jenkins-help-button", "a-jenkins-help-button", ++p, function(e) {
- e.onclick = helpButtonOnClick;
- e.tabIndex = 9999; // make help link unnavigable from keyboard
- e.parentNode.parentNode.addClassName('has-help');
- });
- // legacy class name
- Behaviour.specify("A.help-button", "a-help-button", ++p, function(e) {
- e.onclick = helpButtonOnClick;
- e.tabIndex = 9999; // make help link unnavigable from keyboard
- e.parentNode.parentNode.addClassName('has-help');
- });
- // Script Console : settings and shortcut key
- Behaviour.specify("TEXTAREA.script", "textarea-script", ++p, function(e) {
- (function() {
- var cmdKeyDown = false;
- var mode = e.getAttribute("script-mode") || "text/x-groovy";
- var readOnly = eval(e.getAttribute("script-readOnly")) || false;
-
- var w = CodeMirror.fromTextArea(e,{
- mode: mode,
- lineNumbers: true,
- matchBrackets: true,
- readOnly: readOnly,
- onKeyEvent: function (editor, event){
- function saveAndSubmit() {
- editor.save();
- getParentForm(e).submit();
- event.stop();
- }
- // Mac (Command + Enter)
- if (navigator.userAgent.indexOf('Mac') > -1) {
- if (event.type == 'keydown' && isCommandKey()) {
- cmdKeyDown = true;
- }
- if (event.type == 'keyup' && isCommandKey()) {
- cmdKeyDown = false;
- }
- if (cmdKeyDown && isReturnKeyDown()) {
- saveAndSubmit();
- return true;
- }
- // Windows, Linux (Ctrl + Enter)
- } else {
- if (event.ctrlKey && isReturnKeyDown()) {
- saveAndSubmit();
- return true;
- }
- }
- }
- }).getWrapperElement();
- w.setAttribute("style","border:1px solid black; margin-top: 1em; margin-bottom: 1em")
- })();
- });
- // deferred client-side clickable map.
- // this is useful where the generation of <map> element is time consuming
- Behaviour.specify("IMG[lazymap]", "img-lazymap-", ++p, function(e) {
- new Ajax.Request(
- e.getAttribute("lazymap"),
- {
- method : 'get',
- onSuccess : function(x) {
- var div = document.createElement("div");
- document.body.appendChild(div);
- div.innerHTML = x.responseText;
- var id = "map" + (iota++);
- div.firstElementChild.setAttribute("name", id);
- e.setAttribute("usemap", "#" + id);
- }
- });
- });
- // resizable text area
- Behaviour.specify("TEXTAREA", "textarea", ++p, function(textarea) {
- if(Element.hasClassName(textarea,"rich-editor")) {
- // rich HTML editor
- try {
- var editor = new YAHOO.widget.Editor(textarea, {
- dompath: true,
- animate: true,
- handleSubmit: true
- });
- // probably due to the timing issue, we need to let the editor know
- // that DOM is ready
- editor.DOMReady=true;
- editor.fireQueue();
- editor.render();
- layoutUpdateCallback.call();
- } catch(e) {
- alert(e);
- }
- return;
- }
- // CodeMirror inserts a wrapper element next to the textarea.
- // textarea.nextSibling may not be the handle.
- var handles = findElementsBySelector(textarea.parentNode, ".textarea-handle");
- if(handles.length != 1) return;
- var handle = handles[0];
- var Event = YAHOO.util.Event;
- function getCodemirrorScrollerOrTextarea(){
- return textarea.codemirrorObject ? textarea.codemirrorObject.getScrollerElement() : textarea;
- }
- handle.onmousedown = function(ev) {
- ev = Event.getEvent(ev);
- var s = getCodemirrorScrollerOrTextarea();
- var offset = s.offsetHeight-Event.getPageY(ev);
- s.style.opacity = 0.5;
- document.onmousemove = function(ev) {
- ev = Event.getEvent(ev);
- function max(a,b) { if(a<b) return b; else return a; }
- s.style.height = max(32, offset + Event.getPageY(ev)) + 'px';
- layoutUpdateCallback.call();
- return false;
- };
- document.onmouseup = function() {
- document.onmousemove = null;
- document.onmouseup = null;
- var s = getCodemirrorScrollerOrTextarea();
- s.style.opacity = 1;
- }
- };
- handle.ondblclick = function() {
- var s = getCodemirrorScrollerOrTextarea();
- s.style.height = "1px"; // To get actual height of the textbox, shrink it and show its scrollbar
- s.style.height = s.scrollHeight + 'px';
- }
- });
- // structured form submission
- Behaviour.specify("FORM", "form", ++p, function(form) {
- crumb.appendToForm(form);
- if(Element.hasClassName(form, "no-json"))
- return;
- // add the hidden 'json' input field, which receives the form structure in JSON
- var div = document.createElement("div");
- div.innerHTML = "<input type=hidden name=json value=init>";
- form.appendChild(div);
- var oldOnsubmit = form.onsubmit;
- if (typeof oldOnsubmit == "function") {
- form.onsubmit = function() { return buildFormTree(this) && oldOnsubmit.call(this); }
- } else {
- form.onsubmit = function() { return buildFormTree(this); };
- }
- form = null; // memory leak prevention
- });
- // hook up tooltip.
- // add nodismiss="" if you'd like to display the tooltip forever as long as the mouse is on the element.
- Behaviour.specify("[tooltip]", "-tooltip-", ++p, function(e) {
- applyTooltip(e,e.getAttribute("tooltip"));
- });
- Behaviour.specify("INPUT.submit-button", "input-submit-button", ++p, function(e) {
- makeButton(e);
- });
- Behaviour.specify("INPUT.yui-button", "input-yui-button", ++p, function(e) {
- makeButton(e);
- });
- 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
- // set start.ref to checkbox in preparation of row-set-end processing
- var checkbox = e.down().down();
- e.setAttribute("ref", checkbox.id = "cb"+(iota++));
- });
- // see RowVisibilityGroupTest
- Behaviour.specify("TR.rowvg-start,DIV.tr.rowvg-start", "tr-rowvg-start-div-tr-rowvg-start", ++p, function(e) {
- e.rowVisibilityGroup = {
- outerVisible: true,
- innerVisible: true,
- /**
- * TR that marks the beginning of this visibility group.
- */
- start: e,
- /**
- * TR that marks the end of this visibility group.
- */
- end: findEnd(e),
- /**
- * Considers the visibility of the row group from the point of view of outside.
- * If you think of a row group like a logical DOM node, this is akin to its .style.display.
- */
- makeOuterVisible: makeOuterVisible,
- /**
- * Considers the visibility of the rows in this row group. Since all the rows in a rowvg
- * shares the single visibility, this just needs to be one boolean, as opposed to many.
- *
- * If you think of a row group like a logical DOM node, this is akin to its children's .style.display.
- */
- makeInnerVisible: makeInnerVisible,
- /**
- * Based on innerVisible and outerVisible, update the relevant rows' actual CSS display attribute.
- */
- updateVisibility: updateVisibility,
- /**
- * Enumerate each row and pass that to the given function.
- *
- * @param {boolean} recursive
- * If true, this visits all the rows from nested visibility groups.
- */
- eachRow: rowvgStartEachRow
- };
- });
- 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
- // figure out the corresponding start block
- e = $(e);
- var end = e;
- for( var depth=0; ; e=e.previous()) {
- if(e.hasClassName("row-set-end")) depth++;
- if(e.hasClassName("row-set-start")) depth--;
- if(depth==0) break;
- }
- var start = e;
- // @ref on start refers to the ID of the element that controls the JSON object created from these rows
- // if we don't find it, turn the start node into the governing node (thus the end result is that you
- // created an intermediate JSON object that's always on.)
- var ref = start.getAttribute("ref");
- if(ref==null)
- start.id = ref = "rowSetStart"+(iota++);
- applyNameRef(start,end,ref);
- });
- 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
- // this is suffixed by a pointless string so that two processing for optional-block-start
- // can sandwich row-set-end
- // this requires "TR.row-set-end" to mark rows
- var checkbox = e.down().down();
- updateOptionalBlock(checkbox,false);
- });
- // image that shows [+] or [-], with hover effect.
- // oncollapsed and onexpanded will be called when the button is triggered.
- Behaviour.specify("IMG.fold-control", "img-fold-control", ++p, function(e) {
- function changeTo(e,img) {
- var src = e.src;
- e.src = src.substring(0,src.lastIndexOf('/'))+"/"+e.getAttribute("state")+img;
- }
- e.onmouseover = function() {
- changeTo(this,"-hover.png");
- };
- e.onmouseout = function() {
- changeTo(this,".png");
- };
- e.parentNode.onclick = function(event) {
- var e = this.firstElementChild;
- var s = e.getAttribute("state");
- if(s=="plus") {
- e.setAttribute("state","minus");
- if(e.onexpanded) e.onexpanded();
- } else {
- e.setAttribute("state","plus");
- if(e.oncollapsed) e.oncollapsed();
- }
- changeTo(e,"-hover.png");
- YAHOO.util.Event.stopEvent(event);
- return false;
- };
- e = null; // memory leak prevention
- });
- // editableComboBox.jelly
- Behaviour.specify("INPUT.combobox", "input-combobox", ++p, function(c) {
- // Next element after <input class="combobox"/> should be <div class="combobox-values">
- var vdiv = $(c).next();
- if (vdiv.hasClassName("combobox-values")) {
- createComboBox(c, function() {
- return vdiv.childElements().collect(function(value) {
- return value.getAttribute('value');
- });
- });
- }
- });
- // dropdownList.jelly
- Behaviour.specify("SELECT.dropdownList", "select-dropdownlist", ++p, function(e) {
- if(isInsideRemovable(e)) return;
- var subForms = [];
- var start = findInFollowingTR(e, 'dropdownList-container'), end;
- do { start = start.firstElementChild; } while (start && !isTR(start));
- if (start && !Element.hasClassName(start,'dropdownList-start'))
- start = findFollowingTR(start, 'dropdownList-start');
- while (start != null) {
- subForms.push(start);
- start = findFollowingTR(start, 'dropdownList-start');
- }
- // control visibility
- function updateDropDownList() {
- for (var i = 0; i < subForms.length; i++) {
- var show = e.selectedIndex == i;
- var f = $(subForms[i]);
- if (show) renderOnDemand(f.next());
- f.rowVisibilityGroup.makeInnerVisible(show);
- // TODO: this is actually incorrect in the general case if nested vg uses field-disabled
- // so far dropdownList doesn't create such a situation.
- f.rowVisibilityGroup.eachRow(true, show?function(e) {
- e.removeAttribute("field-disabled");
- } : function(e) {
- e.setAttribute("field-disabled","true");
- });
- }
- }
- e.onchange = updateDropDownList;
- updateDropDownList();
- });
- Behaviour.specify("A.showDetails", "a-showdetails", ++p, function(e) {
- e.onclick = function() {
- this.style.display = 'none';
- $(this).next().style.display = 'block';
- layoutUpdateCallback.call();
- return false;
- };
- e = null; // avoid memory leak
- });
- Behaviour.specify("DIV.behavior-loading", "div-behavior-loading", ++p, function(e) {
- e.style.display = 'none';
- });
- Behaviour.specify(".button-with-dropdown", "-button-with-dropdown", ++p, function (e) {
- new YAHOO.widget.Button(e, { type: "menu", menu: $(e).next() });
- });
- Behaviour.specify(".track-mouse", "-track-mouse", ++p, function (element) {
- var DOM = YAHOO.util.Dom;
- $(element).observe("mouseenter",function () {
- element.addClassName("mouseover");
- var mousemoveTracker = function (event) {
- var elementRegion = DOM.getRegion(element);
- if (event.x < elementRegion.left || event.x > elementRegion.right ||
- event.y < elementRegion.top || event.y > elementRegion.bottom) {
- element.removeClassName("mouseover");…