diff --git a/src/web/App.mjs b/src/web/App.mjs index 9f43a9d2..f66dd9b6 100755 --- a/src/web/App.mjs +++ b/src/web/App.mjs @@ -265,7 +265,7 @@ class App { if (this.ioSplitter) this.ioSplitter.destroy(); this.columnSplitter = Split(["#operations", "#recipe", "#IO"], { - sizes: [20, 30, 50], + sizes: [20, 40, 40], minSize: minimise ? [0, 0, 0] : [360, 330, 310], gutterSize: 4, expandToMin: true, @@ -510,7 +510,7 @@ class App { // Search for nearest match and add it const matchedOps = this.manager.ops.filterOperations(this.uriParams.op, false); if (matchedOps.length) { - this.manager.recipe.addOperation(matchedOps[0].name); + this.manager.recipe.addOperation(matchedOps[0][0]); } // Populate search with the string diff --git a/src/web/Manager.mjs b/src/web/Manager.mjs index 59590e0c..64884156 100755 --- a/src/web/Manager.mjs +++ b/src/web/Manager.mjs @@ -144,7 +144,7 @@ class Manager { document.getElementById("maximise-output").addEventListener("click", this.controls.onMaximiseButtonClick.bind(this.controls)); // 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("save-favourites").addEventListener("click", this.ops.saveFavouritesClick.bind(this.ops)); document.getElementById("reset-favourites").addEventListener("click", this.ops.resetFavouritesClick.bind(this.ops)); diff --git a/src/web/TODO.md b/src/web/TODO.md index f7b669d5..e69de29b 100644 --- a/src/web/TODO.md +++ b/src/web/TODO.md @@ -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 - diff --git a/src/web/components/c-category-li.mjs b/src/web/components/c-category-li.mjs index 8a49050b..baa1cf50 100644 --- a/src/web/components/c-category-li.mjs +++ b/src/web/components/c-category-li.mjs @@ -64,7 +64,7 @@ export class CCategoryLi extends HTMLElement { const opList = new COperationList( this.app, - this.category.ops, + this.category.ops.map( op => [op]), this.includeOpLiStarIcon, false, true @@ -81,7 +81,6 @@ export class CCategoryLi extends HTMLElement { buildListItem() { const li = document.createElement("li"); - li.classList.add("panel"); li.classList.add("category"); return li; diff --git a/src/web/components/c-operation-li.mjs b/src/web/components/c-operation-li.mjs index 112d4b26..f0acceb5 100644 --- a/src/web/components/c-operation-li.mjs +++ b/src/web/components/c-operation-li.mjs @@ -1,5 +1,4 @@ import url from "url"; -import HTMLIngredient from "../HTMLIngredient.mjs"; /** * 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 {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 {[number[]]} charIndicesToHighlight - optional array of indices that indicate characters to highlight (bold) in operation name */ export class COperationLi extends HTMLElement { constructor( @@ -15,6 +15,7 @@ export class COperationLi extends HTMLElement { name, icon, includeStarIcon, + charIndicesToHighlight = [] ) { super(); @@ -22,6 +23,7 @@ export class COperationLi extends HTMLElement { this.name = name; this.icon = icon; this.includeStarIcon = includeStarIcon; + this.charIndicesToHighlight = charIndicesToHighlight; this.config = this.app.operations[name]; @@ -54,7 +56,8 @@ export class COperationLi extends HTMLElement { * @param {Event} 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); } } @@ -169,6 +172,8 @@ export class COperationLi extends HTMLElement { buildListItem() { const li = document.createElement("li"); + li.appendChild( this.buildOperationName() ); + li.setAttribute("data-name", this.name); li.classList.add("operation"); @@ -176,7 +181,6 @@ export class COperationLi extends HTMLElement { li.classList.add("favourite"); } - li.textContent = this.name; if (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-content", dataContent); } - return li; } @@ -248,59 +251,62 @@ export class COperationLi extends HTMLElement { * with constructor arguments for sortable and cloneable lists */ cloneNode() { - const { app, name, icon, includeStarIcon } = this; - return new COperationLi( app, name, icon, includeStarIcon ); + const { app, name, icon, includeStarIcon, charIndicesToHighlight } = this; + return new COperationLi( app, name, icon, includeStarIcon, charIndicesToHighlight ); } /** * 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) { - if (nameIdxs.length && typeof nameIdxs[0][0] === "number") { + buildOperationName() { + const span = document.createElement('span'); + + if (this.charIndicesToHighlight.length) { let opName = "", pos = 0; - nameIdxs.forEach(idxs => { + this.charIndicesToHighlight.forEach(idxs => { const [start, length] = idxs; if (typeof start !== "number") return; - opName += this.name.slice(pos, start) + "" + - this.name.slice(start, start + length) + ""; + opName += this.name.slice(pos, start) + "" + + this.name.slice(start, start + length) + ""; pos = start + 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) { - // 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); + return span; - if (inHTMLTag) return; - } - - let desc = "", - pos = 0; - - descIdxs.forEach(idxs => { - const [start, length] = idxs; - desc += this.description.slice(pos, start) + "" + - this.description.slice(start, start + length) + ""; - pos = start + length; - }); - desc += this.description.slice(pos, this.description.length); - this.description = desc; - } + // if (this.description && descIdxs.length && descIdxs[0][0] >= 0) { + // // 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; + // } + // + // let desc = "", + // pos = 0; + // + // descIdxs.forEach(idxs => { + // const [start, length] = idxs; + // desc += this.description.slice(pos, start) + "" + + // this.description.slice(start, start + length) + ""; + // pos = start + length; + // }); + // desc += this.description.slice(pos, this.description.length); + // this.description = desc; + // } } } diff --git a/src/web/components/c-operation-list.mjs b/src/web/components/c-operation-list.mjs index d922a6ec..9bfb586a 100644 --- a/src/web/components/c-operation-list.mjs +++ b/src/web/components/c-operation-list.mjs @@ -5,14 +5,14 @@ import Sortable from "sortablejs"; * c(ustom element)-operation-list * * @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 {Object} icon ( { class: string, innerText: string } ). check-icon by default */ export class COperationList extends HTMLElement { constructor( app, - opNames, + operations, includeStarIcon, isSortable = false, isCloneable = true, @@ -21,7 +21,7 @@ export class COperationList extends HTMLElement { super(); this.app = app; - this.opNames = opNames; + this.operations = operations; this.includeStarIcon = includeStarIcon; this.isSortable = isSortable; this.isCloneable = isCloneable; @@ -35,7 +35,7 @@ export class COperationList extends HTMLElement { const ul = document.createElement("ul"); ul.classList.add("op-list"); - this.opNames.forEach((opName => { + this.operations.forEach((([opName, charIndicesToHighlight]) => { const cOpLi = new COperationLi( this.app, opName, @@ -43,7 +43,8 @@ export class COperationList extends HTMLElement { class: this.icon ? this.icon.class : "check-icon", innerText: this.icon ? this.icon.innerText : "check" }, - this.includeStarIcon + this.includeStarIcon, + charIndicesToHighlight ); ul.appendChild(cOpLi); diff --git a/src/web/stylesheets/components/_operation.css b/src/web/stylesheets/components/_operation.css index 05325f87..7eb02521 100755 --- a/src/web/stylesheets/components/_operation.css +++ b/src/web/stylesheets/components/_operation.css @@ -311,13 +311,13 @@ input.toggle-string { .break .form-group * { color: var(--breakpoint-font-colour) !important; } -li.operation.selected-op { - /*color: var(--selected-operation-font-color) !important;*/ - background-color: var(--selected-operation-bg-colour) !important; - /*border-color: var(--selected-operation-border-colour) !important;*/ +c-operation-li li.operation.focused-op { + color: var(--focused-operation-font-color) !important; + background-color: var(--focused-operation-bg-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 { color: var(--fc-operation-font-colour) !important; diff --git a/src/web/stylesheets/components/_recipe.css b/src/web/stylesheets/components/_recipe.css index 5ee46615..3b8aba68 100644 --- a/src/web/stylesheets/components/_recipe.css +++ b/src/web/stylesheets/components/_recipe.css @@ -27,3 +27,4 @@ #rec-list li.sortable-chosen{ filter: brightness(0.8); } + diff --git a/src/web/stylesheets/themes/_classic.css b/src/web/stylesheets/themes/_classic.css index 4d5c2b43..e3fee882 100755 --- a/src/web/stylesheets/themes/_classic.css +++ b/src/web/stylesheets/themes/_classic.css @@ -54,11 +54,12 @@ --rec-list-operation-bg-colour: #dff0d8; --rec-list-operation-border-colour: #d3e8c0; - --selected-operation-font-color: #c09853; - --selected-operation-bg-colour: #d5ebf5; - --selected-operation-border-colour: #fbeed5; + --focused-operation-font-color: #c09853; + --focused-operation-bg-colour: #fcf8e3; + --focused-operation-border-colour: #fbeed5; + + --selected-operation-bg-colour: #d5ebf5; - /*mobile UI: selected operation checkmark*/ --checkmark-color: var(--op-list-operation-font-colour); --breakpoint-font-colour: #b94a48; diff --git a/src/web/stylesheets/themes/_dark.css b/src/web/stylesheets/themes/_dark.css index 37fa6381..d914e7e7 100755 --- a/src/web/stylesheets/themes/_dark.css +++ b/src/web/stylesheets/themes/_dark.css @@ -50,9 +50,11 @@ --rec-list-operation-bg-colour: #252525; --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-border-colour: #444; --checkmark-color: #47d047; diff --git a/src/web/stylesheets/themes/_geocities.css b/src/web/stylesheets/themes/_geocities.css index fcf3cdb5..2e96a836 100755 --- a/src/web/stylesheets/themes/_geocities.css +++ b/src/web/stylesheets/themes/_geocities.css @@ -50,9 +50,9 @@ --rec-list-operation-bg-colour: purple; --rec-list-operation-border-colour: green; - --selected-operation-font-color: white; - --selected-operation-bg-colour: pink; - --selected-operation-border-colour: blue; + --focused-operation-font-color: white; + --focused-operation-bg-colour: pink; + --focused-operation-border-colour: blue; --breakpoint-font-colour: white; --breakpoint-bg-colour: red; diff --git a/src/web/stylesheets/themes/_solarizedDark.css b/src/web/stylesheets/themes/_solarizedDark.css index 5bb18d2e..724e61b9 100755 --- a/src/web/stylesheets/themes/_solarizedDark.css +++ b/src/web/stylesheets/themes/_solarizedDark.css @@ -69,9 +69,9 @@ --rec-list-operation-bg-colour: var(--base02); --rec-list-operation-border-colour: var(--base01); - --selected-operation-font-color: var(--base1); - --selected-operation-bg-colour: var(--base02); - --selected-operation-border-colour: var(--base01); + --focused-operation-font-color: var(--base1); + --focused-operation-bg-colour: var(--base02); + --focused-operation-border-colour: var(--base01); --breakpoint-font-colour: var(--sol-red); --breakpoint-bg-colour: var(--base02); diff --git a/src/web/stylesheets/themes/_solarizedLight.css b/src/web/stylesheets/themes/_solarizedLight.css index f884c3e8..11b8778c 100755 --- a/src/web/stylesheets/themes/_solarizedLight.css +++ b/src/web/stylesheets/themes/_solarizedLight.css @@ -69,9 +69,9 @@ --rec-list-operation-bg-colour: var(--base2); --rec-list-operation-border-colour: var(--base1); - --selected-operation-font-color: var(--base01); - --selected-operation-bg-colour: var(--base2); - --selected-operation-border-colour: var(--base1); + --focused-operation-font-color: var(--base01); + --focused-operation-bg-colour: var(--base2); + --focused-operation-border-colour: var(--base1); --breakpoint-font-colour: var(--sol-red); --breakpoint-bg-colour: var(--base2); diff --git a/src/web/waiters/OperationsWaiter.mjs b/src/web/waiters/OperationsWaiter.mjs index b2463a3a..9d5cb70e 100755 --- a/src/web/waiters/OperationsWaiter.mjs +++ b/src/web/waiters/OperationsWaiter.mjs @@ -34,8 +34,7 @@ class OperationsWaiter { * @param {Event} e */ searchOperations(e) { - let ops, selected; - + let ops, focused; if (e.type === "keyup") { 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(); ops = document.querySelectorAll("#search-results c-operation-list c-operation-li li"); if (ops.length) { - selected = this.getSelectedOp(ops); - if (selected > -1) { - this.manager.recipe.addOperation(ops[selected].getAttribute("data-name")); + focused = this.getFocusedOp(ops); + if (focused > -1) { + this.manager.recipe.addOperation(ops[focused].getAttribute("data-name")); } } } @@ -60,28 +59,28 @@ class OperationsWaiter { if (e.type === "click" && !e.target.value.length) { this.openOpsDropdown(); } else if (e.key === "Escape") { // Escape - this.closeOpsDropdown() + this.closeOpsDropdown(); } else if (e.key === "ArrowDown") { // Down e.preventDefault(); ops = document.querySelectorAll("#search-results c-operation-list c-operation-li li"); if (ops.length) { - selected = this.getSelectedOp(ops); - if (selected > -1) { - ops[selected].classList.remove("selected-op"); + focused = this.getFocusedOp(ops); + if (focused > -1) { + ops[focused].classList.remove("focused-op"); } - if (selected === ops.length-1) selected = -1; - ops[selected+1].classList.add("selected-op"); + if (focused === ops.length-1) focused = -1; + ops[focused+1].classList.add("focused-op"); } } else if (e.key === "ArrowUp") { // Up e.preventDefault(); ops = document.querySelectorAll("#search-results c-operation-list c-operation-li li"); if (ops.length) { - selected = this.getSelectedOp(ops); - if (selected > -1) { - ops[selected].classList.remove("selected-op"); + focused = this.getFocusedOp(ops); + if (focused > -1) { + ops[focused].classList.remove("focused-op"); } - if (selected === 0) selected = ops.length; - ops[selected-1].classList.add("selected-op"); + if (focused === 0) focused = ops.length; + ops[focused-1].classList.add("focused-op"); } } else { const searchResultsEl = document.getElementById("search-results"); @@ -99,15 +98,10 @@ class OperationsWaiter { if (str) { const matchedOps = this.filterOperations(str, true); - let formattedOpNames = []; - - matchedOps.forEach((operation) => { - formattedOpNames.push(operation.name.replace(/(<([^>]+)>)/ig, "")); - }) const cOpList = new COperationList( this.app, - formattedOpNames, + matchedOps, true, false, true, @@ -132,15 +126,15 @@ class OperationsWaiter { * @param {string} searchStr * @param {boolean} highlight - Whether to highlight the matching string in the operation * name and description - * @returns {string[]} + * @returns {[[string, number[]]]} */ - filterOperations(inStr, highlight) { + filterOperations(searchStr, highlight) { const matchedOps = []; const matchedDescs = []; // Create version with no whitespace for the fuzzy match // 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) { const op = this.app.operations[opName]; @@ -149,26 +143,13 @@ class OperationsWaiter { const [nameMatch, score, idxs] = fuzzyMatch(inStrNWS, opName); // 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) { - const operation = new COperationLi( - this.app, - opName, - { - class: "check-icon", - innerText: "check" - }, - true ); - - if (highlight) { - operation.highlightSearchStrings(calcMatchRanges(idxs), [[descPos, inStr.length]]); - } - if (nameMatch) { - matchedOps.push([operation, score]); + matchedOps.push([[opName, calcMatchRanges(idxs)], score]); } 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 - * 'selected-op' set. Returns the index of the operation within the given list. + * Finds the operation which has been focused on using keyboard shortcuts. This will have the class + * 'focused-op' set. Returns the index of the operation within the given list. * * @param {element[]} ops * @returns {number} */ - getSelectedOp(ops) { + getFocusedOp(ops) { for (let i = 0; i < ops.length; i++) { - if (ops[i].classList.contains("selected-op")) { + if (ops[i].classList.contains("focused-op")) { return i; } } @@ -216,7 +197,7 @@ class OperationsWaiter { if(favCatConfig !== undefined) { const opList = new COperationList( this.app, - favCatConfig.ops, + favCatConfig.ops.map( op => [op]), false, true, false,