Update link dropdown menus to use new Tippy dropdowns (#7474)

* Squashed commit of the following:

commit 0fafc6f2b50c817c11a6149fb4374bfb1a1d486a
Merge: 9046da814f d3d1c88d29
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Sun Dec 4 21:31:15 2022 +0000

    Merge branch 'master' into add-model-link-dropdowns

commit 9046da814f3aa26ec19ece970b25a29375377bfa
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Sun Dec 4 21:31:09 2022 +0000

    Reset

commit 5f81d254112c7179f9c83e7dd3650aa9ba0b0c37
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Sat Nov 26 00:56:02 2022 +0000

    Update prototype.js

commit dab47993c6c5f9bd77e83d392c737f3d6d3ba830
Merge: b166f016a3 221ff946b3
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Fri Nov 25 22:32:28 2022 +0000

    Merge branch 'master' into add-model-link-dropdowns

commit b166f016a3e81b3787702e061cb7793a79295a49
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Fri Nov 25 20:11:45 2022 +0000

    Add support for breadcrumb-config-outline

commit cc05b743192bdfbf68d1399f77a32583f2a65182
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Nov 23 23:03:40 2022 +0000

    Imports

commit d23b82247e9e2defb5501622f4c3a2b3c4ee95c7
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Nov 23 23:02:25 2022 +0000

    Tidy up JS

commit a578f6aa1f1b35db83a3543417b87381f636c3c7
Merge: f48e7ef370 51e084a01a
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Nov 23 11:13:00 2022 +0000

    Merge branch 'master' into add-model-link-dropdowns

commit f48e7ef370b7e807689b2edea81d137ca1e7dddb
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Nov 23 09:57:41 2022 +0000

    Update dropdowns.less

commit 8e54b2d9f17afd8c9d7ccf9f481ad1c571ed43d2
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Nov 23 00:54:33 2022 +0000

    Init

commit 0a161523b2e195dc89c75296111a0be0a9f9319e
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Mon Nov 21 19:56:48 2022 +0000

    Update yarn.lock

commit 8aa08f0fc1ac4e5d2c082e3e9b96344cb255257b
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Mon Nov 21 19:40:26 2022 +0000

    Fix

commit 7a2668ccfde9ec79dfb1fd20d0da70fb4817b1d7
Merge: faf3888e27 df89aa6855
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Mon Nov 21 19:39:51 2022 +0000

    Merge branch 'master' into add-dropdown-button

commit faf3888e279e5ffb2ec99f47506548c435659c76
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Fri Sep 23 23:57:35 2022 +0100

    Update overflowButton.jelly

commit 2c33dee5ec1f6cbcad335f12db58e84966af1ade
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Fri Sep 23 23:55:31 2022 +0100

    Remove unused method, fix docs

commit ed8c2b02e13306e810da721c69486011cfd80308
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Fri Sep 23 23:48:51 2022 +0100

    Fix link appearance

commit 9d04e8092ff8bab60b76bdfb41d2431a6737ce6c
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Fri Sep 23 23:45:40 2022 +0100

    Fix up

commit 109dda954e29d8f85d5dcbaf9b630ce02d17648b
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Fri Sep 23 23:22:10 2022 +0100

    Small cleanup

commit 0f27194eba82073f513b205da991b6a729b40bb0
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Fri Sep 23 23:19:39 2022 +0100

    Lint

commit e63c95620cec8f26fca33605389e5bac751471ac
Merge: dccceff218 7cb5eb78c0
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Fri Sep 23 23:18:39 2022 +0100

    Merge branch 'master' into add-dropdown-button

commit dccceff2185fb4870f1c4639579d472866975cb7
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Jul 27 19:21:39 2022 +0100

    Update dropdowns.less

commit 373b8d90a056453ebb8ccdbe613a8d2eb5c0be51
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Jul 27 19:20:40 2022 +0100

    Update table.jelly

commit 62dff833f3593981c093c0a2a63291cae0adda26
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Jul 27 19:19:51 2022 +0100

    Update index.jelly

commit 9bacedaac4b02d0b7786d22e06277903c2556c62
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Jul 27 19:19:32 2022 +0100

    Update rowSelectionController.jelly

commit 06591b1e01f6eb51721edce588f9f818ee9a4cad
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Jul 27 19:18:30 2022 +0100

    Update row selection controller to use new dropdown component

commit 21b5844fde72cf81d0ff4a57358a9e35d98c8afb
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Jul 27 19:02:52 2022 +0100

    Restyle

commit 9966fe79472fdd7b9229143089d4161a6ddf0ab6
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Jul 27 18:49:06 2022 +0100

    WB

commit 81b073a7143256b6369470794f947b6d989bd957
Merge: 8b58f7234b 315b2f410c
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Jul 27 18:29:11 2022 +0100

    Merge branch 'master' into add-dropdown-button

commit 8b58f7234b96acb0263f02d54cffc3e1fe4705e1
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Jul 20 08:41:48 2022 +0100

    Replace icon with hand made divs

commit 22908c71d286dbb907acc52df1cea49f7966c5d9
Merge: fb89bc5891 b3320ca766
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Tue Jul 19 23:28:58 2022 +0100

    Merge branch 'new-buttons-1-dashboard' into add-dropdown-button

commit fb89bc5891ba15558fef93ddad30e881c7a08175
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Tue Jul 19 23:28:37 2022 +0100

    Init

commit b3320ca766
Merge: 1fa24ea550 f52d99a0e5
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Thu Jul 14 09:50:06 2022 +0100

    Merge branch 'master' into new-buttons-1-dashboard

commit 1fa24ea550
Merge: 8598f99f00 a990ebcda4
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Jul 13 00:02:33 2022 +0100

    Merge branch 'master' into new-buttons-1-dashboard

commit 8598f99f00
Author: Alexander Brandes <mc.cache@web.de>
Date:   Mon Jul 11 15:11:36 2022 +0200

    style: Address stylelint violations

commit fd4da119c4
Merge: 0480268a50 aba1c17ede
Author: Alexander Brandes <mc.cache@web.de>
Date:   Sun Jul 10 23:30:55 2022 +0200

    Merge branch 'master' into new-buttons-1-dashboard

commit 0480268a50
Merge: 9b145b1ae4 ab0bb84958
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Sat Jul 9 21:52:43 2022 +0100

    Merge branch 'master' into new-buttons-1-dashboard

commit 9b145b1ae4
Merge: 418ab063b2 03617e0b43
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Fri Jul 8 19:44:20 2022 +0100

    Merge branch 'master' into new-buttons-1-dashboard

commit 418ab063b2
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Thu Jul 7 22:41:32 2022 +0100

    Update buttons-deprecated.less

commit 2798afa6df
Merge: 296e309afc 6de288f424
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Thu Jul 7 22:40:35 2022 +0100

    Merge branch 'master' into new-buttons-1-dashboard

commit 296e309afc
Merge: 1e646ff7ee 22dcefcd8f
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Thu Jul 7 14:50:14 2022 +0100

    Merge branch 'master' into new-buttons-1-dashboard

commit 1e646ff7ee
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Jul 6 22:59:13 2022 +0100

    Use rem, fix cursor for dashboard icon size

commit 21d4c73d9e
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Jul 6 22:56:05 2022 +0100

    Update with master, make minor changes

commit 1d59bd4a21
Merge: f6fc19bd3e 644a261e78
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Jul 6 22:48:59 2022 +0100

    Merge branch 'master' into new-buttons-1-dashboard

commit f6fc19bd3e
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Sun Jun 26 22:58:12 2022 +0100

    Update dashboard.less

commit 665daa0b85
Merge: a57f13ac88 c60ea9210c
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Sun Jun 26 22:52:33 2022 +0100

    Merge branch 'master' into new-buttons-1-dashboard

commit a57f13ac88
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Mon Jun 20 19:01:18 2022 +0100

    Update simple-page.less

commit 2d3695d163
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Sun Jun 19 22:24:32 2022 +0100

    Update theme.less

commit 361f06c7ef
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Sun Jun 19 22:22:48 2022 +0100

    Split out vars

commit 205336fe9f
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Sun Jun 19 21:48:33 2022 +0100

    Move files about, fix focus state, fix colors

commit 204bafbef1
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Sun Jun 19 21:00:57 2022 +0100

    Update buttons to support colors

commit 1d4be07f52
Merge: c4075772f0 2391319f45
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Sun Jun 19 20:01:19 2022 +0100

    Merge branch 'move-manage-jenkins-less' into new-buttons-1-dashboard

commit c4075772f0
Merge: 0c8d538242 65fcda1350
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Sun Jun 19 20:01:02 2022 +0100

    Merge branch 'master' into new-buttons-1-dashboard

commit 2391319f45
Merge: c9e2823d4a ac66b476d9
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Thu Jun 16 17:47:45 2022 +0100

    Merge branch 'master' into move-manage-jenkins-less

commit c9e2823d4a
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Thu Jun 16 17:35:37 2022 +0100

    Update colors

commit c78dfd6b50
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Thu Jun 16 17:33:47 2022 +0100

    Update theme.less

commit 0996e759d6
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Thu Jun 16 17:32:52 2022 +0100

    Update theme.less

commit e57bc28553
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Thu Jun 16 17:29:15 2022 +0100

    Update theme.less

commit 69c1633e1e
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Thu Jun 16 17:00:06 2022 +0100

    Update style.less

commit e21d4e7ccb
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Jun 15 19:03:50 2022 +0100

    Update breadcrumbs to use new item mixin

commit 90fc4ba147
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Jun 15 18:57:51 2022 +0100

    Rename mixin

commit 28d06386ce
Merge: d1f42acfd5 65fcda1350
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Tue Jun 14 23:35:10 2022 +0100

    Merge branch 'master' into move-manage-jenkins-less

commit d1f42acfd5
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Fri Jun 10 11:13:58 2022 +0100

    Update table.less

commit 13d5729892
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Thu Jun 9 10:03:01 2022 +0100

    Move from px to rem

commit 68f5425a7b
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Jun 8 16:40:41 2022 +0100

    Update theme.less

commit 929e2d5e5a
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Jun 8 16:28:06 2022 +0100

    Update mixins.less

commit 8c4b4662a2
Merge: 8b22270188 77a36fcee9
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Jun 8 16:27:25 2022 +0100

    Merge branch 'master' into move-manage-jenkins-less

commit 8b22270188
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Jun 8 16:19:42 2022 +0100

    Update theme.less

commit e925490722
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Jun 8 16:17:02 2022 +0100

    Add mixin for items

commit 0c8d538242
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Sat Jun 4 00:15:05 2022 +0100

    Update button styling

commit 45c9d078be
Merge: e9cb78a924 77a36fcee9
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Fri Jun 3 23:38:49 2022 +0100

    Merge branch 'master' into new-buttons-1-dashboard

commit d579dc5ae2
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Tue May 17 00:52:29 2022 +0100

    Update section.less

commit 43e4f5c48a
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Tue May 17 00:29:22 2022 +0100

    Init

commit e9cb78a924
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Apr 20 21:01:06 2022 +0100

    Add backplate to table size toggles

commit 8cbfe00561
Merge: 41d45b5e1f b28d225d61
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Wed Apr 20 20:30:39 2022 +0100

    Merge branch 'master' into new-buttons-1-dashboard

commit 41d45b5e1f
Author: Jan Faracik <43062514+janfaracik@users.noreply.github.com>
Date:   Mon Apr 18 12:50:17 2022 +0100

    Initial

* Remove old import

* Simplify code

* Update rowSelectionController.jelly

* Update inpage-jumplist.js

* Escape menu item labels

* Temp commenting out

* More HTMLUnit fixes

* Revert "Temp commenting out"

This reverts commit d61d94332f.

* Add support for left/right keyboard nav in menus, fix keyboard priority in submenus, add scroll into view, remove old menu target element

* Send error to console rather than displaying it in dropdown

* Use 'auto' for overscroll-y

* Remove Util.escape(...) from withDisplayName builder

* Remove unused import

* Show 'No items' placeholder for menus with no items

* Keep dropdown parent links active when expanded

* Update jumplists.js

* Potential fix for failing tests

* Update utils.js

* Update utils.js

* Add badge support to dropdown menus

* Remove font weight from badge

* Merge master

---------

Co-authored-by: Daniel Beck <1831569+daniel-beck@users.noreply.github.com>
This commit is contained in:
Jan Faracik 2023-04-25 08:41:05 +01:00 committed by GitHub
parent 42cd0b7c85
commit 54eb759b74
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 679 additions and 394 deletions

View File

@ -2,7 +2,6 @@ package jenkins.model;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import hudson.Functions;
import hudson.Util;
import hudson.model.Action;
import hudson.model.Actionable;
import hudson.model.BallColor;
@ -414,7 +413,7 @@ public interface ModelObjectWithContextMenu extends ModelObject {
}
public MenuItem withDisplayName(String displayName) {
this.displayName = Util.escape(displayName);
this.displayName = displayName;
return this;
}

View File

@ -32,6 +32,5 @@ THE SOFTWARE.
</st:attribute>
</st:documentation>
<st:adjunct includes="lib.form.breadcrumb-config-outline.init"/>
<l:breadcrumb title="${attrs.title?:'%configuration'}" id="inpage-nav" />
</j:jelly>

View File

@ -1,7 +0,0 @@
A.section-anchor {
position: relative;
top: -2em;
visibility: hidden;
display: inline-block;
width: 0px;
}

View File

@ -1,24 +0,0 @@
Event.observe(window, "load", function () {
/** @type section.SectionNode */
var outline = section.buildTree();
var menu = new breadcrumbs.ContextMenu();
$A(outline.children).each(function (e) {
var id = "section" + iota++; // TODO: use human-readable ID
var caption = e.getHTML();
var cur = $(e.section).down("A.section-anchor");
if (cur != null) {
id = cur.id;
caption = caption.substring(caption.indexOf("&lt;/a>") + 4);
} else {
$(e.section).insert({
top: "<a id=" + id + " class='section-anchor'>#</a>",
});
}
menu.add("#" + id, null, caption);
});
var inpageNav = document.getElementById("inpage-nav");
var chevron = document.createElement("li");
chevron.classList.add("children");
inpageNav.parentNode.insertBefore(chevron, inpageNav.nextSibling);
breadcrumbs.attachMenu(chevron, menu);
});

View File

@ -35,8 +35,6 @@ THE SOFTWARE.
]]>
</st:documentation>
<st:adjunct includes="lib.layout.breadcrumbs" />
<j:set var="contents" trim="true">
<d:invokeBody />
</j:set>
@ -51,7 +49,7 @@ THE SOFTWARE.
hasMenu="${h.isModelWithContextMenu(anc.object)}" />
<j:choose>
<j:when test="${h.isModelWithChildren(anc.object)}">
<li class="children" href="${anc.url}/">
<li class="children" data-href="${anc.url}/">
<!-- shows '>' for rendering children -->
</li>
</j:when>
@ -67,6 +65,4 @@ THE SOFTWARE.
<j:out value="${contents}" />
</ol>
</div>
<div id="breadcrumb-menu-target"/><!-- this is where the menu gets rendered -->
</j:jelly>

