mirror of https://github.com/jenkinsci/jenkins.git
Replace YUI tooltips with Tippy.js (#6408)
Co-authored-by: Alexander Brandes <brandes.alexander@web.de> Co-authored-by: Yaroslav <91559310+yaroslavafenkin@users.noreply.github.com> Co-authored-by: Daniel Beck <1831569+daniel-beck@users.noreply.github.com> Co-authored-by: Kevin Guerroudj <91883215+Kevin-CB@users.noreply.github.com> Co-authored-by: Tim Jacomb <timjacomb1+github@gmail.com> Co-authored-by: Tim Jacomb <21194782+timja@users.noreply.github.com> Co-authored-by: Alexander Brandes <mc.cache@web.de> Co-authored-by: Daniel Beck <daniel-beck@users.noreply.github.com> Co-authored-by: Basil Crow <me@basilcrow.com> Co-authored-by: Tim Jacomb <timjacomb1@gmail.com>
This commit is contained in:
parent
86786b035c
commit
221ff946b3
|
@ -85,7 +85,7 @@ public class IconSet {
|
|||
|
||||
// for Jelly
|
||||
@Restricted(NoExternalUse.class)
|
||||
public static String getSymbol(String name, String title, String tooltip, String classes, String pluginName, String id) {
|
||||
public static String getSymbol(String name, String title, String tooltip, String htmlTooltip, String classes, String pluginName, String id) {
|
||||
String translatedName = cleanName(name);
|
||||
|
||||
String identifier = Util.fixEmpty(pluginName) == null ? "core" : pluginName;
|
||||
|
@ -95,10 +95,14 @@ public class IconSet {
|
|||
String symbol = symbolsForLookup.get(translatedName);
|
||||
symbol = symbol.replaceAll("(class=\").*?(\")", "$1$2");
|
||||
symbol = symbol.replaceAll("(tooltip=\").*?(\")", "");
|
||||
symbol = symbol.replaceAll("(data-html-tooltip=\").*?(\")", "");
|
||||
symbol = symbol.replaceAll("(id=\").*?(\")", "");
|
||||
if (!tooltip.isEmpty()) {
|
||||
if (!tooltip.isEmpty() && htmlTooltip.isEmpty()) {
|
||||
symbol = symbol.replaceAll("<svg", "<svg tooltip=\"" + Functions.htmlAttributeEscape(tooltip) + "\"");
|
||||
}
|
||||
if (!htmlTooltip.isEmpty()) {
|
||||
symbol = symbol.replaceAll("<svg", "<svg data-html-tooltip=\"" + Functions.htmlAttributeEscape(htmlTooltip) + "\"");
|
||||
}
|
||||
if (!id.isEmpty()) {
|
||||
symbol = symbol.replaceAll("<svg", "<svg id=\"" + Functions.htmlAttributeEscape(id) + "\"");
|
||||
}
|
||||
|
@ -124,10 +128,14 @@ public class IconSet {
|
|||
symbol = symbol.replaceAll("(<title>).*(</title>)", "$1$2");
|
||||
symbol = symbol.replaceAll("(class=\").*?(\")", "$1$2");
|
||||
symbol = symbol.replaceAll("(tooltip=\").*?(\")", "$1$2");
|
||||
symbol = symbol.replaceAll("(data-html-tooltip=\").*?(\")", "$1$2");
|
||||
symbol = symbol.replaceAll("(id=\").*?(\")", "");
|
||||
if (!tooltip.isEmpty()) {
|
||||
if (!tooltip.isEmpty() && htmlTooltip.isEmpty()) {
|
||||
symbol = symbol.replaceAll("<svg", "<svg tooltip=\"" + Functions.htmlAttributeEscape(tooltip) + "\"");
|
||||
}
|
||||
if (!htmlTooltip.isEmpty()) {
|
||||
symbol = symbol.replaceAll("<svg", "<svg data-html-tooltip=\"" + Functions.htmlAttributeEscape(htmlTooltip) + "\"");
|
||||
}
|
||||
if (!id.isEmpty()) {
|
||||
symbol = symbol.replaceAll("<svg", "<svg id=\"" + Functions.htmlAttributeEscape(id) + "\"");
|
||||
}
|
||||
|
|
|
@ -23,4 +23,4 @@ THE SOFTWARE.
|
|||
-->
|
||||
|
||||
<?jelly escape-by-default='true'?>
|
||||
<l:icon class="icon-lock icon-sm" tooltip="${%Keep this build forever}:<br/>${h.xmlEscape(build.whyKeepLog)}" xmlns:l="/lib/layout"/>
|
||||
<l:icon class="icon-lock icon-sm" tooltip="${%Keep this build forever}:\n${build.whyKeepLog}" xmlns:l="/lib/layout"/>
|
||||
|
|
|
@ -25,6 +25,6 @@ THE SOFTWARE.
|
|||
<?jelly escape-by-default='true'?>
|
||||
<j:if xmlns:j="jelly:core" xmlns:l="/lib/layout" test="${it.isTagged()}">
|
||||
<a href="${rootURL}/${it.run.url}tagBuild/">
|
||||
<l:icon class="icon-save icon-sm" tooltip="${h.escape(it.tooltip)}"/>
|
||||
<l:icon class="icon-save icon-sm" tooltip="${it.tooltip}"/>
|
||||
</a>
|
||||
</j:if>
|
|
@ -39,7 +39,7 @@ THE SOFTWARE.
|
|||
</j:otherwise>
|
||||
</j:choose>
|
||||
<j:set var="isQueued" value="${app.queue.contains(job)}"/>
|
||||
<a id="${id}" tooltip="${h.htmlAttributeEscape(title)}" class="jenkins-table__button jenkins-!-build-color" href="${href}">
|
||||
<a id="${id}" tooltip="${title}" class="jenkins-table__button jenkins-!-build-color" href="${href}">
|
||||
<span class="${isQueued ? 'pulse-animation': ''}">
|
||||
<l:icon src="symbol-play" />
|
||||
</span>
|
||||
|
|
|
@ -38,7 +38,9 @@ THE SOFTWARE.
|
|||
<td class="build-row-cell">
|
||||
<div class="pane build-name">
|
||||
<div class="build-icon">
|
||||
<a class="build-status-link" href="${link}console"><l:icon alt="${build.iconColor.description} > ${%Console Output}" class="${build.buildStatusIconClassName} icon-sm" tooltip="${build.iconColor.description} > ${%Console Output}"/></a>
|
||||
<a class="build-status-link" href="${link}console" tooltip="${build.iconColor.description} > ${%Console Output}">
|
||||
<l:icon class="${build.buildStatusIconClassName} icon-sm" />
|
||||
</a>
|
||||
</div>
|
||||
<a class="model-link inside build-link display-name" update-parent-class=".build-row" href="${link}">${build.displayName}</a>
|
||||
</div>
|
||||
|
|
|
@ -58,7 +58,7 @@ THE SOFTWARE.
|
|||
</j:choose>
|
||||
<j:if test="${!item.params.isEmpty()}">
|
||||
<div style="float:right;margin-right:10px;">
|
||||
<a href="#" tooltip="Build Parameters:${h.escape(item.params)}"><l:icon class="icon-notepad icon-sm" /></a>
|
||||
<a href="#" tooltip="Build Parameters:${item.params}"><l:icon class="icon-notepad icon-sm" /></a>
|
||||
</div>
|
||||
</j:if>
|
||||
</div>
|
||||
|
|
|
@ -32,7 +32,7 @@ THE SOFTWARE.
|
|||
</st:attribute>
|
||||
</st:documentation>
|
||||
<st:adjunct includes="lib.form.repeatable.repeatable"/>
|
||||
<button tooltip="${h.xmlEscape(attrs.value ?: '%Delete')}" class="repeatable-delete danger" type="button">
|
||||
<button tooltip="${attrs.value ?: '%Delete'}" class="repeatable-delete danger" type="button">
|
||||
<l:icon src="symbol-close" />
|
||||
</button>
|
||||
</j:jelly>
|
||||
|
|
|
@ -38,12 +38,40 @@ THE SOFTWARE.
|
|||
<x:element name="${useTdElement!=null?'td':'div'}">
|
||||
<x:attribute name="data">${buildHealth.score}</x:attribute>
|
||||
<x:attribute name="class">jenkins-table__cell--tight jenkins-table__icon healthReport</x:attribute>
|
||||
<j:if test="${!empty(healthReports)}">
|
||||
<x:attribute name="data-html-tooltip">
|
||||
<div class="jenkins-tooltip--table-wrapper">
|
||||
<table class="jenkins-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="jenkins-!-padding-left-0" align="center">W</th>
|
||||
<th align="left">${%Description}</th>
|
||||
<th align="right">%</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<j:forEach var="rpt" items="${healthReports}">
|
||||
<tr>
|
||||
<td align="left" class="jenkins-table__cell--tight jenkins-table__icon">
|
||||
<div class="jenkins-table__cell__button-wrapper">
|
||||
<l:icon src="symbol-weather-${buildHealth.iconClassName}" />
|
||||
</div>
|
||||
</td>
|
||||
<td align="left">${rpt.localizableDescription}</td>
|
||||
<td align="right">${rpt.score}</td>
|
||||
</tr>
|
||||
</j:forEach>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</x:attribute>
|
||||
</j:if>
|
||||
<j:if test="${buildHealth!=null}">
|
||||
<div class="jenkins-table__cell__button-wrapper">
|
||||
<j:choose>
|
||||
<j:when test="${!empty(healthReports)}">
|
||||
<a class="build-health-link jenkins-table__button" href="${empty(link)?'#':link}" style="${attrs.style}">
|
||||
<l:icon src="symbol-weather-${buildHealth.iconClassName}" />
|
||||
<l:icon src="symbol-weather-${buildHealth.iconClassName}" />
|
||||
</a>
|
||||
</j:when>
|
||||
<j:otherwise>
|
||||
|
@ -52,31 +80,5 @@ THE SOFTWARE.
|
|||
</j:choose>
|
||||
</div>
|
||||
</j:if>
|
||||
<j:if test="${!empty(healthReports)}">
|
||||
<div class="jenkins-tooltip healthReportDetails">
|
||||
<table class="jenkins-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="jenkins-!-padding-left-0" align="center">W</th>
|
||||
<th align="left">${%Description}</th>
|
||||
<th align="right">%</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<j:forEach var="rpt" items="${healthReports}">
|
||||
<tr>
|
||||
<td align="left" class="jenkins-table__cell--tight jenkins-table__icon">
|
||||
<div class="jenkins-table__cell__button-wrapper">
|
||||
<l:icon src="symbol-weather-${buildHealth.iconClassName}" />
|
||||
</div>
|
||||
</td>
|
||||
<td align="left">${rpt.localizableDescription}</td>
|
||||
<td align="right">${rpt.score}</td>
|
||||
</tr>
|
||||
</j:forEach>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</j:if>
|
||||
</x:element>
|
||||
</j:jelly>
|
||||
|
|
|
@ -74,7 +74,7 @@ THE SOFTWARE.
|
|||
<j:set var="stuck" value="${item.isStuck()}"/>
|
||||
<j:choose>
|
||||
<j:when test="${h.hasPermission(item.task,item.task.READ)}">
|
||||
<a href="${rootURL}/${item.task.url}" class="model-link inside tl-tr" tooltip="${h.escape(item.causesDescription)}${h.escape(item.why)}${h.escape(item.params)}<br>${%WaitingFor(item.inQueueForString)}">
|
||||
<a href="${rootURL}/${item.task.url}" class="model-link inside tl-tr" tooltip="${item.causesDescription} ${item.why} ${item.params} \n ${%WaitingFor(item.inQueueForString)}">
|
||||
<l:breakable value="${item.task.fullDisplayName}"/>
|
||||
</a>
|
||||
<!-- TODO include estimated number as in BuildHistoryWidget/entries.jelly if possible -->
|
||||
|
|
|
@ -19,7 +19,7 @@
|
|||
<j:if test="${attrs.iconSize != null}">
|
||||
<j:set var="iconSize" value="icon-${iconSize}"/>
|
||||
</j:if>
|
||||
<l:icon tooltip="${h.htmlAttributeEscape(tooltip)}"
|
||||
<l:icon tooltip="${tooltip}"
|
||||
class="${class} ${iconSize}"
|
||||
src="symbol-help-circle" />
|
||||
</j:jelly>
|
||||
|
|
|
@ -43,12 +43,13 @@ THE SOFTWARE.
|
|||
</st:attribute>
|
||||
|
||||
<st:attribute name="onclick" deprecated="true">onclick handler. Deprecated; assign an ID and look up the element that way to attach event handlers.</st:attribute>
|
||||
<st:attribute name="title" deprecated="true">title, deprecated use tooltip instead, but beware of its support for HTML</st:attribute>
|
||||
<st:attribute name="title" deprecated="true">title, deprecated use tooltip instead, or htmlTooltip if you intend to pass HTML.</st:attribute>
|
||||
<st:attribute name="style">style</st:attribute>
|
||||
<st:attribute name="tooltip">
|
||||
tooltip (supports HTML for PNG and symbol icons).
|
||||
Make sure to call h.htmlAttributeEscape on all user-specified parts of the value to prevent cross-site scripting.
|
||||
Icons based on classic (non-symbol) SVG do not support HTML tooltips due to how SECURITY-1955 was fixed in Jenkins 2.252 and 2.235.4, but since such icons can be upgraded to symbols, it is important to still escape user-specified parts of the text (resulting in double escaping while the icon is based on classic SVG).</st:attribute>
|
||||
Adds a tooltip to the icon, ignores HTML except 'br' tags (but '\n' should be preferred for line breaks).</st:attribute>
|
||||
<st:attribute name="htmlTooltip">
|
||||
Tooltip but with HTML support. Make sure to call h.htmlAttributeEscape on all user-specified parts of the value to prevent cross-site scripting.
|
||||
Use 'tooltip' if you don't need to pass HTML.</st:attribute>
|
||||
<st:attribute name="alt">alt, adds invisible text suitable for screen-readers for symbols, sets the alt attribute for normal images</st:attribute>
|
||||
</st:documentation>
|
||||
|
||||
|
@ -71,6 +72,7 @@ THE SOFTWARE.
|
|||
<j:arg value="${iconSrc.substring(7)}" />
|
||||
<j:arg value="${alt ?: ''}" />
|
||||
<j:arg value="${attrs.tooltip ?: ''}" />
|
||||
<j:arg value="${attrs.htmlTooltip ?: ''}" />
|
||||
<j:arg value="${attrs.class ?: iconMetadata.classSpec ?: ''}" />
|
||||
<j:arg value="${h.extractPluginNameFromIconSrc(iconSrc)}" />
|
||||
<j:arg value="${attrs.id ?: ''}" />
|
||||
|
@ -96,7 +98,7 @@ THE SOFTWARE.
|
|||
|
||||
<j:otherwise>
|
||||
<img class="${attrs.class}" src="${iconSrc}" style="${imgStyle}" title="${attrs.title}" height="${attrs.height}"
|
||||
alt="${attrs.alt}" width="${attrs.width}" onclick="${attrs.onclick}" tooltip="${attrs.tooltip}" id="${attrs.id}" />
|
||||
alt="${attrs.alt}" width="${attrs.width}" onclick="${attrs.onclick}" tooltip="${attrs.tooltip}" data-html-tooltip="${attrs.htmlTooltip}" id="${attrs.id}" />
|
||||
</j:otherwise>
|
||||
</j:choose>
|
||||
</j:otherwise>
|
||||
|
|
|
@ -42,7 +42,7 @@
|
|||
style="${attrs.style}"
|
||||
onclick="${attrs.onclick}"
|
||||
id="${attrs.id}"
|
||||
tooltip="${attrs.tooltip != null ? h.xmlEscape(attrs.tooltip) : null}">
|
||||
tooltip="${attrs.tooltip != null ? attrs.tooltip : null}">
|
||||
|
||||
<j:choose>
|
||||
<j:when test="${attrs.href != null}">
|
||||
|
|
|
@ -19,12 +19,12 @@ public class IconSetJenkins68805Test {
|
|||
@Issue("JENKINS-68805")
|
||||
void getSymbol_notSettingTooltipDoesntAddTooltipAttribute_evenWithAmpersand() {
|
||||
// cache a symbol with tooltip containing ampersand:
|
||||
String symbolWithTooltip = IconSet.getSymbol("download", "Title", "With&Ampersand", "class1 class2", "", "id");
|
||||
String symbolWithTooltip = IconSet.getSymbol("download", "Title", "With&Ampersand", "", "class1 class2", "", "id");
|
||||
assertThat(symbolWithTooltip, containsString("tooltip"));
|
||||
assertThat(symbolWithTooltip, containsString("With&"));
|
||||
|
||||
// Same symbol, no tooltip
|
||||
String symbolWithoutTooltip = IconSet.getSymbol("download", "Title", "", "class1 class2", "", "id");
|
||||
String symbolWithoutTooltip = IconSet.getSymbol("download", "Title", "", "", "class1 class2", "", "id");
|
||||
|
||||
assertThat(symbolWithoutTooltip, not(containsString("tooltip")));
|
||||
}
|
||||
|
|
|
@ -21,7 +21,7 @@ public class IconSetTest {
|
|||
|
||||
@Test
|
||||
void getSymbol() {
|
||||
String symbol = IconSet.getSymbol("download", "Title", "Tooltip", "class1 class2", "", "id");
|
||||
String symbol = IconSet.getSymbol("download", "Title", "Tooltip", "", "class1 class2", "", "id");
|
||||
|
||||
assertThat(symbol, containsString("<span class=\"jenkins-visually-hidden\">Title</span>"));
|
||||
assertThat(symbol, containsString("tooltip=\"Tooltip\""));
|
||||
|
@ -31,8 +31,8 @@ public class IconSetTest {
|
|||
|
||||
@Test
|
||||
void getSymbol_cachedSymbolDoesntReturnAttributes() {
|
||||
IconSet.getSymbol("download", "Title", "Tooltip", "class1 class2", "", "id");
|
||||
String symbol = IconSet.getSymbol("download", "", "", "", "", "");
|
||||
IconSet.getSymbol("download", "Title", "Tooltip", "", "class1 class2", "", "id");
|
||||
String symbol = IconSet.getSymbol("download", "", "", "", "", "", "");
|
||||
|
||||
assertThat(symbol, not(containsString("<span class=\"jenkins-visually-hidden\">Title</span>")));
|
||||
assertThat(symbol, not(containsString("tooltip=\"Tooltip\"")));
|
||||
|
@ -43,8 +43,8 @@ public class IconSetTest {
|
|||
|
||||
@Test
|
||||
void getSymbol_cachedSymbolAllowsSettingAllAttributes() {
|
||||
IconSet.getSymbol("download", "Title", "Tooltip", "class1 class2", "", "id");
|
||||
String symbol = IconSet.getSymbol("download", "Title2", "Tooltip2", "class3 class4", "", "id2");
|
||||
IconSet.getSymbol("download", "Title", "Tooltip", "", "class1 class2", "", "id");
|
||||
String symbol = IconSet.getSymbol("download", "Title2", "Tooltip2", "", "class3 class4", "", "id2");
|
||||
|
||||
assertThat(symbol, not(containsString("<span class=\"jenkins-visually-hidden\">Title</span>")));
|
||||
assertThat(symbol, not(containsString("tooltip=\"Tooltip\"")));
|
||||
|
@ -62,7 +62,7 @@ public class IconSetTest {
|
|||
*/
|
||||
@Test
|
||||
void getSymbol_notSettingTooltipDoesntAddTooltipAttribute() {
|
||||
String symbol = IconSet.getSymbol("download", "Title", "", "class1 class2", "", "id");
|
||||
String symbol = IconSet.getSymbol("download", "Title", "", "", "class1 class2", "", "id");
|
||||
|
||||
assertThat(symbol, not(containsString("tooltip")));
|
||||
}
|
||||
|
|
|
@ -46,12 +46,10 @@ import static org.junit.Assert.assertTrue;
|
|||
|
||||
import com.gargoylesoftware.htmlunit.HttpMethod;
|
||||
import com.gargoylesoftware.htmlunit.Page;
|
||||
import com.gargoylesoftware.htmlunit.ScriptResult;
|
||||
import com.gargoylesoftware.htmlunit.WebRequest;
|
||||
import com.gargoylesoftware.htmlunit.html.DomElement;
|
||||
import com.gargoylesoftware.htmlunit.html.DomNode;
|
||||
import com.gargoylesoftware.htmlunit.html.DomNodeList;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlAnchor;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlElement;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlFileInput;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlForm;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlFormUtil;
|
||||
|
@ -1272,14 +1270,11 @@ public class QueueTest {
|
|||
|
||||
HtmlPage page = wc.goTo("");
|
||||
|
||||
DomElement buildQueue = page.getElementById("buildQueue");
|
||||
DomNodeList<HtmlElement> anchors = buildQueue.getElementsByTagName("a");
|
||||
HtmlAnchor anchorWithTooltip = (HtmlAnchor) anchors.stream()
|
||||
.filter(a -> a.getAttribute("tooltip") != null && !a.getAttribute("tooltip").isEmpty())
|
||||
.findFirst().orElseThrow(IllegalStateException::new);
|
||||
page.executeJavaScript("document.querySelector('#buildQueue a[tooltip]:not([tooltip=\"\"])')._tippy.show()");
|
||||
wc.waitForBackgroundJavaScript(1000);
|
||||
ScriptResult result = page.executeJavaScript("document.querySelector('.tippy-content').innerHTML;");
|
||||
|
||||
String tooltip = anchorWithTooltip.getAttribute("tooltip");
|
||||
return tooltip;
|
||||
return result.getJavaScriptResult().toString();
|
||||
}
|
||||
|
||||
public static class BrokenAffinityKeyProject extends Project<BrokenAffinityKeyProject, BrokenAffinityKeyBuild> implements TopLevelItem {
|
||||
|
|
|
@ -157,9 +157,9 @@ public class RunTest {
|
|||
HtmlPage htmlPage = wc.goTo(upProject.getUrl());
|
||||
|
||||
// trigger the tooltip display
|
||||
htmlPage.executeJavaScript("document.querySelector('#buildHistory table .build-badge img').dispatchEvent(new Event('mouseover'));");
|
||||
htmlPage.executeJavaScript("document.querySelector('#buildHistory table .build-badge img')._tippy.show()");
|
||||
wc.waitForBackgroundJavaScript(500);
|
||||
ScriptResult result = htmlPage.executeJavaScript("document.querySelector('#tt').innerHTML;");
|
||||
ScriptResult result = htmlPage.executeJavaScript("document.querySelector('.tippy-content').innerHTML;");
|
||||
Object jsResult = result.getJavaScriptResult();
|
||||
assertThat(jsResult, instanceOf(String.class));
|
||||
String jsResultString = (String) jsResult;
|
||||
|
|
|
@ -24,10 +24,6 @@
|
|||
|
||||
package hudson.scm;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.containsString;
|
||||
import static org.hamcrest.Matchers.not;
|
||||
import static org.hamcrest.Matchers.startsWith;
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
import com.gargoylesoftware.htmlunit.html.DomElement;
|
||||
|
@ -45,7 +41,6 @@ import hudson.model.TaskListener;
|
|||
import java.io.File;
|
||||
import org.junit.Rule;
|
||||
import org.junit.Test;
|
||||
import org.jvnet.hudson.test.Issue;
|
||||
import org.jvnet.hudson.test.JenkinsRule;
|
||||
|
||||
public class AbstractScmTagActionTest {
|
||||
|
@ -66,19 +61,6 @@ public class AbstractScmTagActionTest {
|
|||
assertEquals(tagToKeep, tooltip);
|
||||
}
|
||||
|
||||
@Test
|
||||
@Issue("SECURITY-1537")
|
||||
public void preventXssInTagAction() throws Exception {
|
||||
FreeStyleProject p = j.createFreeStyleProject();
|
||||
p.setScm(new FakeSCM("<img src='x' onerror=alert(123)>XSS"));
|
||||
|
||||
j.buildAndAssertSuccess(p);
|
||||
|
||||
String tooltip = buildAndExtractTooltipAttribute(p);
|
||||
assertThat(tooltip, not(containsString("<")));
|
||||
assertThat(tooltip, startsWith("<"));
|
||||
}
|
||||
|
||||
private String buildAndExtractTooltipAttribute(FreeStyleProject p) throws Exception {
|
||||
JenkinsRule.WebClient wc = j.createWebClient();
|
||||
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
package jenkins.security;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.containsString;
|
||||
import static org.hamcrest.CoreMatchers.instanceOf;
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
|
||||
import com.gargoylesoftware.htmlunit.ScriptResult;
|
||||
import com.gargoylesoftware.htmlunit.html.HtmlPage;
|
||||
import hudson.Util;
|
||||
import hudson.Functions;
|
||||
import hudson.model.InvisibleAction;
|
||||
import hudson.model.UnprotectedRootAction;
|
||||
import java.io.IOException;
|
||||
|
@ -25,29 +25,35 @@ public class Security2776Test {
|
|||
|
||||
@Test
|
||||
public void escapedTooltipIsEscaped() throws Exception {
|
||||
assertExpectedBehaviorForTooltip("#symbol-icons .unsafe svg", _getUnsafeTooltip(), true);
|
||||
assertExpectedBehaviorForTooltip("#symbol-icons .safe svg", _getSafeTooltip(), false);
|
||||
assertExpectedBehaviorForTooltip("#png-icons .unsafe img", _getUnsafeTooltip(), true);
|
||||
assertExpectedBehaviorForTooltip("#png-icons .safe img", _getSafeTooltip(), false);
|
||||
assertExpectedBehaviorForTooltip("#symbol-icons .unsafe svg",
|
||||
"<img src=\"x\" onerror=\"alert(1)\">");
|
||||
assertExpectedBehaviorForTooltip("#symbol-icons .safe svg",
|
||||
Functions.htmlAttributeEscape(_getSafeTooltip()));
|
||||
assertExpectedBehaviorForTooltip("#png-icons .unsafe img",
|
||||
"<img src=\"x\" onerror=\"alert(1)\">");
|
||||
assertExpectedBehaviorForTooltip("#png-icons .safe img",
|
||||
Functions.htmlAttributeEscape(_getSafeTooltip()));
|
||||
|
||||
// Outlier after the fix for SECURITY-1955
|
||||
assertExpectedBehaviorForTooltip("#svgIcons .unsafe svg", _getSafeTooltip(), false);
|
||||
assertExpectedBehaviorForTooltip("#svgIcons .safe svg", Util.xmlEscape(_getSafeTooltip()), false);
|
||||
assertExpectedBehaviorForTooltip("#svgIcons .unsafe svg",
|
||||
"<img src=\"x\" onerror=\"alert(1)\">");
|
||||
assertExpectedBehaviorForTooltip("#svgIcons .safe svg",
|
||||
"&lt;img src=&quot;x&quot; onerror=&quot;alert(1)&quot;&gt;");
|
||||
}
|
||||
|
||||
private void assertExpectedBehaviorForTooltip(String selector, String expectedTooltipContent, boolean alertExpected) throws IOException, SAXException {
|
||||
private void assertExpectedBehaviorForTooltip(String selector, String expectedResult) throws IOException, SAXException {
|
||||
final AtomicBoolean alerts = new AtomicBoolean();
|
||||
final JenkinsRule.WebClient wc = j.createWebClient();
|
||||
wc.setAlertHandler((p, s) -> alerts.set(true));
|
||||
final HtmlPage page = wc.goTo(URL_NAME);
|
||||
page.executeJavaScript("document.querySelector('" + selector + "').dispatchEvent(new Event('mouseover'));");
|
||||
page.executeJavaScript("document.querySelector('" + selector + "')._tippy.show()");
|
||||
wc.waitForBackgroundJavaScript(2000L);
|
||||
ScriptResult result = page.executeJavaScript("document.querySelector('#tt').innerHTML;");
|
||||
ScriptResult result = page.executeJavaScript("document.querySelector('.tippy-content').innerHTML;");
|
||||
Object jsResult = result.getJavaScriptResult();
|
||||
assertThat(jsResult, instanceOf(String.class));
|
||||
String jsResultString = (String) jsResult;
|
||||
assertThat(jsResultString, containsString(expectedTooltipContent));
|
||||
Assert.assertEquals(alertExpected ? "Alert expected" : "No alert expected", alertExpected, alerts.get());
|
||||
assertThat(jsResultString, is(expectedResult));
|
||||
Assert.assertFalse("No alert expected", alerts.get());
|
||||
}
|
||||
|
||||
private static String _getUnsafeTooltip() {
|
||||
|
@ -55,7 +61,7 @@ public class Security2776Test {
|
|||
}
|
||||
|
||||
private static String _getSafeTooltip() {
|
||||
return Util.xmlEscape(_getUnsafeTooltip());
|
||||
return Functions.htmlAttributeEscape(_getUnsafeTooltip());
|
||||
}
|
||||
|
||||
@TestExtension
|
||||
|
|
|
@ -37,22 +37,19 @@ public class Security2779Test {
|
|||
final JenkinsRule.WebClient webClient = j.createWebClient();
|
||||
webClient.setAlertHandler((AlertHandler) (p, s) -> alerts.addAndGet(1));
|
||||
final HtmlPage page = webClient.goTo(URL_NAME);
|
||||
final ScriptResult eventScript = page.executeJavaScript("document.querySelector('" + selector + "').dispatchEvent(new Event('mouseover'))");
|
||||
final Object eventResult = eventScript.getJavaScriptResult();
|
||||
assertThat(eventResult, instanceOf(boolean.class));
|
||||
Assert.assertTrue((boolean) eventResult);
|
||||
page.executeJavaScript("document.querySelector('" + selector + "')._tippy.show()");
|
||||
webClient.waitForBackgroundJavaScript(2000);
|
||||
// Assertion includes the selector for easier diagnosis
|
||||
Assert.assertEquals("Alert with selector '" + selector + "'", 0, alerts.get());
|
||||
|
||||
final ScriptResult innerHtmlScript = page.executeJavaScript("document.querySelector('#tt').innerHTML");
|
||||
final ScriptResult innerHtmlScript = page.executeJavaScript("document.querySelector('.tippy-content').innerHTML");
|
||||
Object jsResult = innerHtmlScript.getJavaScriptResult();
|
||||
assertThat(jsResult, instanceOf(String.class));
|
||||
String jsResultString = (String) jsResult;
|
||||
|
||||
// assert leading space to identify unintentional double-escaping (&lt;) as test failure
|
||||
assertThat("tooltip does not contain dangerous HTML", jsResultString, not(containsString(" <img src=x")));
|
||||
assertThat("tooltip contains safe text", jsResultString, containsString(" <img src=x"));
|
||||
assertThat("tooltip contains safe text", jsResultString, containsString("lt;img src=x"));
|
||||
}
|
||||
|
||||
@TestExtension
|
||||
|
|
|
@ -27,9 +27,9 @@ public class Security2780Test {
|
|||
AtomicBoolean alertTriggered = new AtomicBoolean(false);
|
||||
wc.setAlertHandler((p, s) -> alertTriggered.set(true));
|
||||
HtmlPage page = wc.goTo("");
|
||||
page.executeJavaScript("document.querySelector('a.jenkins-table__button').dispatchEvent(new Event('mouseover'));");
|
||||
page.executeJavaScript("document.querySelector('a.jenkins-table__button')._tippy.show()");
|
||||
wc.waitForBackgroundJavaScript(2000L);
|
||||
ScriptResult result = page.executeJavaScript("document.querySelector('#tt').innerHTML;");
|
||||
ScriptResult result = page.executeJavaScript("document.querySelector('.tippy-content').innerHTML;");
|
||||
Object jsResult = result.getJavaScriptResult();
|
||||
assertThat(jsResult, instanceOf(String.class));
|
||||
String jsResultString = (String) jsResult;
|
||||
|
|
|
@ -639,7 +639,7 @@ public class RepeatableTest {
|
|||
*/
|
||||
private static List<?> getButtonsList(HtmlForm form, String buttonCaption) {
|
||||
return form.getByXPath(
|
||||
String.format("//button[text() = '%s'] | //button[@title = '%s']", buttonCaption, buttonCaption
|
||||
String.format("//button[text() = '%s'] | //button[@tooltip = '%s']", buttonCaption, buttonCaption, buttonCaption
|
||||
)
|
||||
);
|
||||
}
|
||||
|
|
|
@ -66,9 +66,7 @@ public class SvgIconTest {
|
|||
|
||||
String pristineTooltip = "Special tooltip with double quotes \", simple quotes ', and html characters <>&.";
|
||||
|
||||
// Escaped twice, once per new h.xmlEscape then once per Jelly.
|
||||
// But as the tooltip lib interprets HTML, it's fine, the tooltip displays the original values without interpreting them
|
||||
String expectedTooltip = "Special tooltip with double quotes ", simple quotes ', and html characters &lt;&gt;&amp;.";
|
||||
String expectedTooltip = "Special tooltip with double quotes ", simple quotes ', and html characters <>&.";
|
||||
testRootAction.tooltipContent = pristineTooltip;
|
||||
|
||||
HtmlPage p = j.createWebClient().goTo(testRootAction.getUrlName());
|
||||
|
@ -106,9 +104,9 @@ public class SvgIconTest {
|
|||
String jsControlString = (String) jsControlResult;
|
||||
assertThat("The title attribute is not populated", jsControlString, containsString(validationPart));
|
||||
|
||||
page.executeJavaScript("document.querySelector('#test-panel svg').dispatchEvent(new Event('mouseover'));");
|
||||
page.executeJavaScript("document.querySelector('#test-panel svg')._tippy.show()");
|
||||
wc.waitForBackgroundJavaScript(1000);
|
||||
ScriptResult result = page.executeJavaScript("document.querySelector('#tt').innerHTML;");
|
||||
ScriptResult result = page.executeJavaScript("document.querySelector('.tippy-content').innerHTML;");
|
||||
Object jsResult = result.getJavaScriptResult();
|
||||
assertThat(jsResult, instanceOf(String.class));
|
||||
String jsResultString = (String) jsResult;
|
||||
|
|
|
@ -58,6 +58,7 @@
|
|||
"postcss-less": "6.0.0",
|
||||
"sortablejs": "1.15.0",
|
||||
"stylelint-checkstyle-reporter": "0.2.0",
|
||||
"tippy.js": "^6.3.7",
|
||||
"window-handle": "1.0.1"
|
||||
},
|
||||
"browserslist": [
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
import Notifications from "@/components/notifications";
|
||||
import Tooltips from "@/components/tooltips";
|
||||
|
||||
Notifications.init();
|
||||
Tooltips.init();
|
||||
|
|
|
@ -0,0 +1,124 @@
|
|||
import tippy from "tippy.js";
|
||||
import behaviorShim from "@/util/behavior-shim";
|
||||
|
||||
const TOOLTIP_BASE = {
|
||||
arrow: false,
|
||||
theme: "tooltip",
|
||||
animation: "tooltip",
|
||||
appendTo: document.body,
|
||||
};
|
||||
|
||||
let tooltipInstances = [];
|
||||
const globalPlugin = {
|
||||
fn(instance) {
|
||||
return {
|
||||
onCreate() {
|
||||
tooltipInstances = tooltipInstances.concat(instance);
|
||||
},
|
||||
onDestroy() {
|
||||
tooltipInstances = tooltipInstances.filter((i) => i !== instance);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
tippy.setDefaultProps({
|
||||
plugins: [globalPlugin],
|
||||
});
|
||||
|
||||
/**
|
||||
* Registers tooltips for the page
|
||||
* If called again, destroys existing tooltips and registers them again (useful for progressive rendering)
|
||||
* @param {HTMLElement} container - Registers the tooltips for the given container
|
||||
*/
|
||||
function registerTooltips(container) {
|
||||
if (!container) {
|
||||
container = document;
|
||||
}
|
||||
|
||||
tooltipInstances.forEach((instance) => {
|
||||
if (instance.props.container === container) {
|
||||
instance.destroy();
|
||||
}
|
||||
});
|
||||
|
||||
tippy(
|
||||
container.querySelectorAll(
|
||||
'[tooltip]:not([tooltip=""]):not([data-html-tooltip])'
|
||||
),
|
||||
Object.assign(
|
||||
{
|
||||
content: (element) =>
|
||||
element.getAttribute("tooltip").replace(/<br[ /]?\/?>|\\n/g, "\n"),
|
||||
container: container,
|
||||
onCreate(instance) {
|
||||
instance.reference.setAttribute("title", instance.props.content);
|
||||
},
|
||||
onShow(instance) {
|
||||
instance.reference.removeAttribute("title");
|
||||
},
|
||||
onHidden(instance) {
|
||||
instance.reference.setAttribute("title", instance.props.content);
|
||||
},
|
||||
},
|
||||
TOOLTIP_BASE
|
||||
)
|
||||
);
|
||||
|
||||
tippy(
|
||||
container.querySelectorAll("[data-html-tooltip]"),
|
||||
Object.assign(
|
||||
{
|
||||
content: (element) => element.getAttribute("data-html-tooltip"),
|
||||
allowHTML: true,
|
||||
container: container,
|
||||
onCreate(instance) {
|
||||
instance.props.interactive =
|
||||
instance.reference.getAttribute("data-tooltip-interactive") ===
|
||||
"true";
|
||||
},
|
||||
},
|
||||
TOOLTIP_BASE
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Displays a tooltip for three seconds on the provided element after interaction
|
||||
* @param {string} text - The tooltip text
|
||||
* @param {HTMLElement} element - The element to show the tooltip
|
||||
*/
|
||||
function hoverNotification(text, element) {
|
||||
const tooltip = tippy(
|
||||
element,
|
||||
Object.assign(
|
||||
{
|
||||
trigger: "hover",
|
||||
offset: [0, 0],
|
||||
content: text,
|
||||
onShow(instance) {
|
||||
setTimeout(() => {
|
||||
instance.hide();
|
||||
}, 3000);
|
||||
},
|
||||
},
|
||||
TOOLTIP_BASE
|
||||
)
|
||||
);
|
||||
tooltip.show();
|
||||
}
|
||||
|
||||
function init() {
|
||||
behaviorShim.specify(
|
||||
"[tooltip], [data-html-tooltip]",
|
||||
"-tooltip-",
|
||||
1000,
|
||||
function () {
|
||||
registerTooltips(null);
|
||||
}
|
||||
);
|
||||
|
||||
window.hoverNotification = hoverNotification;
|
||||
}
|
||||
|
||||
export default { init };
|
|
@ -162,9 +162,9 @@
|
|||
--link-font-weight: 600;
|
||||
|
||||
// Tooltips
|
||||
--tooltip-background-color: var(--background);
|
||||
--tooltip-foreground-color: var(--text-color);
|
||||
--tooltip-shadow: 0 0 8px 2px rgba(0, 0, 0, 0.05), 0 2px 2px rgba(0, 0, 0, 0.05), 0 10px 20px rgba(0, 0, 0, 0.2);
|
||||
--tooltip-backdrop-filter: contrast(0.6) brightness(2.4) saturate(2) blur(15px);
|
||||
--tooltip-color: var(--text-color);
|
||||
--tooltip-box-shadow: 0 0 8px 2px rgba(0, 0, 30, 0.05), 0 0 1px 1px rgba(0, 0, 20, 0.025), 0 10px 20px rgba(0, 0, 20, 0.15);
|
||||
|
||||
// Dark link
|
||||
--link-dark-color: var(--text-color);
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
@import url("../abstracts/theme.less");
|
||||
|
||||
html {
|
||||
position: relative;
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
|
|
@ -49,16 +49,22 @@
|
|||
width: 24px;
|
||||
}
|
||||
}
|
||||
|
||||
svg {
|
||||
vertical-align: middle;
|
||||
width: 0.8rem;
|
||||
height: 0.8rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
& > tbody {
|
||||
& > tr {
|
||||
background: var(--table-body-background);
|
||||
color: var(--table-body-foreground);
|
||||
|
||||
& > td {
|
||||
background: var(--table-body-background);
|
||||
vertical-align: middle;
|
||||
padding: var(--table-padding) 0 var(--table-padding)
|
||||
var(--table-padding);
|
||||
|
|
|
@ -1,12 +1,53 @@
|
|||
.jenkins-tooltip {
|
||||
position: absolute;
|
||||
padding: 5px 10px;
|
||||
border-radius: 10px;
|
||||
background: var(--tooltip-background-color);
|
||||
box-shadow: var(--tooltip-shadow);
|
||||
color: var(--tooltip-foreground-color);
|
||||
font-size: 0.8rem;
|
||||
z-index: 1001 !important;
|
||||
overflow: hidden;
|
||||
max-width: none !important;
|
||||
.tippy-box[data-theme~="tooltip"] {
|
||||
color: var(--tooltip-color);
|
||||
padding: 0.45rem 0.8rem;
|
||||
border-radius: 0.66rem;
|
||||
box-shadow: var(--tooltip-box-shadow);
|
||||
font-weight: 550;
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.6;
|
||||
max-width: ~"min(50vw, 1000px)" !important;
|
||||
white-space: pre-line;
|
||||
z-index: 0;
|
||||
backdrop-filter: var(--tooltip-backdrop-filter);
|
||||
|
||||
.tippy-content {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
// We style tables as they have additional margin/border radius when in tooltips
|
||||
.jenkins-tooltip--table-wrapper {
|
||||
background-color: rgba(black, 0.05);
|
||||
margin: -0.45rem -0.8rem;
|
||||
border-radius: 0.6rem;
|
||||
}
|
||||
|
||||
.jenkins-table {
|
||||
--table-background: transparent;
|
||||
--table-border-radius: 8px;
|
||||
|
||||
margin: 0;
|
||||
width: 450px;
|
||||
}
|
||||
}
|
||||
|
||||
.tippy-box[data-animation="tooltip"][data-state="hidden"] {
|
||||
opacity: 0;
|
||||
transform: scale(0.995);
|
||||
|
||||
&[data-placement^="top"] {
|
||||
transform-origin: bottom;
|
||||
transform: translateY(2px) scale(0.995);
|
||||
}
|
||||
|
||||
&[data-placement^="bottom"] {
|
||||
transform-origin: top;
|
||||
transform: translateY(-2px) scale(0.995);
|
||||
}
|
||||
}
|
||||
|
||||
// Workaround for NG Warnings which supports modern Tippy tooltips and a custom solution,
|
||||
// hide the custom solution
|
||||
.jenkins-table .healthReportDetails {
|
||||
display: none !important;
|
||||
}
|
||||
|
|
|
@ -539,9 +539,6 @@ function fireEvent(element, event) {
|
|||
}
|
||||
}
|
||||
|
||||
// shared tooltip object
|
||||
var tooltip;
|
||||
|
||||
// Behavior rules
|
||||
//========================================================
|
||||
// using tag names in CSS selector makes the processing faster
|
||||
|
@ -1120,10 +1117,6 @@ function rowvgStartEachRow(recursive, f) {
|
|||
|
||||
(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);
|
||||
|
@ -1419,12 +1412,6 @@ function rowvgStartEachRow(recursive, f) {
|
|||
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",
|
||||
|
@ -1802,36 +1789,6 @@ var hudsonRules = {}; // legacy name
|
|||
// now empty, but plugins can stuff things in here later:
|
||||
Behaviour.register(hudsonRules);
|
||||
|
||||
function applyTooltip(e, text) {
|
||||
// copied from YAHOO.widget.Tooltip.prototype.configContext to efficiently add a new element
|
||||
// event registration via YAHOO.util.Event.addListener leaks memory, so do it by ourselves here
|
||||
e.onmouseover = function (ev) {
|
||||
var delay = this.getAttribute("nodismiss") != null ? 99999999 : 5000;
|
||||
tooltip.cfg.setProperty("autodismissdelay", delay);
|
||||
return tooltip.onContextMouseOver.call(
|
||||
this,
|
||||
YAHOO.util.Event.getEvent(ev),
|
||||
tooltip
|
||||
);
|
||||
};
|
||||
e.onmousemove = function (ev) {
|
||||
return tooltip.onContextMouseMove.call(
|
||||
this,
|
||||
YAHOO.util.Event.getEvent(ev),
|
||||
tooltip
|
||||
);
|
||||
};
|
||||
e.onmouseout = function (ev) {
|
||||
return tooltip.onContextMouseOut.call(
|
||||
this,
|
||||
YAHOO.util.Event.getEvent(ev),
|
||||
tooltip
|
||||
);
|
||||
};
|
||||
e.title = text;
|
||||
e = null; // avoid memory leak
|
||||
}
|
||||
|
||||
var Path = {
|
||||
tail: function (p) {
|
||||
var idx = p.lastIndexOf("/");
|
||||
|
@ -2514,55 +2471,6 @@ function buildFormTree(form) {
|
|||
}
|
||||
}
|
||||
|
||||
var hoverNotification = (function () {
|
||||
var msgBox;
|
||||
var body;
|
||||
|
||||
// animation effect that automatically hide the message box
|
||||
var effect = function (overlay, dur) {
|
||||
var o = YAHOO.widget.ContainerEffect.FADE(overlay, dur);
|
||||
o.animateInCompleteEvent.subscribe(function () {
|
||||
window.setTimeout(function () {
|
||||
msgBox.hide();
|
||||
}, 1500);
|
||||
});
|
||||
return o;
|
||||
};
|
||||
|
||||
function init() {
|
||||
if (msgBox != null) return; // already initialized
|
||||
|
||||
var div = document.createElement("DIV");
|
||||
document.body.appendChild(div);
|
||||
div.innerHTML =
|
||||
"<div id=hoverNotification class='jenkins-tooltip'><div class=bd></div></div>";
|
||||
body = $("hoverNotification");
|
||||
|
||||
msgBox = new YAHOO.widget.Overlay(body, {
|
||||
visible: false,
|
||||
zIndex: 1000,
|
||||
effect: {
|
||||
effect: effect,
|
||||
duration: 0.25,
|
||||
},
|
||||
});
|
||||
msgBox.render();
|
||||
}
|
||||
|
||||
return function (title, anchor, offset) {
|
||||
if (typeof offset === "undefined") {
|
||||
offset = 48;
|
||||
}
|
||||
init();
|
||||
body.innerHTML = title;
|
||||
var xy = YAHOO.util.Dom.getXY(anchor);
|
||||
xy[0] += offset;
|
||||
xy[1] += anchor.offsetHeight;
|
||||
msgBox.cfg.setProperty("xy", xy);
|
||||
msgBox.show();
|
||||
};
|
||||
})();
|
||||
|
||||
// Decrease vertical padding for checkboxes
|
||||
window.addEventListener("load", function () {
|
||||
document.querySelectorAll(".jenkins-form-item").forEach(function (element) {
|
||||
|
|
|
@ -908,7 +908,7 @@ var Enumerable = (function() {
|
|||
|
||||
function findAll(iterator, context) {
|
||||
var results = [];
|
||||
this.each(function(value, index) {
|
||||
this.forEach(function(value, index) {
|
||||
if (iterator.call(context, value, index))
|
||||
results.push(value);
|
||||
});
|
||||
|
@ -1069,7 +1069,6 @@ var Enumerable = (function() {
|
|||
detect: detect,
|
||||
findAll: findAll,
|
||||
select: findAll,
|
||||
filter: findAll,
|
||||
grep: grep,
|
||||
include: include,
|
||||
member: include,
|
||||
|
|
|
@ -1635,6 +1635,13 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@popperjs/core@npm:^2.9.0":
|
||||
version: 2.11.6
|
||||
resolution: "@popperjs/core@npm:2.11.6"
|
||||
checksum: 47fb328cec1924559d759b48235c78574f2d71a8a6c4c03edb6de5d7074078371633b91e39bbf3f901b32aa8af9b9d8f82834856d2f5737a23475036b16817f0
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@sinclair/typebox@npm:^0.24.1":
|
||||
version: 0.24.44
|
||||
resolution: "@sinclair/typebox@npm:0.24.44"
|
||||
|
@ -4106,6 +4113,7 @@ __metadata:
|
|||
stylelint: 14.15.0
|
||||
stylelint-checkstyle-reporter: 0.2.0
|
||||
stylelint-config-standard: 29.0.0
|
||||
tippy.js: ^6.3.7
|
||||
webpack: 5.75.0
|
||||
webpack-cli: 5.0.0
|
||||
webpack-remove-empty-scripts: 1.0.1
|
||||
|
@ -6410,6 +6418,15 @@ __metadata:
|
|||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"tippy.js@npm:^6.3.7":
|
||||
version: 6.3.7
|
||||
resolution: "tippy.js@npm:6.3.7"
|
||||
dependencies:
|
||||
"@popperjs/core": ^2.9.0
|
||||
checksum: cac955318a65288e8d2dca05059878b003c6e66f92c94f7810f5bc5448eb6646abdf7dacc9bd00020e2611592598d0aae3a28ec9a45349a159603c3fdddce5fb
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"to-fast-properties@npm:^2.0.0":
|
||||
version: 2.0.0
|
||||
resolution: "to-fast-properties@npm:2.0.0"
|
||||
|
|
Loading…
Reference in New Issue