search result highlighted strings, do not add op to recipe on escape

This commit is contained in:
Robin Scholtes 2023-08-07 16:42:06 +12:00
parent 85fff21068
commit f176a1106a
14 changed files with 108 additions and 125 deletions

View file

@ -265,7 +265,7 @@ class App {
if (this.ioSplitter) this.ioSplitter.destroy(); if (this.ioSplitter) this.ioSplitter.destroy();
this.columnSplitter = Split(["#operations", "#recipe", "#IO"], { this.columnSplitter = Split(["#operations", "#recipe", "#IO"], {
sizes: [20, 30, 50], sizes: [20, 40, 40],
minSize: minimise ? [0, 0, 0] : [360, 330, 310], minSize: minimise ? [0, 0, 0] : [360, 330, 310],
gutterSize: 4, gutterSize: 4,
expandToMin: true, expandToMin: true,
@ -510,7 +510,7 @@ class App {
// Search for nearest match and add it // Search for nearest match and add it
const matchedOps = this.manager.ops.filterOperations(this.uriParams.op, false); const matchedOps = this.manager.ops.filterOperations(this.uriParams.op, false);
if (matchedOps.length) { if (matchedOps.length) {
this.manager.recipe.addOperation(matchedOps[0].name); this.manager.recipe.addOperation(matchedOps[0][0]);
} }
// Populate search with the string // Populate search with the string

View file

@ -144,7 +144,7 @@ class Manager {
document.getElementById("maximise-output").addEventListener("click", this.controls.onMaximiseButtonClick.bind(this.controls)); document.getElementById("maximise-output").addEventListener("click", this.controls.onMaximiseButtonClick.bind(this.controls));
// Operations // Operations
this.addMultiEventListener("#search", "keyup paste search click", this.ops.searchOperations, this.ops); this.addMultiEventListener("#search", "keyup paste click", this.ops.searchOperations, this.ops);
document.getElementById("close-ops-dropdown-icon").addEventListener("click", this.ops.closeOpsDropdown.bind(this.ops)); document.getElementById("close-ops-dropdown-icon").addEventListener("click", this.ops.closeOpsDropdown.bind(this.ops));
document.getElementById("save-favourites").addEventListener("click", this.ops.saveFavouritesClick.bind(this.ops)); document.getElementById("save-favourites").addEventListener("click", this.ops.saveFavouritesClick.bind(this.ops));
document.getElementById("reset-favourites").addEventListener("click", this.ops.resetFavouritesClick.bind(this.ops)); document.getElementById("reset-favourites").addEventListener("click", this.ops.resetFavouritesClick.bind(this.ops));

View file

@ -1,8 +0,0 @@
- ignore dropped item outside of rec-list
- can only drag an op to favourites 1 time
- stupid popovers on deleting favs for instance ( dont always close nicely )
- UI tests etc.
- esc on selected-op search results will add that op to recipe
- highlight strings
- initial search is kinda slow

View file

@ -64,7 +64,7 @@ export class CCategoryLi extends HTMLElement {
const opList = new COperationList( const opList = new COperationList(
this.app, this.app,
this.category.ops, this.category.ops.map( op => [op]),
this.includeOpLiStarIcon, this.includeOpLiStarIcon,
false, false,
true true
@ -81,7 +81,6 @@ export class CCategoryLi extends HTMLElement {
buildListItem() { buildListItem() {
const li = document.createElement("li"); const li = document.createElement("li");
li.classList.add("panel");
li.classList.add("category"); li.classList.add("category");
return li; return li;

View file

@ -1,5 +1,4 @@
import url from "url"; import url from "url";
import HTMLIngredient from "../HTMLIngredient.mjs";
/** /**
* c(ustom element)-operation-li ( list item ) * c(ustom element)-operation-li ( list item )
@ -8,6 +7,7 @@ import HTMLIngredient from "../HTMLIngredient.mjs";
* @param {string} name - The name of the operation * @param {string} name - The name of the operation
* @param {Object} icon - { class: string, innerText: string } - The optional and customizable icon displayed on the right side of the operation * @param {Object} icon - { class: string, innerText: string } - The optional and customizable icon displayed on the right side of the operation
* @param {Boolean} includeStarIcon - Include the left side 'star' icon to favourite an operation easily * @param {Boolean} includeStarIcon - Include the left side 'star' icon to favourite an operation easily
* @param {[number[]]} charIndicesToHighlight - optional array of indices that indicate characters to highlight (bold) in operation name
*/ */
export class COperationLi extends HTMLElement { export class COperationLi extends HTMLElement {
constructor( constructor(
@ -15,6 +15,7 @@ export class COperationLi extends HTMLElement {
name, name,
icon, icon,
includeStarIcon, includeStarIcon,
charIndicesToHighlight = []
) { ) {
super(); super();
@ -22,6 +23,7 @@ export class COperationLi extends HTMLElement {
this.name = name; this.name = name;
this.icon = icon; this.icon = icon;
this.includeStarIcon = includeStarIcon; this.includeStarIcon = includeStarIcon;
this.charIndicesToHighlight = charIndicesToHighlight;
this.config = this.app.operations[name]; this.config = this.app.operations[name];
@ -54,7 +56,8 @@ export class COperationLi extends HTMLElement {
* @param {Event} e * @param {Event} e
*/ */
handleDoubleClick(e) { handleDoubleClick(e) {
if (e.target === this.querySelector("li")) { // Span contains operation title (highlighted or not)
if (e.target === this.querySelector("li") || e.target === this.querySelector("span")) {
this.app.manager.recipe.addOperation(this.name); this.app.manager.recipe.addOperation(this.name);
} }
} }
@ -169,6 +172,8 @@ export class COperationLi extends HTMLElement {
buildListItem() { buildListItem() {
const li = document.createElement("li"); const li = document.createElement("li");
li.appendChild( this.buildOperationName() );
li.setAttribute("data-name", this.name); li.setAttribute("data-name", this.name);
li.classList.add("operation"); li.classList.add("operation");
@ -176,7 +181,6 @@ export class COperationLi extends HTMLElement {
li.classList.add("favourite"); li.classList.add("favourite");
} }
li.textContent = this.name;
if (this.config.description){ if (this.config.description){
let dataContent = this.config.description; let dataContent = this.config.description;
@ -193,7 +197,6 @@ export class COperationLi extends HTMLElement {
li.setAttribute("data-boundary", "viewport"); li.setAttribute("data-boundary", "viewport");
li.setAttribute("data-content", dataContent); li.setAttribute("data-content", dataContent);
} }
return li; return li;
} }
@ -248,59 +251,62 @@ export class COperationLi extends HTMLElement {
* with constructor arguments for sortable and cloneable lists * with constructor arguments for sortable and cloneable lists
*/ */
cloneNode() { cloneNode() {
const { app, name, icon, includeStarIcon } = this; const { app, name, icon, includeStarIcon, charIndicesToHighlight } = this;
return new COperationLi( app, name, icon, includeStarIcon ); return new COperationLi( app, name, icon, includeStarIcon, charIndicesToHighlight );
} }
/** /**
* Highlights searched strings in the name and description of the operation. * Highlights searched strings in the name and description of the operation.
*
* @param {[[number]]} nameIdxs - Indexes of the search strings in the operation name [[start, length]]
* @param {[[number]]} descIdxs - Indexes of the search strings in the operation description [[start, length]]
*/ */
highlightSearchStrings(nameIdxs, descIdxs) { buildOperationName() {
if (nameIdxs.length && typeof nameIdxs[0][0] === "number") { const span = document.createElement('span');
if (this.charIndicesToHighlight.length) {
let opName = "", let opName = "",
pos = 0; pos = 0;
nameIdxs.forEach(idxs => { this.charIndicesToHighlight.forEach(idxs => {
const [start, length] = idxs; const [start, length] = idxs;
if (typeof start !== "number") return; if (typeof start !== "number") return;
opName += this.name.slice(pos, start) + "<b>" + opName += this.name.slice(pos, start) + "<strong>" +
this.name.slice(start, start + length) + "</b>"; this.name.slice(start, start + length) + "</strong>";
pos = start + length; pos = start + length;
}); });
opName += this.name.slice(pos, this.name.length); opName += this.name.slice(pos, this.name.length);
this.name = opName; span.innerHTML = opName;
} else {
span.innerText = this.name;
} }
if (this.description && descIdxs.length && descIdxs[0][0] >= 0) { return span;
// Find HTML tag offsets
const re = /<[^>]+>/g;
let match;
while ((match = re.exec(this.description))) {
// If the search string occurs within an HTML tag, return without highlighting it.
const inHTMLTag = descIdxs.reduce((acc, idxs) => {
const start = idxs[0];
return start >= match.index && start <= (match.index + match[0].length);
}, false);
if (inHTMLTag) return; // if (this.description && descIdxs.length && descIdxs[0][0] >= 0) {
} // // Find HTML tag offsets
// const re = /<[^>]+>/g;
let desc = "", // let match;
pos = 0; // while ((match = re.exec(this.description))) {
// // If the search string occurs within an HTML tag, return without highlighting it.
descIdxs.forEach(idxs => { // const inHTMLTag = descIdxs.reduce((acc, idxs) => {
const [start, length] = idxs; // const start = idxs[0];
desc += this.description.slice(pos, start) + "<b><u>" + // return start >= match.index && start <= (match.index + match[0].length);
this.description.slice(start, start + length) + "</u></b>"; // }, false);
pos = start + length; //
}); // if (inHTMLTag) return;
desc += this.description.slice(pos, this.description.length); // }
this.description = desc; //
} // let desc = "",
// pos = 0;
//
// descIdxs.forEach(idxs => {
// const [start, length] = idxs;
// desc += this.description.slice(pos, start) + "<b><u>" +
// this.description.slice(start, start + length) + "</u></b>";
// pos = start + length;
// });
// desc += this.description.slice(pos, this.description.length);
// this.description = desc;
// }
} }
} }

View file

@ -5,14 +5,14 @@ import Sortable from "sortablejs";
* c(ustom element)-operation-list * c(ustom element)-operation-list
* *
* @param {App} app - The main view object for CyberChef * @param {App} app - The main view object for CyberChef
* @param {string[]} opNames - A list of operation names * @param {[string, number[]]} operations - A list of operation names and indexes of characters to highlight
* @param {boolean} includeStarIcon - optionally add the 'star' icon to the left of the operation * @param {boolean} includeStarIcon - optionally add the 'star' icon to the left of the operation
* @param {Object} icon ( { class: string, innerText: string } ). check-icon by default * @param {Object} icon ( { class: string, innerText: string } ). check-icon by default
*/ */
export class COperationList extends HTMLElement { export class COperationList extends HTMLElement {
constructor( constructor(
app, app,
opNames, operations,
includeStarIcon, includeStarIcon,
isSortable = false, isSortable = false,
isCloneable = true, isCloneable = true,
@ -21,7 +21,7 @@ export class COperationList extends HTMLElement {
super(); super();
this.app = app; this.app = app;
this.opNames = opNames; this.operations = operations;
this.includeStarIcon = includeStarIcon; this.includeStarIcon = includeStarIcon;
this.isSortable = isSortable; this.isSortable = isSortable;
this.isCloneable = isCloneable; this.isCloneable = isCloneable;
@ -35,7 +35,7 @@ export class COperationList extends HTMLElement {
const ul = document.createElement("ul"); const ul = document.createElement("ul");
ul.classList.add("op-list"); ul.classList.add("op-list");
this.opNames.forEach((opName => { this.operations.forEach((([opName, charIndicesToHighlight]) => {
const cOpLi = new COperationLi( const cOpLi = new COperationLi(
this.app, this.app,
opName, opName,
@ -43,7 +43,8 @@ export class COperationList extends HTMLElement {
class: this.icon ? this.icon.class : "check-icon", class: this.icon ? this.icon.class : "check-icon",
innerText: this.icon ? this.icon.innerText : "check" innerText: this.icon ? this.icon.innerText : "check"
}, },
this.includeStarIcon this.includeStarIcon,
charIndicesToHighlight
); );
ul.appendChild(cOpLi); ul.appendChild(cOpLi);

View file

@ -311,13 +311,13 @@ input.toggle-string {
.break .form-group * { color: var(--breakpoint-font-colour) !important; } .break .form-group * { color: var(--breakpoint-font-colour) !important; }
li.operation.selected-op { c-operation-li li.operation.focused-op {
/*color: var(--selected-operation-font-color) !important;*/ color: var(--focused-operation-font-color) !important;
background-color: var(--selected-operation-bg-colour) !important; background-color: var(--focused-operation-bg-colour) !important;
/*border-color: var(--selected-operation-border-colour) !important;*/ border-color: var(--focused-operation-border-colour) !important;
} }
/*.selected-op .form-group * { color: var(--selected-operation-font-color) !important; }*/ /*.focused-op .form-group * { color: var(--focused-operation-font-color) !important; }*/
.flow-control-op { .flow-control-op {
color: var(--fc-operation-font-colour) !important; color: var(--fc-operation-font-colour) !important;

View file

@ -27,3 +27,4 @@
#rec-list li.sortable-chosen{ #rec-list li.sortable-chosen{
filter: brightness(0.8); filter: brightness(0.8);
} }

View file

@ -54,11 +54,12 @@
--rec-list-operation-bg-colour: #dff0d8; --rec-list-operation-bg-colour: #dff0d8;
--rec-list-operation-border-colour: #d3e8c0; --rec-list-operation-border-colour: #d3e8c0;
--selected-operation-font-color: #c09853; --focused-operation-font-color: #c09853;
--selected-operation-bg-colour: #d5ebf5; --focused-operation-bg-colour: #fcf8e3;
--selected-operation-border-colour: #fbeed5; --focused-operation-border-colour: #fbeed5;
--selected-operation-bg-colour: #d5ebf5;
/*mobile UI: selected operation checkmark*/
--checkmark-color: var(--op-list-operation-font-colour); --checkmark-color: var(--op-list-operation-font-colour);
--breakpoint-font-colour: #b94a48; --breakpoint-font-colour: #b94a48;

View file

@ -50,9 +50,11 @@
--rec-list-operation-bg-colour: #252525; --rec-list-operation-bg-colour: #252525;
--rec-list-operation-border-colour: #444; --rec-list-operation-border-colour: #444;
--selected-operation-font-color: #c5c5c5; --focused-operation-font-color: #c5c5c5;
--focused-operation-bg-colour: #475663;
--focused-operation-border-colour: #444;
--selected-operation-bg-colour: #3f3f3f; --selected-operation-bg-colour: #3f3f3f;
--selected-operation-border-colour: #444;
--checkmark-color: #47d047; --checkmark-color: #47d047;

View file

@ -50,9 +50,9 @@
--rec-list-operation-bg-colour: purple; --rec-list-operation-bg-colour: purple;
--rec-list-operation-border-colour: green; --rec-list-operation-border-colour: green;
--selected-operation-font-color: white; --focused-operation-font-color: white;
--selected-operation-bg-colour: pink; --focused-operation-bg-colour: pink;
--selected-operation-border-colour: blue; --focused-operation-border-colour: blue;
--breakpoint-font-colour: white; --breakpoint-font-colour: white;
--breakpoint-bg-colour: red; --breakpoint-bg-colour: red;

View file

@ -69,9 +69,9 @@
--rec-list-operation-bg-colour: var(--base02); --rec-list-operation-bg-colour: var(--base02);
--rec-list-operation-border-colour: var(--base01); --rec-list-operation-border-colour: var(--base01);
--selected-operation-font-color: var(--base1); --focused-operation-font-color: var(--base1);
--selected-operation-bg-colour: var(--base02); --focused-operation-bg-colour: var(--base02);
--selected-operation-border-colour: var(--base01); --focused-operation-border-colour: var(--base01);
--breakpoint-font-colour: var(--sol-red); --breakpoint-font-colour: var(--sol-red);
--breakpoint-bg-colour: var(--base02); --breakpoint-bg-colour: var(--base02);

View file

@ -69,9 +69,9 @@
--rec-list-operation-bg-colour: var(--base2); --rec-list-operation-bg-colour: var(--base2);
--rec-list-operation-border-colour: var(--base1); --rec-list-operation-border-colour: var(--base1);
--selected-operation-font-color: var(--base01); --focused-operation-font-color: var(--base01);
--selected-operation-bg-colour: var(--base2); --focused-operation-bg-colour: var(--base2);
--selected-operation-border-colour: var(--base1); --focused-operation-border-colour: var(--base1);
--breakpoint-font-colour: var(--sol-red); --breakpoint-font-colour: var(--sol-red);
--breakpoint-bg-colour: var(--base2); --breakpoint-bg-colour: var(--base2);

View file

@ -34,8 +34,7 @@ class OperationsWaiter {
* @param {Event} e * @param {Event} e
*/ */
searchOperations(e) { searchOperations(e) {
let ops, selected; let ops, focused;
if (e.type === "keyup") { if (e.type === "keyup") {
const searchResults = document.getElementById("search-results"); const searchResults = document.getElementById("search-results");
@ -46,13 +45,13 @@ class OperationsWaiter {
} }
} }
if (e.type === "search" || e.key === "Enter") { // Search or Return ( enter ) if (e.key === "Enter") { // Search or Return ( enter )
e.preventDefault(); e.preventDefault();
ops = document.querySelectorAll("#search-results c-operation-list c-operation-li li"); ops = document.querySelectorAll("#search-results c-operation-list c-operation-li li");
if (ops.length) { if (ops.length) {
selected = this.getSelectedOp(ops); focused = this.getFocusedOp(ops);
if (selected > -1) { if (focused > -1) {
this.manager.recipe.addOperation(ops[selected].getAttribute("data-name")); this.manager.recipe.addOperation(ops[focused].getAttribute("data-name"));
} }
} }
} }
@ -60,28 +59,28 @@ class OperationsWaiter {
if (e.type === "click" && !e.target.value.length) { if (e.type === "click" && !e.target.value.length) {
this.openOpsDropdown(); this.openOpsDropdown();
} else if (e.key === "Escape") { // Escape } else if (e.key === "Escape") { // Escape
this.closeOpsDropdown() this.closeOpsDropdown();
} else if (e.key === "ArrowDown") { // Down } else if (e.key === "ArrowDown") { // Down
e.preventDefault(); e.preventDefault();
ops = document.querySelectorAll("#search-results c-operation-list c-operation-li li"); ops = document.querySelectorAll("#search-results c-operation-list c-operation-li li");
if (ops.length) { if (ops.length) {
selected = this.getSelectedOp(ops); focused = this.getFocusedOp(ops);
if (selected > -1) { if (focused > -1) {
ops[selected].classList.remove("selected-op"); ops[focused].classList.remove("focused-op");
} }
if (selected === ops.length-1) selected = -1; if (focused === ops.length-1) focused = -1;
ops[selected+1].classList.add("selected-op"); ops[focused+1].classList.add("focused-op");
} }
} else if (e.key === "ArrowUp") { // Up } else if (e.key === "ArrowUp") { // Up
e.preventDefault(); e.preventDefault();
ops = document.querySelectorAll("#search-results c-operation-list c-operation-li li"); ops = document.querySelectorAll("#search-results c-operation-list c-operation-li li");
if (ops.length) { if (ops.length) {
selected = this.getSelectedOp(ops); focused = this.getFocusedOp(ops);
if (selected > -1) { if (focused > -1) {
ops[selected].classList.remove("selected-op"); ops[focused].classList.remove("focused-op");
} }
if (selected === 0) selected = ops.length; if (focused === 0) focused = ops.length;
ops[selected-1].classList.add("selected-op"); ops[focused-1].classList.add("focused-op");
} }
} else { } else {
const searchResultsEl = document.getElementById("search-results"); const searchResultsEl = document.getElementById("search-results");
@ -99,15 +98,10 @@ class OperationsWaiter {
if (str) { if (str) {
const matchedOps = this.filterOperations(str, true); const matchedOps = this.filterOperations(str, true);
let formattedOpNames = [];
matchedOps.forEach((operation) => {
formattedOpNames.push(operation.name.replace(/(<([^>]+)>)/ig, ""));
})
const cOpList = new COperationList( const cOpList = new COperationList(
this.app, this.app,
formattedOpNames, matchedOps,
true, true,
false, false,
true, true,
@ -132,15 +126,15 @@ class OperationsWaiter {
* @param {string} searchStr * @param {string} searchStr
* @param {boolean} highlight - Whether to highlight the matching string in the operation * @param {boolean} highlight - Whether to highlight the matching string in the operation
* name and description * name and description
* @returns {string[]} * @returns {[[string, number[]]]}
*/ */
filterOperations(inStr, highlight) { filterOperations(searchStr, highlight) {
const matchedOps = []; const matchedOps = [];
const matchedDescs = []; const matchedDescs = [];
// Create version with no whitespace for the fuzzy match // Create version with no whitespace for the fuzzy match
// Helps avoid missing matches e.g. query "TCP " would not find "Parse TCP" // Helps avoid missing matches e.g. query "TCP " would not find "Parse TCP"
const inStrNWS = inStr.replace(/\s/g, ""); const inStrNWS = searchStr.replace(/\s/g, "");
for (const opName in this.app.operations) { for (const opName in this.app.operations) {
const op = this.app.operations[opName]; const op = this.app.operations[opName];
@ -149,26 +143,13 @@ class OperationsWaiter {
const [nameMatch, score, idxs] = fuzzyMatch(inStrNWS, opName); const [nameMatch, score, idxs] = fuzzyMatch(inStrNWS, opName);
// Match description based on exact match // Match description based on exact match
const descPos = op.description.toLowerCase().indexOf(inStr.toLowerCase()); const descPos = op.description.toLowerCase().indexOf(searchStr.toLowerCase());
if (nameMatch || descPos >= 0) { if (nameMatch || descPos >= 0) {
const operation = new COperationLi(
this.app,
opName,
{
class: "check-icon",
innerText: "check"
},
true );
if (highlight) {
operation.highlightSearchStrings(calcMatchRanges(idxs), [[descPos, inStr.length]]);
}
if (nameMatch) { if (nameMatch) {
matchedOps.push([operation, score]); matchedOps.push([[opName, calcMatchRanges(idxs)], score]);
} else { } else {
matchedDescs.push(operation); matchedDescs.push([opName]);
} }
} }
} }
@ -181,15 +162,15 @@ class OperationsWaiter {
/** /**
* Finds the operation which has been selected using keyboard shortcuts. This will have the class * Finds the operation which has been focused on using keyboard shortcuts. This will have the class
* 'selected-op' set. Returns the index of the operation within the given list. * 'focused-op' set. Returns the index of the operation within the given list.
* *
* @param {element[]} ops * @param {element[]} ops
* @returns {number} * @returns {number}
*/ */
getSelectedOp(ops) { getFocusedOp(ops) {
for (let i = 0; i < ops.length; i++) { for (let i = 0; i < ops.length; i++) {
if (ops[i].classList.contains("selected-op")) { if (ops[i].classList.contains("focused-op")) {
return i; return i;
} }
} }
@ -216,7 +197,7 @@ class OperationsWaiter {
if(favCatConfig !== undefined) { if(favCatConfig !== undefined) {
const opList = new COperationList( const opList = new COperationList(
this.app, this.app,
favCatConfig.ops, favCatConfig.ops.map( op => [op]),
false, false,
true, true,
false, false,