"
+ Util.escape(Functions.printThrowable(e)) +
@@ -137,9 +136,7 @@ public String renderHtml() {
if (req == null) { // being called from some other context
return message;
}
- // 1x16 spacer needed for IE since it doesn't support min-height
- return "
"
+ Util.escape(Functions.printThrowable(e)) +
@@ -272,9 +272,7 @@ public String renderHtml() {
if (req == null) { // being called from some other context
return message;
}
- // 1x16 spacer needed for IE since it doesn't support min-height
- return "
*/
DomNodeList domNodes = htmlPage.getDocumentElement().querySelectorAll("*");
- assertThat(domNodes, hasSize(5));
+ assertThat(domNodes, hasSize(4));
assertEquals("head", domNodes.get(0).getNodeName());
assertEquals("body", domNodes.get(1).getNodeName());
assertEquals("div", domNodes.get(2).getNodeName());
- assertEquals("img", domNodes.get(3).getNodeName());
- assertEquals("a", domNodes.get(4).getNodeName());
+ assertEquals("a", domNodes.get(3).getNodeName());
// only: ">
// the first double quote was escaped during creation (with the backslash)
String unquotedLabel = Label.parseExpression(label).getName();
- HtmlAnchor anchor = (HtmlAnchor) domNodes.get(4);
+ HtmlAnchor anchor = (HtmlAnchor) domNodes.get(3);
assertThat(anchor.getHrefAttribute(), containsString(Util.rawEncode(unquotedLabel)));
assertThat(responseContent, containsString("ok"));
diff --git a/test/src/test/java/lib/form/NumberTest.java b/test/src/test/java/lib/form/NumberTest.java
index 0bef86dbc26f..d12840495522 100644
--- a/test/src/test/java/lib/form/NumberTest.java
+++ b/test/src/test/java/lib/form/NumberTest.java
@@ -177,7 +177,7 @@ private String typeValueAndGetErrorMessage(HtmlInput input, String value) throws
input.reset(); // Remove the value that already in the
input.type(value); // Type value to
input.fireEvent(Event.TYPE_CHANGE); // The error message is triggered by change event
- return input.getParentNode().getNextSibling().getChildNodes().get(1).getChildNodes().get(0).getTextContent();
+ return input.getParentNode().getNextSibling().getTextContent();
}
diff --git a/war/src/main/less/base/style.less b/war/src/main/less/base/style.less
index b1b609dce842..7f7944ce9db9 100644
--- a/war/src/main/less/base/style.less
+++ b/war/src/main/less/base/style.less
@@ -542,64 +542,6 @@ div.behavior-loading {
padding: 0;
}
-
-/* ======================== error/warning message (mainly in the form.) Use them on block elements ======================== */
-.error {
- color: #c00;
- font-weight: bold;
- padding-left: 20px;
- min-height: 16px;
- line-height: 16px;
- background-image: url("../../images/svgs/error.svg");
- background-position: left top;
- background-repeat: no-repeat;
- background-size: 16px 16px;
-}
-
-.error-inline {
- color: #c00;
- font-weight: bold;
-}
-
-.warning {
- color: #c4a000;
- font-weight: bold;
- padding-left: 20px;
- min-height: 16px;
- line-height: 16px;
- background-image: url("../../images/svgs/warning.svg");
- background-position: left top;
- background-repeat: no-repeat;
- background-size: 16px 16px;
-}
-
-.warning-inline {
- color: #c4a000;
- font-weight: bold;
-}
-
-.info {
- position: relative;
- color: var(--text-color);
- font-weight: bold;
- min-height: 16px;
- padding-left: 30px;
-
- &::before {
- content: "";
- position: absolute;
- top: 0;
- left: 0;
- bottom: 0;
- width: 20px;
- background: currentColor;
- mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='ionicon' viewBox='0 0 512 512'%3E%3Ctitle%3EArrow Forward%3C/title%3E%3Cpath fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='32' d='M268 112l144 144-144 144M392 256H100'/%3E%3C/svg%3E");
- mask-position: center;
- mask-size: contain;
- mask-repeat: no-repeat;
- }
-}
-
.icon16x16 {
width: 16px;
height: 16px;
diff --git a/war/src/main/less/form/validation.less b/war/src/main/less/form/validation.less
new file mode 100644
index 000000000000..3d2a18c04d82
--- /dev/null
+++ b/war/src/main/less/form/validation.less
@@ -0,0 +1,84 @@
+.validation-error-area {
+ transition: var(--standard-transition);
+ opacity: 0;
+ height: 0;
+ overflow: hidden;
+}
+
+.validation-error-area--visible {
+ margin-top: 0.75rem;
+ opacity: 1;
+
+ & > * {
+ animation: animate-validation-error-area var(--standard-transition);
+ }
+}
+
+@keyframes animate-validation-error-area {
+ from {
+ opacity: 0;
+ }
+ to {
+ opacity: 1;
+ }
+}
+
+.error,
+.warning,
+.info {
+ position: relative;
+ padding-left: calc(22px + 0.4rem);
+ font-weight: 500;
+
+ &::before {
+ content: "";
+ position: absolute;
+ top: 0;
+ left: 0;
+ bottom: 0;
+ width: 22px;
+ height: 22px;
+ background-color: currentColor;
+ mask-position: top center;
+ mask-repeat: no-repeat;
+ mask-size: contain;
+ }
+}
+
+.ok {
+ color: var(--text-color-secondary);
+}
+
+.error {
+ color: var(--red);
+
+ &::before {
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='512' height='512' viewBox='0 0 512 512'%3E%3Ctitle%3Eionicons-v5-a%3C/title%3E%3Cpath d='M256,48C141.31,48,48,141.31,48,256s93.31,208,208,208,208-93.31,208-208S370.69,48,256,48Zm0,319.91a20,20,0,1,1,20-20A20,20,0,0,1,256,367.91Zm21.72-201.15-5.74,122a16,16,0,0,1-32,0l-5.74-121.94v-.05a21.74,21.74,0,1,1,43.44,0Z'/%3E%3C/svg%3E");
+ }
+}
+
+.error-inline {
+ color: var(--red);
+ font-weight: 500;
+}
+
+.warning {
+ color: var(--orange);
+
+ &::before {
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='512' height='512' viewBox='0 0 512 512'%3E%3Ctitle%3Eionicons-v5-r%3C/title%3E%3Cpath d='M449.07,399.08,278.64,82.58c-12.08-22.44-44.26-22.44-56.35,0L51.87,399.08A32,32,0,0,0,80,446.25H420.89A32,32,0,0,0,449.07,399.08Zm-198.6-1.83a20,20,0,1,1,20-20A20,20,0,0,1,250.47,397.25ZM272.19,196.1l-5.74,122a16,16,0,0,1-32,0l-5.74-121.95v0a21.73,21.73,0,0,1,21.5-22.69h.21a21.74,21.74,0,0,1,21.73,22.7Z'/%3E%3C/svg%3E");
+ }
+}
+
+.warning-inline {
+ color: var(--orange);
+ font-weight: 500;
+}
+
+.info {
+ color: var(--text-color);
+
+ &::before {
+ mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='ionicon' viewBox='0 0 512 512'%3E%3Ctitle%3EArrow Forward%3C/title%3E%3Cpath fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='32' d='M268 112l144 144-144 144M392 256H100'/%3E%3C/svg%3E");
+ }
+}
diff --git a/war/src/main/less/styles.less b/war/src/main/less/styles.less
index ea74d1e9e6b0..22772eb8756d 100644
--- a/war/src/main/less/styles.less
+++ b/war/src/main/less/styles.less
@@ -32,6 +32,7 @@ html {
@import './form/search';
@import './form/select';
@import './form/toggle-switch';
+@import './form/validation';
@import './modules/app-bar';
@import './modules/badges';
diff --git a/war/src/main/webapp/scripts/hudson-behavior.js b/war/src/main/webapp/scripts/hudson-behavior.js
index 1e94eb6f928e..c3aebaaa8b2e 100644
--- a/war/src/main/webapp/scripts/hudson-behavior.js
+++ b/war/src/main/webapp/scripts/hudson-behavior.js
@@ -232,7 +232,7 @@ var FormChecker = {
this.sendRequest(next.url, {
method : next.method,
onComplete : function(x) {
- applyErrorMessage(next.target, x);
+ updateValidationArea(next.target, x.responseText);
FormChecker.inProgress--;
FormChecker.schedule();
layoutUpdateCallback.call();
@@ -500,16 +500,53 @@ var tooltip;
// Behavior rules
//========================================================
// using tag names in CSS selector makes the processing faster
+
+
+/**
+ * Updates the validation area for a form element
+ * @param {HTMLElement} validationArea The validation area for a given form element
+ * @param {string} content The content to update the validation area with
+ */
+function updateValidationArea(validationArea, content) {
+ validationArea.classList.add("validation-error-area--visible");
+
+ if (content === "") {
+ validationArea.classList.remove("validation-error-area--visible");
+ validationArea.style.height = "0px";
+ validationArea.innerHTML = content;
+ } else {
+ // Only change content if different, causes an unnecessary animation otherwise
+ if (validationArea.innerHTML !== content) {
+ validationArea.innerHTML = content;
+ validationArea.style.height = validationArea.children[0].offsetHeight + "px";
+
+ // Only include the notice in the validation-error-area, move all other elements out
+ if (validationArea.children.length > 1) {
+ Array.from(validationArea.children).slice(1).forEach((element) => {
+ validationArea.after(element);
+ })
+ }
+
+ Behaviour.applySubtree(validationArea);
+ // For errors with additional details, apply the subtree to the expandable details pane
+ if (validationArea.nextElementSibling) {
+ Behaviour.applySubtree(validationArea.nextElementSibling);
+ }
+ }
+ }
+}
+
function registerValidator(e) {
// Retrieve the validation error area
- var tr = findFollowingTR(e, "validation-error-area");
+ var tr = e.closest(".jenkins-form-item").querySelector(".validation-error-area");
if (!tr) {
- console.warn("Couldn't find the expected parent element (.setting-main) for element", e)
+ console.warn("Couldn't find the expected validation element (.validation-error-area) for element",
+ e.closest(".jenkins-form-item"))
return;
}
// find the validation-error-area
- e.targetElement = tr.firstElementChild.nextSibling;
+ e.targetElement = tr;
e.targetUrl = function() {
var url = this.getAttribute("checkUrl");
@@ -545,19 +582,13 @@ function registerValidator(e) {
}
var checker = function() {
- var target = this.targetElement;
+ const validationArea = 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 = "
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.
An internal error occurred during form field validation (HTTP ${status}). Please reload the page and if the problem persists, ask the administrator for help.
`;
+ updateValidationArea(validationArea, status === 200 ? responseText : errorMessage);
}
});
}
@@ -584,22 +615,25 @@ function registerValidator(e) {
}
function registerRegexpValidator(e,regexp,message) {
- var tr = findFollowingTR(e, "validation-error-area");
+ var tr = e.closest(".jenkins-form-item").querySelector( ".validation-error-area");
if (!tr) {
- console.warn("Couldn't find the expected parent element (.setting-main) for element", e)
+ console.warn("Couldn't find the expected parent element (.setting-main) for element",
+ e.closest(".jenkins-form-item"))
return;
}
// find the validation-error-area
- e.targetElement = tr.firstElementChild.nextSibling;
+ e.targetElement = tr;
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 = "";
+ if (!set) {
+ updateValidationArea(this.targetElement, ``)
+ }
} else {
- this.targetElement.innerHTML = "
" + message + "
";
+ updateValidationArea(this.targetElement, `
${message}
`);
set = true;
}
return set;
@@ -613,13 +647,14 @@ function registerRegexpValidator(e,regexp,message) {
* @param e Input element
*/
function registerMinMaxValidator(e) {
- var tr = findFollowingTR(e, "validation-error-area");
+ var tr = e.closest(".jenkins-form-item").querySelector( ".validation-error-area");
if (!tr) {
- console.warn("Couldn't find the expected parent element (.setting-main) for element", e)
+ console.warn("Couldn't find the expected parent element (.setting-main) for element",
+ e.closest(".jenkins-form-item"))
return;
}
// find the validation-error-area
- e.targetElement = tr.firstElementChild.nextSibling;
+ e.targetElement = tr;
var checkMessage = e.getAttribute('checkMessage');
if (checkMessage) message = checkMessage;
var oldOnchange = e.onchange;
@@ -638,29 +673,35 @@ function registerMinMaxValidator(e) {
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 = "
This value should be between " + min + " and " + max + "
";
+ updateValidationArea(this.targetElement, `
This value should be between ${min} and ${max}
`);
set = true;
} else {
- if (!set) this.targetElement.innerHTML = ""; // The value is valid
+ if (!set) {
+ updateValidationArea(this.targetElement, ``)
+ }
}
}
} else if ((min !== null && isInteger(min)) && (max === null || !isInteger(max))) { // There is only 'min' available
if (parseInt(min) > parseInt(this.value)) {
- this.targetElement.innerHTML = "
This value should be larger than " + min + "
";
+ updateValidationArea(this.targetElement, `
This value should be larger than ${min}
`);
set = true;
} else {
- if (!set) this.targetElement.innerHTML = "";
+ if (!set) {
+ updateValidationArea(this.targetElement, ``)
+ }
}
} else if ((min === null || !isInteger(min)) && (max !== null && isInteger(max))) { // There is only 'max' available
if (parseInt(max) < parseInt(this.value)) {
- this.targetElement.innerHTML = "
This value should be less than " + max + "
";
+ updateValidationArea(this.targetElement, `
This value should be less than ${max}
`);
set = true;
} else {
- if (!set) this.targetElement.innerHTML = "";
+ if (!set) {
+ updateValidationArea(this.targetElement, ``)
+ }
}
}
}
@@ -2324,15 +2365,16 @@ function validateButton(checkUrl,paramList,button) {
}
});
- var spinner = $(button).up("DIV").next();
- var target = spinner.next();
+ var spinner = button.up("DIV").children[0];
+ var target = spinner.next().next();
spinner.style.display="block";
new Ajax.Request(checkUrl, {
parameters: parameters,
onComplete: function(rsp) {
spinner.style.display="none";
- applyErrorMessage(target, rsp);
+ target.innerHTML = ``;
+ updateValidationArea(target.children[0], rsp.responseText);
layoutUpdateCallback.call();
var s = rsp.getResponseHeader("script");
try {
@@ -2344,26 +2386,6 @@ function validateButton(checkUrl,paramList,button) {
});
}
-function applyErrorMessage(elt, rsp) {
- if (rsp.status == 200) {
- elt.innerHTML = rsp.responseText;
- } else {
- var id = 'valerr' + (iota++);
- elt.innerHTML = 'ERROR
' + rsp.responseText + '
';
- var error = document.getElementById('error-description'); // cf. oops.jelly
- if (error) {
- var div = document.getElementById(id);
- while (div.firstElementChild) {
- div.removeChild(div.firstElementChild);
- }
- div.appendChild(error);
- }
- }
- Behaviour.applySubtree(elt);
-}
-
// create a combobox.
// @param idOrField
// ID of the element that becomes a combobox, or the field itself.