Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Revert "Replace hetero-list YUI button and menu with new style button and tippy.js menu" #8412

Merged
merged 1 commit into from
Aug 22, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// prettier-ignore
window.createFilterMenuButton = function (
button,
menu,
menuAlignment,
menuMinScrollHeight
) {
var MIN_NUM_OPTIONS = 5;
var menuButton = new YAHOO.widget.Button(button, {
type: "menu",
menu: menu,
menualignment: menuAlignment,
menuminscrollheight: menuMinScrollHeight,
});

var filter = _createFilterMenuButton(menuButton._menu);

menuButton._menu.element.appendChild(filter);
menuButton._menu.showEvent.subscribe(function () {
_applyFilterKeyword(menuButton._menu, filter.firstElementChild);
filter.style.display =
_getItemList(menuButton._menu).children.length >= MIN_NUM_OPTIONS
? ""
: "NONE";
});
menuButton._menu.setInitialFocus = function () {
setTimeout(function () {
filter.firstElementChild.focus();
}, 0);
};

return menuButton;
};

function _createFilterMenuButton(menu) {
const filterInput = document.createElement("input");
filterInput.classList.add("jenkins-input");
filterInput.setAttribute("placeholder", "Filter");
filterInput.setAttribute("spellcheck", "false");
filterInput.setAttribute("type", "search");

filterInput.addEventListener("input", (event) =>
_applyFilterKeyword(menu, event.currentTarget),
);
filterInput.addEventListener("keypress", (event) => {
if (event.key === "Enter") {
event.preventDefault();
}
});

const filterContainer = document.createElement("div");
filterContainer.appendChild(filterInput);

return filterContainer;
}

function _applyFilterKeyword(menu, filterInput) {
const filterKeyword = (filterInput.value || "").toLowerCase();
const itemList = _getItemList(menu);
let item, match;
for (item of itemList.children) {
match = item.innerText.toLowerCase().includes(filterKeyword);
item.style.display = match ? "" : "NONE";
}
menu.align();
}

function _getItemList(menu) {
return menu.body.children[0];
}
5 changes: 3 additions & 2 deletions core/src/main/resources/lib/form/hetero-list.jelly
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ THE SOFTWARE.
</d:tag>
</d:taglib>

<st:adjunct includes="lib.form.hetero-list.hetero-list"/>

<j:set var="targetType" value="${attrs.targetType?:it.class}"/>
<div class="jenkins-form-item hetero-list-container ${hasHeader?'with-drag-drop':''} ${attrs.oneEach?'one-each':''} ${attrs.honorOrder?'honor-order':''}">
<!-- display existing items -->
Expand Down Expand Up @@ -154,8 +156,7 @@ THE SOFTWARE.

<j:if test="${!readOnlyMode}">
<div>
<button type="button" class="jenkins-button hetero-list-add" menualign="${attrs.menuAlign}" suffix="${attrs.name}">${attrs.addCaption?:'%Add'}<l:icon src="symbol-chevron-down"/>
</button>
<input type="button" value="${attrs.addCaption?:'%Add'}" class="hetero-list-add" menualign="${attrs.menuAlign}" suffix="${attrs.name}"/>
</div>
</j:if>
</div>
Expand Down
201 changes: 201 additions & 0 deletions core/src/main/resources/lib/form/hetero-list/hetero-list.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
// @include lib.form.filter-menu-button.filter-menu-button

