Update to support added features since last master merge

- Operation counts
- Allow toggling on favourite icon
- Notify users that a single tap does nothing in the operations list
- Fix odd checkbox label position on macos
- Fix odd label pinning
- History update now happens on drag operation removal
- Show compile message rather than "download cyberchef" in banner
This commit is contained in:
Matt C 2026-02-06 15:37:20 -05:00
parent 9d3567e0e6
commit d020c53dfb
11 changed files with 157 additions and 89 deletions

View file

@ -67,6 +67,7 @@ class App {
this.loadLocalStorage();
this.buildCategoryList();
this.setCompileMessage();
this.buildUI();
this.manager.setup();
this.manager.output.saveBombe();
@ -385,14 +386,20 @@ class App {
this.updateFavourites(this.dfavourites, true);
}
/**
* Checks if the favourites category is expanded
*/
isFavouritesExpanded() {
const catFavourites = document.getElementById("#catFavourites");
return catFavourites ? catFavourites.classList.contains("show") : false;
}
/**
* Adds an operation to the user's favourites.
*
* @param {string} name - The name of the operation
* @param {Boolean} isExpanded - false by default
*/
addFavourite(name, isExpanded = false) {
addFavourite(name) {
const favourites = JSON.parse(localStorage.favourites);
if (favourites.indexOf(name) >= 0) {
@ -401,7 +408,25 @@ class App {
}
favourites.push(name);
this.updateFavourites(favourites, isExpanded);
this.updateFavourites(favourites, this.isFavouritesExpanded());
}
/**
* Removes an operation from the user's favourites.
*
* @param {string} name - The name of the operation
*/
removeFavourite(name) {
const favourites = JSON.parse(localStorage.favourites);
const index = favourites.indexOf(name);
if (index === -1) {
this.alert(`'${name}' isn't in your favourites`, 3000);
return;
}
favourites.splice(index, 1);
this.updateFavourites(favourites, this.isFavouritesExpanded());
}
@ -810,11 +835,20 @@ class App {
return isVisible ? elm.classList.remove("hidden") : elm.classList.add("hidden");
}
/**
* Set visibility of download link
*
* @param {boolean} isVisible
*/
setDownloadLinkVisibility(isVisible) {
this.setElementVisibility(document.getElementById("download-wrapper"), true);
}
/**
* Set desktop UI ( on init and on window resize events )
*/
setDesktopUI() {
this.setCompileMessage();
this.setDownloadLinkVisibility(true);
this.setSplitter();
this.manager.input.calcMaxTabs();
@ -825,7 +859,9 @@ class App {
* Set mobile UI ( on init and on window resize events )
*/
setMobileUI() {
this.setDownloadLinkVisibility(false);
this.setSplitter(false);
this.assignAvailableHeight();
$("[data-toggle=tooltip]").tooltip("disable");
}
@ -859,6 +895,9 @@ class App {
* Build a CCategoryList component and append it to #categories
*/
buildCategoryList() {
// populate total operations count
document.querySelector("#operations .title .op-count").innerText = Object.keys(this.operations).length;
// double-check if the c-category-list already exists,
if (document.querySelector("#categories > c-category-list")) {
// then destroy it

View file

@ -105,6 +105,10 @@ export class CCategoryLi extends HTMLElement {
a.innerText = this.label;
// Create wrapper for right-aligned controls
const divRight = document.createElement("div");
divRight.setAttribute("class", "category-title-right");
if (this.label === "Favourites") {
const editFavouritesButton = this.buildEditFavouritesButton();
@ -119,9 +123,20 @@ export class CCategoryLi extends HTMLElement {
<li><b>To remove:</b> Click on the 'Edit favourites' button and hit the delete button next to the operation you want to remove</li>
</ul>`);
a.appendChild(editFavouritesButton);
divRight.appendChild(editFavouritesButton);
}
// Show count of operations in category
const opCountSpan = document.createElement("span");
opCountSpan.classList.add("op-count");
if (!this.app.options.showCatCount) {
opCountSpan.classList.add("hidden");
}
opCountSpan.innerText = this.category.ops.length;
divRight.appendChild(opCountSpan);
a.appendChild(divRight);
return a;
}

View file

@ -1,4 +1,5 @@
import url from "url";
import Utils from "../../core/Utils.mjs";
/**
* c(ustom element)-operation-li ( list item )
@ -37,6 +38,7 @@ export class COperationLi extends HTMLElement {
// Use mousedown event instead of click to prevent accidentally firing the handler twice on mobile
this.addEventListener("mousedown", this.handleMousedown.bind(this));
this.addEventListener("dblclick", this.handleDoubleClick.bind(this));
this.addEventListener("touchstart", this.handleTouchStart.bind(this));
if (this.includeStarIcon) {
this.observer = new MutationObserver(this.updateFavourite.bind(this));
@ -50,6 +52,7 @@ export class COperationLi extends HTMLElement {
disconnectedCallback() {
this.removeEventListener("mousedown", this.handleMousedown.bind(this));
this.removeEventListener("dblclick", this.handleDoubleClick.bind(this));
this.removeEventListener("touchstart", this.handleTouchStart.bind(this));
if (this.includeStarIcon) {
this.observer.disconnect();
@ -62,6 +65,7 @@ export class COperationLi extends HTMLElement {
* Handle double click
*/
handleDoubleClick() {
this.app.manager.ops.clearSingleTapAlerts();
this.app.manager.recipe.addOperation(this.operationName);
}
@ -72,13 +76,30 @@ export class COperationLi extends HTMLElement {
*/
handleMousedown(e) {
if (e.target === this.querySelector("i.star-icon")) {
this.app.addFavourite(this.operationName);
}
// current use case: in the 'Edit favourites' modal, the c-operation-li components have a trashcan icon to the
// right
if (e.target === this.querySelector("i.remove-icon")) {
if (!this.isFavourite) {
this.app.addFavourite(this.operationName);
this.isFavourite = true;
} else {
this.app.removeFavourite(this.operationName);
this.isFavourite = false;
}
} else if (e.target === this.querySelector("i.remove-icon")) {
// current use case: in the 'Edit favourites' modal, the c-operation-li components have a trashcan icon to the
// right
this.remove();
} else {
return;
}
// if we've handled another event, don't use this to trigger doubleclick
e.preventDefault();
}
/**
* If the user taps a single operation, alert them that doubletapping adds operation to recipe.
* @param {TouchEvent} e
*/
handleTouchStart(e) {
this.app.manager.ops.sendSingleTapAlert();
}
/**
@ -157,9 +178,9 @@ export class COperationLi extends HTMLElement {
pageTitle = "";
switch (urlObj.host) {
case "forensicswiki.xyz":
case "forensics.wiki":
wikiName = "Forensics Wiki";
pageTitle = urlObj.query.substr(6).replace(/_/g, " "); // Chop off 'title='
pageTitle = Utils.toTitleCase(urlObj.path.replace(/\//g, "").replace(/_/g, " "));
break;
case "wikipedia.org":
wikiName = "Wikipedia";

View file

@ -148,13 +148,13 @@
</button>
<div id="content-wrapper">
<div id="banner">
<div>
<a href="#" class="banner-link" data-toggle="modal" data-target="#download-modal" data-help-title="Downloading CyberChef" data-help="<p>CyberChef can be downloaded to run locally or hosted within your own network. It has no server-side component so all that is required is that the ZIP file is uncompressed and the files are accessible.</p><p>As a user, it is worth noting that unofficial versions of CyberChef could have been modified to introduce Input and/or Recipe exfiltration. We recommend always using the official, open source, up-to-date version of CyberChef hosted at <a href='https://gchq.github.io/CyberChef'>https://gchq.github.io/CyberChef</a> if accessible.</p><p>The Network tab in your browser's Developer console (F12) can be used to inspect the network requests made by a website. This can confirm that no data is uploaded when a CyberChef recipe is baked.</p>">
<div id="download-wrapper">
<a href="#" class="banner-link desktop-only" data-toggle="modal" data-target="#download-modal" data-help-title="Downloading CyberChef" data-help="<p>CyberChef can be downloaded to run locally or hosted within your own network. It has no server-side component so all that is required is that the ZIP file is uncompressed and the files are accessible.</p><p>As a user, it is worth noting that unofficial versions of CyberChef could have been modified to introduce Input and/or Recipe exfiltration. We recommend always using the official, open source, up-to-date version of CyberChef hosted at <a href='https://gchq.github.io/CyberChef'>https://gchq.github.io/CyberChef</a> if accessible.</p><p>The Network tab in your browser's Developer console (F12) can be used to inspect the network requests made by a website. This can confirm that no data is uploaded when a CyberChef recipe is baked.</p>">
<span>Download CyberChef</span>
<i class="material-icons">file_download</i>
<i class="material-icons banner-icon">file_download</i>
</a>
</div>
<div id="notice-wrapper" class="desktop-only">
<div id="notice-wrapper">
<span id="notice">
<script type="text/javascript">
// Must be text/javascript rather than application/javascript otherwise IE won't recognise it...
@ -173,7 +173,7 @@
data-help-title="Options and Settings"
data-help="Configurable options to change how CyberChef behaves. These settings are stored in your browser's local storage, meaning they will persist between sessions that use the same browser profile.">
<span class="desktop-only">Options</span>
<i class="material-icons">settings</i>
<i class="material-icons banner-icon">settings</i>
</a>
<a href="#"
id="support"
@ -183,7 +183,7 @@
data-help-title="About / Support"
data-help="This pane provides information about the CyberChef web app, how to use some of the features, and how to raise bug reports.">
<span class="desktop-only">About / Support</span>
<i class="material-icons">help</i>
<i class="material-icons banner-icon">help</i>
</a>
</div>
</div>
@ -193,15 +193,17 @@
data-help-title="Operations list"
data-help="<p>The Operations list contains all the operations in CyberChef arranged into categories. Some operations may be present in multiple categories. You can search for operations using the search box.</p><p>To use an operation, either double click it, or drag it into the Recipe pane. You will then be able to configure its arguments (or 'Ingredients' in CyberChef terminology).</p>">
Operations
<span class="op-count"></span>
<span class="pane-controls">
<button type="button"
class="btn bmd-btn-icon mobile-only hidden"
id="close-ops-dropdown-icon"
title="Close dropdown">
<i class="material-icons">close</i>
</button>
</span>
<div id="ops-title-right">
<span class="op-count"></span>
<span class="pane-controls">
<button type="button"
class="btn bmd-btn-icon mobile-only hidden"
id="close-ops-dropdown-icon"
title="Close dropdown">
<i class="material-icons">close</i>
</button>
</span>
</div>
</div>
<div id="operations-wrapper">
<input id="search"

View file

@ -47,5 +47,9 @@
}
}
.banner-icon {
transform: translateY(-1.5px);
}

View file

@ -56,6 +56,8 @@
font-size: initial;
color: var(--primary-font-colour);
cursor: pointer;
display: inline-flex;
gap: 5px;
}
#auto-bake-label .check,
@ -65,6 +67,7 @@
}
#auto-bake-label .checkbox-decorator {
display: inline-flex;
position: relative;
margin: 0;
padding: 0;

View file

@ -111,7 +111,6 @@
/* Right hand side icons */
.rhs {
position: fixed;
right: 15px;
}

View file

@ -59,3 +59,14 @@ c-category-list > ul {
padding: 8px 8px 9px 8px;
}
.category-title-right {
display: flex;
}
.op-count {
color: var(--subtext-font-colour);
font-weight: normal;
font-size: xx-small;
opacity: 0.5;
padding-left: .5em;
}

View file

@ -13,6 +13,11 @@
padding: 8px 8px 8px 12px;
}
#operations-title-right {
display: flex;
align-items: center;
}
#operations-wrapper {
position: relative;
}

View file

@ -23,6 +23,9 @@ class OperationsWaiter {
this.manager = manager;
this.options = {};
this.lastSingleTapAlert = null;
this.singleTapAlertTimeout = null;
}
@ -174,64 +177,6 @@ class OperationsWaiter {
}
/**
* Handler for edit favourites click events.
* Displays the 'Edit favourites' modal and handles the c-operation-list in the modal
*
* @param {Event} e
*/
opListCreate(e) {
this.manager.recipe.createSortableSeedList(e.target);
// Populate ops total
document.querySelector("#operations .title .op-count").innerText = Object.keys(this.app.operations).length;
this.enableOpsListPopovers(e.target);
}
/**
* Sets up popovers, allowing the popover itself to gain focus which enables scrolling
* and other interactions.
*
* @param {Element} el - The element to start selecting from
*/
enableOpsListPopovers(el) {
$(el).find("[data-toggle=popover]").addBack("[data-toggle=popover]")
.popover({trigger: "manual"})
.on("mouseenter", function(e) {
if (e.buttons > 0) return; // Mouse button held down - likely dragging an operation
const _this = this;
$(this).popover("show");
$(".popover").on("mouseleave", function () {
$(_this).popover("hide");
});
}).on("mouseleave", function () {
const _this = this;
setTimeout(function() {
// Determine if the popover associated with this element is being hovered over
if ($(_this).data("bs.popover") &&
($(_this).data("bs.popover").tip && !$($(_this).data("bs.popover").tip).is(":hover"))) {
$(_this).popover("hide");
}
}, 50);
});
}
/**
* Handler for operation doubleclick events.
* Adds the operation to the recipe and auto bakes.
*
* @param {event} e
*/
operationDblclick(e) {
const li = e.target;
this.manager.recipe.addOperation(li.textContent);
}
/**
* Handler for edit favourites click events.
* Sets up the 'Edit favourites' pane and displays it.
@ -323,18 +268,41 @@ class OperationsWaiter {
this.app.resetFavourites();
}
/**
* Sets whether operation counts are displayed next to a category title
*/
setCatCount() {
if (this.app.options.showCatCount) {
document.querySelectorAll(".category-title .op-count").forEach(el => el.classList.remove("hidden"));
document.querySelectorAll(".category-title .category-title-right .op-count").forEach(el => el.classList.remove("hidden"));
} else {
document.querySelectorAll(".category-title .op-count").forEach(el => el.classList.add("hidden"));
document.querySelectorAll(".category-title .category-title-right .op-count").forEach(el => el.classList.add("hidden"));
}
}
/**
* Handles alerts for single tap events on mobile. Will disallow more than one every
* 30 seconds.
*/
sendSingleTapAlert() {
clearTimeout(this.singleTapAlertTimeout);
this.singleTapAlertTimeout = setTimeout(() => {
const now = new Date();
// Only display alert if we've not seen one before or in at least 30 seconds.
if (!this.lastSingleTapAlert || now.getTime() - this.lastSingleTapAlert.getTime() > 30_000) {
this.lastSingleTapAlert = now;
this.app.alert("Double tap an operation to add it to your recipe.", 3000);
}
}, 500);
}
/**
* Clear pending single tap alerts.
*/
clearSingleTapAlerts() {
clearTimeout(this.singleTapAlertTimeout);
this.singleTapAlertTimeout = null;
}
}
export default OperationsWaiter;

View file

@ -63,6 +63,7 @@ class RecipeWaiter {
onEnd: function(e) {
if (this.removeIntent) {
e.item.remove();
document.dispatchEvent(this.manager.statechange);
e.target.dispatchEvent(this.manager.operationremove);
}
}.bind(this),