diff --git a/src/web/HTMLOperation.mjs b/src/web/HTMLOperation.mjs index 8dcf1168..aa160ebc 100755 --- a/src/web/HTMLOperation.mjs +++ b/src/web/HTMLOperation.mjs @@ -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; @@ -86,11 +87,11 @@ class HTMLOperation { return ""; } - // Find all categories this operation belongs to, excluding Favourites + // 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.name)) { + if (cat.name !== "Favourites" && cat.ops.includes(this.originalName)) { categories.push(cat.name); } } @@ -99,11 +100,12 @@ class HTMLOperation { return ""; } - // Build the category links - const categoryLinks = categories.map(catName => { - const catId = "cat" + catName.replace(/[\s/\-:_]/g, ""); - return `${catName}`; - }).join(", "); + // 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 => + `${Utils.escapeHtml(catName)}` + ).join(", "); return `
Category: ${categoryLinks}`; } diff --git a/src/web/waiters/OperationsWaiter.mjs b/src/web/waiters/OperationsWaiter.mjs index ef6caeb8..39862ec9 100755 --- a/src/web/waiters/OperationsWaiter.mjs +++ b/src/web/waiters/OperationsWaiter.mjs @@ -354,8 +354,12 @@ class OperationsWaiter { e.preventDefault(); e.stopPropagation(); - const categoryId = e.target.dataset.category; - if (!categoryId) return; + // 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"); @@ -377,12 +381,15 @@ class OperationsWaiter { } } - // Close all categories and open the target one - $("#categories .collapse").collapse("hide"); - $(`#${categoryId}`).collapse("show"); + // 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 && !categoryElement.classList.contains("show")) { + $(categoryElement).collapse("show"); + } // Scroll the category into view - const categoryElement = document.getElementById(categoryId); if (categoryElement) { categoryElement.scrollIntoView({behavior: "smooth", block: "nearest"}); }