Skip to content
Open
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
38 changes: 37 additions & 1 deletion src/web/HTMLOperation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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'
Expand All @@ -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.
*
Expand Down
2 changes: 2 additions & 0 deletions src/web/Manager.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
7 changes: 7 additions & 0 deletions src/web/html/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -529,6 +529,13 @@ <h5 class="modal-title">Options</h5>
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>
Expand Down
1 change: 1 addition & 0 deletions src/web/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ function main() {
imagePreview: true,
syncTabs: true,
showCatCount: false,
showOpCategories: true,
};

document.removeEventListener("DOMContentLoaded", main, false);
Expand Down
79 changes: 79 additions & 0 deletions src/web/waiters/OperationsWaiter.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
74 changes: 74 additions & 0 deletions tests/browser/00_nightwatch.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down