View File

@ -1,343 +0,0 @@
window.breadcrumbs = (function () {
/**
* This component actually renders the menu.
*
* @type {YAHOO.widget.Menu}
*/
var menu;
/**
* Used for fetching the content of the menu asynchronously from the server
*/
var controller;
/**
* Current mouse cursor position in the page coordinate.
*
* @type {YAHOO.util.Point}
*/
// eslint-disable-next-line no-unused-vars
var mouse;
var logger = function () {};
// logger = function() { console.log.apply(console,arguments) }; // uncomment this line to enable logging
// TODO - Use util/security.js xmlEscape in #7474
function xmlEscape(str) {
if (!str) {
return;
}
return str.replace(/[<>&'"]/g, (match) => {
switch (match) {
case "<":
return "&lt;";
case ">":
return "&gt;";
case "&":
return "&amp;";
case "'":
return "&apos;";
case '"':
return "&quot;";
}
});
}
function makeMenuHtml(icon, iconXml, displayName, badge) {
var displaynameSpan = "<span>" + displayName + "</span>";
let badgeText;
let badgeTooltip;
if (badge) {
badgeText = xmlEscape(badge.text);
badgeTooltip = xmlEscape(badge.tooltip);
}
const badgeSpan =
badge === null
? ""
: `<span class="yui-menu-badge" tooltip="${badgeTooltip}">${badgeText}</span>`;
if (iconXml != null) {
return iconXml + displaynameSpan + badgeSpan;
}
if (icon === null) {
return (
"<span style='margin: 2px 4px 2px 2px;' />" +
displaynameSpan +
badgeSpan
);
}
// TODO: move this to the API response in a clean way
var isSvgSprite = icon.toLowerCase().indexOf("svg#") !== -1;
return isSvgSprite
? "<svg class='svg-icon' width='24' height='24' style='margin: 2px 4px 2px 2px;' aria-label='' focusable='false'>" +
"<use href='" +
icon +
"' />" +
"</svg>" +
displaynameSpan +
badgeSpan
: "<img src='" +
icon +
"' width=24 height=24 style='margin: 2px 4px 2px 2px;' alt=''>" +
displaynameSpan +
badgeSpan;
}
Event.observe(window, "load", function () {
menu = new YAHOO.widget.Menu("breadcrumb-menu", {
position: "dynamic",
hidedelay: 1000,
zIndex: 2001,
scrollincrement: 2,
});
});
Event.observe(document, "mousemove", function (ev) {
mouse = new YAHOO.util.Point(ev.pageX, ev.pageY);
});
function combinePath(a, b) {
var qs;
var i = a.indexOf("?");
if (i >= 0) {
qs = a.substring(i);
a = a.substring(0, i);
} else {
qs = "";
}
i = a.indexOf("#");
if (i >= 0) {
a = a.substring(0, i);
}
if (a.endsWith("/")) {
return a + b + qs;
}
return a + "/" + b + qs;
}
function postRequest(action, event, url) {
fetch(url, {
method: "post",
headers: crumb.wrap({}),
});
if (event.length == 1 && event[0].target != null) {
hoverNotification("Done.", event[0].target);
}
}
function requireConfirmation(action, event, cfg) {
if (confirm(cfg.displayName + ": are you sure?")) {
// TODO I18N
var form = document.createElement("form");
form.setAttribute("method", cfg.post ? "POST" : "GET");
form.setAttribute("action", cfg.url);
if (cfg.post) {
crumb.appendToForm(form);
}
document.body.appendChild(form);
form.submit();
}
}
/**
* Wraps a delayed action and its cancellation.
*/
// unsure if used in plugins
// eslint-disable-next-line no-unused-vars
function Delayed(action, timeout) {
this.schedule = function () {
this.cancel();
this.token = window.setTimeout(
function () {
this.token = null;
action();
}.bind(this),
timeout
);
logger("Scheduled %s", this.token);
};
this.cancel = function () {
if (this.token != null) {
logger("Cancelling %s", this.token);
window.clearTimeout(this.token);
}
this.token = null;
};
}
/**
* Called when the user clicks a mouse to show a context menu.
*
* If the mouse stays there for a while, a context menu gets displayed.
*
* @param {HTMLElement} e
* anchor tag
* @param {String} contextMenuUrl
* The URL that renders JSON for context menu. Optional.
*/
function invokeContextMenu(e, contextMenuUrl) {
contextMenuUrl = contextMenuUrl || "contextMenu";
function showMenu(items) {
menu.hide();
var pos = [e, "tl", "bl"];
if ($(e).hasClassName("tl-tr")) {
pos = [e, "tl", "tr"];
}
menu.cfg.setProperty("context", pos);
menu.clearContent();
menu.addItems(items);
menu.render("breadcrumb-menu-target");
menu.show();
Behaviour.applySubtree(menu.body);
}
// ignore the currently pending call
if (controller) {
controller.abort();
}
if (e.items) {
// use what's already loaded
showMenu(e.items());
} else {
// fetch menu on demand
controller = new AbortController();
let { signal } = controller;
fetch(combinePath(e.getAttribute("href"), contextMenuUrl), { signal })
.then((response) => {
response.json().then((json) => {
const items = json.items;
function fillMenuItem(e) {
if (e.type === "HEADER") {
e.text = makeMenuHtml(
e.icon,
e.iconXml,
"<span class='header'>" + e.displayName + "</span>",
e.badge
);
e.disabled = true;
} else if (e.type === "SEPARATOR") {
e.text = "<span class='separator'>--</span>";
e.disabled = true;
} else {
e.text = makeMenuHtml(
e.icon,
e.iconXml,
e.displayName,
e.badge
);
}
if (e.subMenu != null) {
e.subMenu = {
id: "submenu" + iota++,
itemdata: e.subMenu.items.each(fillMenuItem),
};
}
if (e.requiresConfirmation) {
e.onclick = {
fn: requireConfirmation,
obj: { url: e.url, displayName: e.displayName, post: e.post },
};
delete e.url;
} else if (e.post) {
e.onclick = { fn: postRequest, obj: e.url };
delete e.url;
}
}
items.forEach(fillMenuItem);
e.items = function () {
return items;
};
showMenu(items);
});
})
.catch((err) => {
if (err.name === "AbortError") {
// ignore user aborting request, browser console will get unnecessary spam if we don't catch this
} else {
throw err;
}
});
}
return false;
}
Behaviour.specify("A.model-link", "breadcrumbs", 0, function (link) {
const isFirefox = navigator.userAgent.indexOf("Firefox") !== -1;
// Firefox adds unwanted lines when copying buttons in text, so use a span instead
const dropdownChevron = document.createElement(
isFirefox ? "span" : "button"
);
dropdownChevron.className = "jenkins-menu-dropdown-chevron";
dropdownChevron.addEventListener("click", function (e) {
e.preventDefault();
invokeContextMenu(link);
});
link.appendChild(dropdownChevron);
});
Behaviour.specify("#breadcrumbs LI.children", "breadcrumbs", 0, function (a) {
a.observe("click", function () {
invokeContextMenu(this, "childrenContextMenu");
});
});
/**
* @namespace breadcrumbs
* @class ContextMenu
* @constructor
*/
var ContextMenu = function () {
this.items = [];
};
ContextMenu.prototype = {
/**
* Creates a menu item.
*
* @return {breadcrumbs.MenuItem}
*/
add: function (url, icon, displayName) {
this.items.push({
url: url,
text: makeMenuHtml(icon, null, displayName),
});
return this;
},
};
return {
/**
* Activates the context menu for the specified breadcrumb element.
*
* @param {String|HTMLElement} li
* The LI tag to which you associate the menu (or its ID)
* @param {Function|breadcrumbs.ContextMenu} menu
* Pass in the configured menu object. If a function is given, this function
* is called each time a menu needs to be displayed. This is convenient for dynamically
* populating the content.
*/
attachMenu: function (li, menu) {
$(li).items =
typeof menu == "function"
? menu
: function () {
return menu.items;
};
$(li).addEventListener("click", function () {
invokeContextMenu($(li));
});
},
ContextMenu: ContextMenu,
};
})();

