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"});
}