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.loadLocalStorage();
this.buildCategoryList(); this.buildCategoryList();
this.setCompileMessage();
this.buildUI(); this.buildUI();
this.manager.setup(); this.manager.setup();
this.manager.output.saveBombe(); this.manager.output.saveBombe();
@ -385,14 +386,20 @@ class App {
this.updateFavourites(this.dfavourites, true); 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. * Adds an operation to the user's favourites.
* *
* @param {string} name - The name of the operation * @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); const favourites = JSON.parse(localStorage.favourites);
if (favourites.indexOf(name) >= 0) { if (favourites.indexOf(name) >= 0) {
@ -401,7 +408,25 @@ class App {
} }
favourites.push(name); 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"); 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 ) * Set desktop UI ( on init and on window resize events )
*/ */
setDesktopUI() { setDesktopUI() {
this.setCompileMessage(); this.setDownloadLinkVisibility(true);
this.setSplitter(); this.setSplitter();
this.manager.input.calcMaxTabs(); this.manager.input.calcMaxTabs();
@ -825,7 +859,9 @@ class App {
* Set mobile UI ( on init and on window resize events ) * Set mobile UI ( on init and on window resize events )
*/ */
setMobileUI() { setMobileUI() {
this.setDownloadLinkVisibility(false);
this.setSplitter(false); this.setSplitter(false);
this.assignAvailableHeight(); this.assignAvailableHeight();
$("[data-toggle=tooltip]").tooltip("disable"); $("[data-toggle=tooltip]").tooltip("disable");
} }
@ -859,6 +895,9 @@ class App {
* Build a CCategoryList component and append it to #categories * Build a CCategoryList component and append it to #categories
*/ */
buildCategoryList() { 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, // double-check if the c-category-list already exists,
if (document.querySelector("#categories > c-category-list")) { if (document.querySelector("#categories > c-category-list")) {
// then destroy it // then destroy it

View File

@ -105,6 +105,10 @@ export class CCategoryLi extends HTMLElement {
a.innerText = this.label; 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") { if (this.label === "Favourites") {
const editFavouritesButton = this.buildEditFavouritesButton(); 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> <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>`); </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; return a;
} }

View File

@ -1,4 +1,5 @@
import url from "url"; import url from "url";
import Utils from "../../core/Utils.mjs";
/** /**
* c(ustom element)-operation-li ( list item ) * 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 // Use mousedown event instead of click to prevent accidentally firing the handler twice on mobile
this.addEventListener("mousedown", this.handleMousedown.bind(this)); this.addEventListener("mousedown", this.handleMousedown.bind(this));
this.addEventListener("dblclick", this.handleDoubleClick.bind(this)); this.addEventListener("dblclick", this.handleDoubleClick.bind(this));
this.addEventListener("touchstart", this.handleTouchStart.bind(this));
if (this.includeStarIcon) { if (this.includeStarIcon) {
this.observer = new MutationObserver(this.updateFavourite.bind(this)); this.observer = new MutationObserver(this.updateFavourite.bind(this));
@ -50,6 +52,7 @@ export class COperationLi extends HTMLElement {
disconnectedCallback() { disconnectedCallback() {
this.removeEventListener("mousedown", this.handleMousedown.bind(this)); this.removeEventListener("mousedown", this.handleMousedown.bind(this));
this.removeEventListener("dblclick", this.handleDoubleClick.bind(this)); this.removeEventListener("dblclick", this.handleDoubleClick.bind(this));
this.removeEventListener("touchstart", this.handleTouchStart.bind(this));
if (this.includeStarIcon) { if (this.includeStarIcon) {
this.observer.disconnect(); this.observer.disconnect();
@ -62,6 +65,7 @@ export class COperationLi extends HTMLElement {
* Handle double click * Handle double click
*/ */
handleDoubleClick() { handleDoubleClick() {
this.app.manager.ops.clearSingleTapAlerts();
this.app.manager.recipe.addOperation(this.operationName); this.app.manager.recipe.addOperation(this.operationName);
} }
@ -72,13 +76,30 @@ export class COperationLi extends HTMLElement {
*/ */
handleMousedown(e) { handleMousedown(e) {
if (e.target === this.querySelector("i.star-icon")) { if (e.target === this.querySelector("i.star-icon")) {
this.app.addFavourite(this.operationName); if (!this.isFavourite) {
} this.app.addFavourite(this.operationName);
// current use case: in the 'Edit favourites' modal, the c-operation-li components have a trashcan icon to the this.isFavourite = true;
// right } else {
if (e.target === this.querySelector("i.remove-icon")) { 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(); 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 = ""; pageTitle = "";
switch (urlObj.host) { switch (urlObj.host) {
case "forensicswiki.xyz": case "forensics.wiki":
wikiName = "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; break;
case "wikipedia.org": case "wikipedia.org":
wikiName = "Wikipedia"; wikiName = "Wikipedia";

View File

@ -148,13 +148,13 @@
</button> </button>
<div id="content-wrapper"> <div id="content-wrapper">
<div id="banner"> <div id="banner">
<div> <div id="download-wrapper">
<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>"> <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> <span>Download CyberChef</span>
<i class="material-icons">file_download</i> <i class="material-icons banner-icon">file_download</i>
</a> </a>
</div> </div>
<div id="notice-wrapper" class="desktop-only"> <div id="notice-wrapper">
<span id="notice"> <span id="notice">
<script type="text/javascript"> <script type="text/javascript">
// Must be text/javascript rather than application/javascript otherwise IE won't recognise it... // 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-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."> 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> <span class="desktop-only">Options</span>
<i class="material-icons">settings</i> <i class="material-icons banner-icon">settings</i>
</a> </a>
<a href="#" <a href="#"
id="support" id="support"
@ -183,7 +183,7 @@
data-help-title="About / Support" 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."> 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> <span class="desktop-only">About / Support</span>
<i class="material-icons">help</i> <i class="material-icons banner-icon">help</i>
</a> </a>
</div> </div>
</div> </div>
@ -193,15 +193,17 @@
data-help-title="Operations list" 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>"> 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 Operations
<span class="op-count"></span> <div id="ops-title-right">
<span class="pane-controls"> <span class="op-count"></span>
<button type="button" <span class="pane-controls">
class="btn bmd-btn-icon mobile-only hidden" <button type="button"
id="close-ops-dropdown-icon" class="btn bmd-btn-icon mobile-only hidden"
title="Close dropdown"> id="close-ops-dropdown-icon"
<i class="material-icons">close</i> title="Close dropdown">
</button> <i class="material-icons">close</i>
</span> </button>
</span>
</div>
</div> </div>
<div id="operations-wrapper"> <div id="operations-wrapper">
<input id="search" <input id="search"

View File

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

View File

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

View File

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

View File

@ -59,3 +59,14 @@ c-category-list > ul {
padding: 8px 8px 9px 8px; 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; padding: 8px 8px 8px 12px;
} }
#operations-title-right {
display: flex;
align-items: center;
}
#operations-wrapper { #operations-wrapper {
position: relative; position: relative;
} }

View File

@ -23,6 +23,9 @@ class OperationsWaiter {
this.manager = manager; this.manager = manager;
this.options = {}; 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. * Handler for edit favourites click events.
* Sets up the 'Edit favourites' pane and displays it. * Sets up the 'Edit favourites' pane and displays it.
@ -323,18 +268,41 @@ class OperationsWaiter {
this.app.resetFavourites(); this.app.resetFavourites();
} }
/** /**
* Sets whether operation counts are displayed next to a category title * Sets whether operation counts are displayed next to a category title
*/ */
setCatCount() { setCatCount() {
if (this.app.options.showCatCount) { 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 { } 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; export default OperationsWaiter;

View File

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