View File

@ -1,7 +1,9 @@
import Dropdowns from "@/components/dropdowns";
import Notifications from "@/components/notifications";
import SearchBar from "@/components/search-bar";
import Tooltips from "@/components/tooltips";
Dropdowns.init();
Notifications.init();
SearchBar.init();
Tooltips.init();

View File

@ -0,0 +1,9 @@
import Jumplists from "@/components/dropdowns/jumplists";
import InpageJumplist from "@/components/dropdowns/inpage-jumplist";
function init() {
Jumplists.init();
InpageJumplist.init();
}
export default { init };

View File

@ -0,0 +1,26 @@
import { toId } from "@/util/dom";
/*
* Generates a jump list for the active breadcrumb to jump to
* sections on the page (if using <f:breadcrumb-config-outline />)
*/
function init() {
const inpageNavigationBreadcrumb = document.querySelector("#inpage-nav");
if (inpageNavigationBreadcrumb) {
const chevron = document.createElement("li");
chevron.classList.add("children");
chevron.items = Array.from(
document.querySelectorAll(
"form > div > div > .jenkins-section > .jenkins-section__title"
)
).map((section) => {
section.id = toId(section.textContent);
return { label: section.textContent, url: "#" + section.id };
});
inpageNavigationBreadcrumb.after(chevron);
}
}
export default { init };