// do the ones that extract innerHTML so that they can get their original HTML before
// other behavior rules change them (like YUI buttons.)
Behaviour.specify(
"DIV.hetero-list-container",
"hetero-list",
-100,
function (e) {
if (isInsideRemovable(e)) {
return;
}

// components for the add button
var menu = document.createElement("SELECT");
// In case nested content also uses hetero-list
var btn = Array.from(e.querySelectorAll("INPUT.hetero-list-add")).pop();
if (!btn) {
return;
}
YAHOO.util.Dom.insertAfter(menu, btn);

var prototypes = e.lastElementChild;
while (!prototypes.classList.contains("prototypes")) {
prototypes = prototypes.previousElementSibling;
}
var insertionPoint = prototypes.previousElementSibling; // this is where the new item is inserted.

// extract templates
var templates = [];
var children = prototypes.children;
for (var i = 0; i < children.length; i++) {
var n = children[i];
var name = n.getAttribute("name");
var tooltip = n.getAttribute("tooltip");
var descriptorId = n.getAttribute("descriptorId");
// YUI Menu interprets this <option> text node as HTML, so let's escape it again!
var title = n.getAttribute("title");
if (title) {
title = escapeHTML(title);
}
menu.options[i] = new Option(title, "" + i);
templates.push({
html: n.innerHTML,
name: name,
tooltip: tooltip,
descriptorId: descriptorId,
});
}
prototypes.remove();

// Initialize drag & drop for this component
var withDragDrop = registerSortableDragDrop(e);

var menuAlign = btn.getAttribute("menualign") || "tl-bl";

var menuButton = createFilterMenuButton(
btn,
menu,
menuAlign.split("-"),
250,
);
// copy class names
for (i = 0; i < btn.classList.length; i++) {
menuButton._button.classList.add(btn.classList.item(i));
}
menuButton._button.setAttribute("suffix", btn.getAttribute("suffix"));
menuButton.getMenu().clickEvent.subscribe(function (type, args) {
var item = args[1];
if (item.cfg.getProperty("disabled")) {
return;
}
var t = templates[parseInt(item.value)];

var nc = document.createElement("div");
nc.className = "repeated-chunk";
nc.setAttribute("name", t.name);
nc.setAttribute("descriptorId", t.descriptorId);
nc.innerHTML = t.html;
nc.style.opacity = "0";

renderOnDemand(
nc.querySelector("div.config-page"),
function () {
function findInsertionPoint() {
// given the element to be inserted 'prospect',
// and the array of existing items 'current',
// and preferred ordering function, return the position in the array
// the prospect should be inserted.
// (for example 0 if it should be the first item)
function findBestPosition(prospect, current, order) {
function desirability(pos) {
var count = 0;
for (var i = 0; i < current.length; i++) {
if (i < pos == order(current[i]) <= order(prospect)) {
count++;
}
}
return count;
}

var bestScore = -1;
var bestPos = 0;
for (var i = 0; i <= current.length; i++) {
var d = desirability(i);
if (bestScore <= d) {
// prefer to insert them toward the end
bestScore = d;
bestPos = i;
}
}
return bestPos;
}

var current = Array.from(e.children).filter(function (e) {
return e.matches("DIV.repeated-chunk");
});

function o(did) {
if (did instanceof Element) {
did = did.getAttribute("descriptorId");
}
for (var i = 0; i < templates.length; i++) {
if (templates[i].descriptorId == did) {
return i;
}
}
return 0; // can't happen
}

var bestPos = findBestPosition(t.descriptorId, current, o);
if (bestPos < current.length) {
return current[bestPos];
} else {
return insertionPoint;
}
}
var referenceNode = e.classList.contains("honor-order")
? findInsertionPoint()
: insertionPoint;
referenceNode.parentNode.insertBefore(nc, referenceNode);

// Initialize drag & drop for this component
if (withDragDrop) {
registerSortableDragDrop(nc);
}

new YAHOO.util.Anim(
nc,
{
opacity: { to: 1 },
},
0.2,
YAHOO.util.Easing.easeIn,
).animate();

Behaviour.applySubtree(nc, true);
ensureVisible(nc);
layoutUpdateCallback.call();
},
true,
);
});

menuButton.getMenu().renderEvent.subscribe(function () {
// hook up tooltip for menu items
var items = menuButton.getMenu().getItems();
for (i = 0; i < items.length; i++) {
var t = templates[i].tooltip;
if (t != null) {
applyTooltip(items[i].element, t);
}
}
});

// does this container already has a configured instance of the specified descriptor ID?
function has(id) {
return (
e.querySelector('DIV.repeated-chunk[descriptorId="' + id + '"]') != null
);
}

if (e.classList.contains("one-each")) {
menuButton.getMenu().showEvent.subscribe(function () {
var items = menuButton.getMenu().getItems();
for (i = 0; i < items.length; i++) {
items[i].cfg.setProperty("disabled", has(templates[i].descriptorId));
}
});
}
},
);

Behaviour.specify("DIV.dd-handle", "hetero-list", -100, function (e) {
e.addEventListener("mouseover", function () {
this.closest(".repeated-chunk").classList.add("hover");
});
e.addEventListener("mouseout", function () {
this.closest(".repeated-chunk").classList.remove("hover");
});
});
8 changes: 4 additions & 4 deletions test/src/test/java/lib/form/HeteroListTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
import org.htmlunit.html.HtmlButton;
import org.htmlunit.html.HtmlElementUtil;
import org.htmlunit.html.HtmlPage;
import org.htmlunit.javascript.host.html.HTMLButtonElement;
import org.htmlunit.javascript.host.html.HTMLAnchorElement;
import org.jenkinsci.Symbol;
import org.junit.Rule;
import org.junit.Test;
Expand All @@ -75,9 +75,9 @@ public void xssPrevented_heteroList_usingDescriptorDisplayName() throws Exceptio
HtmlPage page = wc.goTo("root");

page.executeJavaScript("document.querySelector('.hetero-list-add').click();");
Object result = page.executeJavaScript("document.querySelector('.jenkins-dropdown__item')").getJavaScriptResult();
assertThat(result, instanceOf(HTMLButtonElement.class));
HTMLButtonElement menuItem = (HTMLButtonElement) result;
Object result = page.executeJavaScript("document.querySelector('.yuimenuitem a')").getJavaScriptResult();
assertThat(result, instanceOf(HTMLAnchorElement.class));
HTMLAnchorElement menuItem = (HTMLAnchorElement) result;
String menuItemContent = menuItem.getInnerHTML();
assertThat(menuItemContent, not(containsString("<")));
}
Expand Down
Loading