Merge aea2b5fa2513a141c4636998f9a6dba5f77cf35b into 4290ea753912378913b1f3f54e0fc5720afeda5d

This commit is contained in:
alleria173 2026-08-10 00:01:41 -07:00 committed by GitHub
commit d23b365044
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 200 additions and 1 deletions

View File

@ -27,6 +27,7 @@ class HTMLOperation {
this.manager = manager;
this.name = name;
this.originalName = name;
this.description = config.description;
this.infoURL = config.infoURL;
this.manualBake = config.manualBake || false;
@ -56,7 +57,8 @@ class HTMLOperation {
if (this.description) {
const infoLink = this.infoURL ? `<hr>${titleFromWikiLink(this.infoURL)}` : "";
const content = Utils.escapeHtml(this.description + infoLink);
const categoryInfo = this.getCategoryInfo();
const content = Utils.escapeHtml(this.description + infoLink + categoryInfo);
html += ` data-container='body' data-toggle='popover' data-placement='right'
data-content="${content}" data-html='true' data-trigger='hover'
@ -75,6 +77,40 @@ class HTMLOperation {
}
/**
* Gets the category information for this operation as an HTML string.
*
* @returns {string}
*/
getCategoryInfo() {
if (!this.app.options.showOpCategories) {
return "";
}
// Use originalName because this.name may have been modified by highlightSearchStrings
const categories = [];
for (let i = 0; i < this.app.categories.length; i++) {
const cat = this.app.categories[i];
if (cat.name !== "Favourites" && cat.ops.includes(this.originalName)) {
categories.push(cat.name);
}
}
if (categories.length === 0) {
return "";
}
// Build the category links. Note that Bootstrap's popover sanitizer strips
// custom data-* attributes, so the category name is carried in the link text
// and resolved to a category ID by OperationsWaiter.categoryLinkClick.
const categoryLinks = categories.map(catName =>
`<a href='#' class='op-category-link'>${Utils.escapeHtml(catName)}</a>`
).join(", ");
return `<hr>Category: ${categoryLinks}`;
}
/**
* Renders the operation in HTML as a full operation with ingredients.
*

View File

@ -232,6 +232,8 @@ class Manager {
this.addDynamicListener(".option-item input[type=checkbox]#wordWrap", "change", this.options.setWordWrap, this.options);
this.addDynamicListener(".option-item input[type=checkbox]#useMetaKey", "change", this.bindings.updateKeybList, this.bindings);
this.addDynamicListener(".option-item input[type=checkbox]#showCatCount", "change", this.ops.setCatCount, this.ops);
this.addDynamicListener(".option-item input[type=checkbox]#showOpCategories", "change", this.ops.toggleOpCategories, this.ops);
this.addDynamicListener(".op-category-link", "click", this.ops.categoryLinkClick, this.ops);
this.addDynamicListener(".option-item input[type=number]", "keyup", this.options.numberChange, this.options);
this.addDynamicListener(".option-item input[type=number]", "change", this.options.numberChange, this.options);
this.addDynamicListener(".option-item select", "change", this.options.selectChange, this.options);

View File

@ -529,6 +529,13 @@
Show the number of operations in each category
</label>
</div>
<div class="checkbox option-item">
<label for="showOpCategories">
<input type="checkbox" option="showOpCategories" id="showOpCategories" checked>
Show operation category in popover
</label>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" id="reset-options">Reset options to default</button>

View File

@ -53,6 +53,7 @@ function main() {
imagePreview: true,
syncTabs: true,
showCatCount: false,
showOpCategories: true,
};
document.removeEventListener("DOMContentLoaded", main, false);

View File

@ -326,6 +326,85 @@ class OperationsWaiter {
}
}
/**
* Handler for toggling the display of operation categories in popovers.
* Repopulates the operations list to show/hide category information.
*/
toggleOpCategories() {
// Repopulate operations list to apply the option change
this.app.populateOperationsList();
this.manager.recipe.initialiseOperationDragNDrop();
// Refresh search results if search is active
const searchInput = document.getElementById("search");
if (searchInput && searchInput.value) {
this.searchOperations({target: searchInput});
}
}
/**
* Handler for clicking a category link in an operation popover.
* Clears the search and opens the clicked category.
*
* @param {event} e
*/
categoryLinkClick(e) {
e.preventDefault();
e.stopPropagation();
// Bootstrap's popover sanitizer strips custom data-* attributes, so the
// category ID is derived from the link text using the same scheme as
// HTMLCategory.toHtml()
const catName = e.target.textContent;
if (!catName) return;
const categoryId = "cat" + catName.replace(/[\s/\-:_]/g, "");
// Hide all popovers
$("[data-toggle=popover]").popover("hide");
// Clear search
const searchInput = document.getElementById("search");
if (searchInput) {
searchInput.value = "";
}
// Clear search results
const searchResults = document.getElementById("search-results");
if (searchResults) {
while (searchResults.firstChild) {
try {
$(searchResults.firstChild).popover("dispose");
} catch (err) {}
searchResults.removeChild(searchResults.firstChild);
}
}
// Open the target category. The accordion behaviour (data-parent) closes
// any other open category automatically. Calling "show" on an already
// open category would toggle-close it mid-transition, so skip it.
const categoryElement = document.getElementById(categoryId);
if (!categoryElement) return;
const showCategory = function() {
if (!categoryElement.classList.contains("show")) {
$(categoryElement).collapse("show");
}
categoryElement.scrollIntoView({behavior: "smooth", block: "nearest"});
};
// Bootstrap ignores "show" while another panel in the accordion is still
// mid-transition (e.g. a category collapsed by the search box a moment
// earlier), so wait for any in-progress transition to finish first.
const transitioning = document.querySelector("#categories .collapsing");
if (transitioning) {
$(transitioning).one("hidden.bs.collapse shown.bs.collapse", showCategory);
} else {
showCategory();
}
}
}
export default OperationsWaiter;

View File

@ -252,6 +252,80 @@ module.exports = {
.waitForElementVisible("//ul[@id='search-results']//b[text()='MD5']", 1000);
},
"Operation category in popover": browser => {
const op = "//ul[@id='search-results']//li[contains(@class, 'operation') and contains(., 'MD5')]";
// Search for an operation
browser
.useCss()
.clearValue("#search")
.setValue("#search", "md5")
.useXpath()
.waitForElementVisible(op, 1000);
// Hover over the operation to show popover
browser
.moveToElement(op, 10, 10)
.useCss()
.waitForElementVisible(".popover-body", 1000);
// Assert that Category line appears in popover
browser
.expect.element(".popover-body").text.to.contain("Category:");
// Click the category link
browser
.click(".popover-body .op-category-link");
// Assert that search is cleared
browser
.useCss()
.expect.element("#search").value.to.equal("");
// Assert that the Hashing category is now visible
browser
.expect.element("#catHashing").to.be.visible;
// Toggle the option off. The checkbox input is restyled by Bootstrap
// Material Design and is not directly interactable, so click its label.
browser
.click("#options");
browser
.waitForElementVisible("#options-modal", 1000)
.click("label[for='showOpCategories']")
.pause(500)
.click("#options-modal .modal-footer .btn-secondary[data-dismiss='modal']")
.waitForElementNotVisible("#options-modal", 1000);
// Search again and verify category line doesn't appear
browser
.clearValue("#search")
.setValue("#search", "md5")
.useXpath()
.waitForElementVisible(op, 1000)
.moveToElement(op, 10, 10)
.useCss()
.waitForElementVisible(".popover-body", 1000);
// Assert that Category line does not appear
browser
.expect.element(".popover-body").text.to.not.contain("Category:");
// Reset option back to enabled
browser
.click("#options")
.waitForElementVisible("#options-modal", 1000)
.click("label[for='showOpCategories']")
.pause(500)
.click("#options-modal .modal-footer .btn-secondary[data-dismiss='modal']")
.waitForElementNotVisible("#options-modal", 1000);
// Clear search
browser
.clearValue("#search");
},
"Alert bar": browser => {
// Bake nothing to create an empty output which can be copied
utils.clear(browser);