View File

@ -0,0 +1,123 @@
import Path from "@/util/path";
import behaviorShim from "@/util/behavior-shim";
import Utils from "@/components/dropdowns/utils";
function init() {
generateJumplistAccessors();
generateDropdowns();
}
/*
* Appends a button at the end of links which support jump lists
*/
function generateJumplistAccessors() {
document.querySelectorAll("A.model-link").forEach((link) => {
const isFirefox = navigator.userAgent.indexOf("Firefox") !== -1;
// Firefox adds unwanted lines when copying buttons in text, so use a span instead
const dropdownChevron = document.createElement(
isFirefox ? "span" : "button"
);
dropdownChevron.className = "jenkins-menu-dropdown-chevron";
dropdownChevron.dataset.href = link.href;
dropdownChevron.addEventListener("click", (event) => {
event.preventDefault();
});
link.appendChild(dropdownChevron);
});
}
/*
* Generates the dropdowns for the jump lists
*/
function generateDropdowns() {
behaviorShim.specify(
"li.children, #menuSelector, .jenkins-menu-dropdown-chevron",
"-dropdown-",
1000,
(element) =>
Utils.generateDropdown(element, (instance) => {
const href = element.dataset.href;
const jumplistType = !element.classList.contains("children")
? "contextMenu"
: "childrenContextMenu";
if (element.items) {
instance.setContent(Utils.generateDropdownItems(element.items));
return;
}
fetch(Path.combinePath(href, jumplistType))
.then((response) => response.json())
.then((json) =>
instance.setContent(
Utils.generateDropdownItems(
mapChildrenItemsToDropdownItems(json.items)
)
)
)
.catch((error) => console.log(`Jumplist request failed: ${error}`))
.finally(() => (instance.loaded = true));
})
);
}
/*
* Generates the contents for the dropdown
*/
function mapChildrenItemsToDropdownItems(items) {
return items.map((item) => {
if (item.type === "HEADER") {
return {
type: "HEADER",
label: item.displayName,
};
}
if (item.type === "SEPARATOR") {
return {
type: "SEPARATOR",
};
}
return {
icon: item.icon,
iconXml: item.iconXml,
label: item.displayName,
url: item.url,
type: item.post || item.requiresConfirmation ? "button" : "link",
badge: item.badge,
onClick: () => {
if (item.post || item.requiresConfirmation) {
if (item.requiresConfirmation) {
if (confirm((item.text || item.displayName) + ": are you sure?")) {
// TODO I18N
const form = document.createElement("form");
form.setAttribute("method", item.post ? "POST" : "GET");
form.setAttribute("action", item.url);
if (item.post) {
crumb.appendToForm(form);
}
document.body.appendChild(form);
form.submit();
}
} else {
fetch(item.url, {
method: "post",
headers: crumb.wrap({}),
});
if (event.length === 1 && event[0].target != null) {
hoverNotification("Done.", event[0].target);
}
}
}
},
subMenu: item.subMenu
? () => {
return mapChildrenItemsToDropdownItems(item.subMenu.items);
}
: null,
};
});
}
export default { init };

View File

@ -0,0 +1,103 @@
import { createElementFromHtml } from "@/util/dom";
import { xmlEscape } from "@/util/security";
function dropdown() {
return {
content: "<p class='jenkins-spinner'></p>",
interactive: true,
trigger: "click",
allowHTML: true,
placement: "bottom-start",
arrow: false,
theme: "dropdown",
appendTo: document.body,
offset: [0, 0],
animation: "dropdown",
onShow: (instance) => {
const referenceParent = instance.reference.parentNode;
if (referenceParent.classList.contains("model-link")) {
referenceParent.classList.add("model-link--open");
}
},
onHide: (instance) => {
const referenceParent = instance.reference.parentNode;
referenceParent.classList.remove("model-link--open");
},
};
}
function menuItem(options) {
const itemOptions = Object.assign(
{
type: "link",
},
options
);
const label = xmlEscape(itemOptions.label);
let badgeText;
let badgeTooltip;
if (itemOptions.badge) {
badgeText = xmlEscape(itemOptions.badge.text);
badgeTooltip = xmlEscape(itemOptions.badge.tooltip);
}
const tag = itemOptions.type === "link" ? "a" : "button";
const item = createElementFromHtml(`
<${tag} class="jenkins-dropdown__item" href="${itemOptions.url}">
${
itemOptions.icon
? `<div class="jenkins-dropdown__item__icon">${
itemOptions.iconXml
? itemOptions.iconXml
: `<img alt="${label}" src="${itemOptions.icon}" />`
}</div>`
: ``
}
${label}
${
itemOptions.badge != null
? `<span class="jenkins-dropdown__item__badge" tooltip="${badgeTooltip}">${badgeText}</span>`
: ``
}
${
itemOptions.subMenu != null
? `<span class="jenkins-dropdown__item__chevron"></span>`
: ``
}
</${tag}>
`);
if (options.onClick) {
item.addEventListener("click", () => options.onClick());
}
return item;
}
function heading(label) {
return createElementFromHtml(
`<p class="jenkins-dropdown__heading">${label}</p>`
);
}
function separator() {
return createElementFromHtml(
`<div class="jenkins-dropdown__separator"></div>`
);
}
function placeholder(label) {
return createElementFromHtml(
`<p class="jenkins-dropdown__placeholder">${label}</p>`
);
}
export default {
dropdown,
menuItem,
heading,
separator,
placeholder,
};

View File

@ -0,0 +1,135 @@
import Templates from "@/components/dropdowns/templates";
import makeKeyboardNavigable from "@/util/keyboard";
import tippy from "tippy.js";
import behaviorShim from "@/util/behavior-shim";
const SELECTED_ITEM_CLASS = "jenkins-dropdown__item--selected";
/*
* Generates the dropdowns for the given element
* Preloads the data on hover for speed
* @param element - the element to generate the dropdown for
* @param callback - called to retrieve the list of dropdown items
*/
function generateDropdown(element, callback) {
tippy(
element,
Object.assign({}, Templates.dropdown(), {
onCreate(instance) {
instance.reference.addEventListener("mouseenter", () => {
if (instance.loaded) {
return;
}
instance.popper.addEventListener("click", () => {
instance.hide();
});
callback(instance);
});
},
})
);
}
/*
* Generates the contents for the dropdown
*/
function generateDropdownItems(items) {
const menuItems = document.createElement("div");
menuItems.classList.add("jenkins-dropdown");
items
.map((item) => {
if (item.type === "HEADER") {
return Templates.heading(item.label);
}
if (item.type === "SEPARATOR") {
return Templates.separator();
}
const menuItem = Templates.menuItem(item);
if (item.subMenu != null) {
tippy(
menuItem,
Object.assign({}, Templates.dropdown(), {
content: generateDropdownItems(item.subMenu()),
trigger: "mouseenter",
placement: "right-start",
offset: [-8, 0],
})
);
}
return menuItem;
})
.forEach((item) => menuItems.appendChild(item));
if (items.length === 0) {
menuItems.appendChild(Templates.placeholder("No items"));
}
makeKeyboardNavigable(
menuItems,
() => menuItems.querySelectorAll(".jenkins-dropdown__item"),
SELECTED_ITEM_CLASS,
(selectedItem, key) => {
switch (key) {
case "ArrowLeft": {
const root = selectedItem.closest("[data-tippy-root]");
if (root) {
const tippyReference = root._tippy;
if (tippyReference) {
tippyReference.hide();
}
}
break;
}
case "ArrowRight": {
const tippyRef = selectedItem._tippy;
if (!tippyRef) {
break;
}
tippyRef.show();
tippyRef.props.content
.querySelector(".jenkins-dropdown__item")
.classList.add(SELECTED_ITEM_CLASS);
break;
}
}
},
(container) => {
const isVisible =
window.getComputedStyle(container).visibility === "visible";
const isLastDropdown = Array.from(
document.querySelectorAll(".jenkins-dropdown")
)
.filter((dropdown) => container !== dropdown)
.filter(
(dropdown) =>
window.getComputedStyle(dropdown).visibility === "visible"
)
.every(
(dropdown) =>
!(
container.compareDocumentPosition(dropdown) &
Node.DOCUMENT_POSITION_FOLLOWING
)
);
return isVisible && isLastDropdown;
}
);
behaviorShim.applySubtree(menuItems);
return menuItems;
}
export default {
generateDropdown,
generateDropdownItems,
};

View File

@ -3,4 +3,9 @@ function specify(selector, id, priority, behavior) {
Behaviour.specify(selector, id, priority, behavior);
}
export default { specify };
function applySubtree(startNode, includeSelf) {
// eslint-ignore-next-line
Behaviour.applySubtree(startNode, includeSelf);
}
export default { specify, applySubtree };

View File

@ -2,26 +2,29 @@
* @param {Element} container - the container for the items
* @param {function(): NodeListOf<Element>} itemsFunc - function which returns the list of items
* @param {string} selectedClass - the class to apply to the selected item
* @param {function()} additionalBehaviours - add additional keyboard shortcuts to the focused item
* @param hasKeyboardPriority - set if custom behaviour is needed to decide whether the element has keyboard priority
*/
export default function makeKeyboardNavigable(
container,
itemsFunc,
selectedClass
selectedClass,
additionalBehaviours = () => {},
hasKeyboardPriority = () =>
window.getComputedStyle(container).visibility === "visible"
) {
window.addEventListener("keydown", (e) => {
let items = itemsFunc();
let selectedItem = Array.from(items).find((a) =>
a.classList.contains(selectedClass)
);
const isVisible =
window.getComputedStyle(container).visibility === "visible";
let items = Array.from(itemsFunc());
let selectedItem = items.find((a) => a.classList.contains(selectedClass));
// Only navigate through the list of items if the container is active on the screen
if (container && isVisible) {
if (container && hasKeyboardPriority(container)) {
if (e.key === "ArrowDown") {
e.preventDefault();
if (selectedItem) {
selectedItem.classList.remove(selectedClass);
const next = selectedItem.nextSibling;
const next = items[items.indexOf(selectedItem) + 1];
if (next) {
selectedItem = next;
@ -33,12 +36,15 @@ export default function makeKeyboardNavigable(
}
if (selectedItem !== null) {
selectedItem.scrollIntoView(false);
selectedItem.classList.add(selectedClass);
}
} else if (e.key === "ArrowUp") {
e.preventDefault();
if (selectedItem) {
selectedItem.classList.remove(selectedClass);
const previous = selectedItem.previousSibling;
const previous = items[items.indexOf(selectedItem) - 1];
if (previous) {
selectedItem = previous;
@ -50,12 +56,15 @@ export default function makeKeyboardNavigable(
}
if (selectedItem !== null) {
selectedItem.scrollIntoView(false);
selectedItem.classList.add(selectedClass);
}
} else if (e.key === "Enter") {
if (selectedItem !== null) {
selectedItem.click();
}
} else {
additionalBehaviours(selectedItem, e.key);
}
}
});

View File

@ -0,0 +1,21 @@
function combinePath(pathOne, pathTwo) {
let queryParams;
let i = pathOne.indexOf("?");
if (i >= 0) {
queryParams = pathOne.substring(i);
} else {
queryParams = "";
}
i = pathOne.indexOf("#");
if (i >= 0) {
pathOne = pathOne.substring(0, i);
}
if (pathOne.endsWith("/")) {
return pathOne + pathTwo + queryParams;
}
return pathOne + "/" + pathTwo + queryParams;
}
export default { combinePath };

View File

@ -62,7 +62,8 @@
margin-right: 30px !important;
}
&:hover {
&:hover,
&--open {
margin-right: 30px !important;
}
}
@ -210,6 +211,14 @@
margin-left: -1.5rem !important;
}
&--open {
&::before {
background-color: var(--item-background--hover) !important;
right: -30px !important;
}
}
&--open,
&:hover {
& + a.jenkins-table__badge {
margin-left: -1.5rem !important;

View File

@ -0,0 +1,222 @@
@use "../abstracts/mixins";
.tippy-box[data-theme~="dropdown"] {
padding: 0.4rem;
border-radius: 15px;
box-shadow: var(--dropdown-box-shadow);
outline: none !important;
backdrop-filter: var(--dropdown-backdrop-filter);
max-width: unset !important;
max-height: 75vh;
overflow-y: auto;
.tippy-content {
display: flex;
flex-direction: column;
padding: 0;
.jenkins-spinner {
margin: 1rem;
}
}
}
.tippy-box[data-animation="dropdown"][data-state="hidden"] {
opacity: 0;
transform: scale(0.975);
&[data-placement^="top"] {
transform-origin: bottom;
transform: translateY(10px) scale(0.975);
}
&[data-placement^="bottom"] {
transform-origin: top;
transform: translateY(-10px) scale(0.975);
}
}
.jenkins-dropdown {
display: contents;
&__separator {
position: relative;
height: 0.125rem;
margin: 0.3rem -0.3rem;
border: none;
&::before {
content: "";
position: absolute;
inset: 0;
background-color: var(--text-color-secondary);
opacity: 0.1;
}
}
&__heading {
color: var(--text-color-secondary) !important;
margin: 0.4rem 0.55rem;
font-size: 0.8rem;
font-weight: 600;
opacity: 0.8;
&:not(:first-of-type) {
margin-top: 1rem;
}
}
&__placeholder {
color: var(--text-color-secondary) !important;
margin: 0.4rem 0.55rem;
font-size: 0.8rem;
opacity: 0.8;
}
&__item {
--item-background--hover: var(--button-background--hover);
--item-background--active: var(--button-background--active);
--item-box-shadow--focus: var(--button-box-shadow--focus);
@include mixins.item;
appearance: none;
display: inline-flex;
align-items: center;
justify-content: flex-start;
border: none;
outline: none;
margin: 0;
padding: 0.4rem 1.75rem 0.4rem 0.6rem;
font-size: 0.8rem;
font-weight: 500;
text-decoration: none !important;
background: transparent;
color: var(--text-color) !important;
border-radius: 0.66rem;
cursor: pointer;
min-height: 36px;
white-space: nowrap;
gap: 1.2ch;
min-width: 180px;
user-select: none;
&__icon {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.125rem;
height: 1.125rem;
margin-right: 0.1rem;
svg,
img {
width: 1.125rem;
height: 1.125rem;
color: inherit;
}
}
&:disabled {
pointer-events: none;
opacity: 0.5;
filter: saturate(0.6);
}
&[class*="color"] {
background: transparent;
color: var(--color) !important;
&::before {
background: currentColor !important;
opacity: 0;
}
&::after {
box-shadow: 0 0 0 0.66rem currentColor;
opacity: 0;
}
&:hover {
&::before {
opacity: 0.15;
}
}
&:active,
&:focus {
&::before {
opacity: 0.2;
}
&::after {
box-shadow: 0 0 0 0.33rem currentColor;
opacity: 0.1;
}
}
}
&__badge {
position: relative;
margin-left: auto;
margin-right: -0.85rem;
&::before {
content: "";
position: absolute;
inset: -0.0625rem -0.375rem;
background: var(--text-color-secondary);
opacity: 0.1;
border-radius: 100vmax;
}
}
&__chevron {
position: absolute;
top: 0;
right: 0.125rem;
bottom: 0;
background: var(--text-color-secondary);
width: 1rem;
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' class='ionicon' viewBox='0 0 512 512'%3E%3Ctitle%3EChevron Forward%3C/title%3E%3Cpath fill='none' stroke='currentColor' stroke-linecap='round' stroke-linejoin='round' stroke-width='48' d='M184 112l144 144-144 144'/%3E%3C/svg%3E");
mask-size: contain;
mask-repeat: no-repeat;
mask-position: center;
opacity: 0.6;
}
}
&:hover {
.jenkins-dropdown__item--selected {
background: transparent;
}
}
}
.jenkins-dropdown__item--selected {
background: var(--item-background--hover);
animation: pulse 1s ease-in-out forwards;
@keyframes pulse {
50% {
background: var(--item-background--active);
}
}
}
.jenkins-overflow-button__ellipsis {
display: flex;
align-items: center;
justify-content: center;
gap: 0.15rem;
width: 1.4rem;
height: 1.1rem;
margin-inline: -0.1rem;
span {
min-width: 5px;
min-height: 5px;
border: 1px solid currentColor;
border-radius: 50%;
}
}

View File

@ -24,6 +24,7 @@
@use "./modules/buttons";
@use "./modules/buttons-deprecated";
@use "./modules/content-blocks";
@use "./modules/dropdowns";
@use "./modules/icons";
@use "./modules/modals";
@use "./modules/notifications";