From ae3a97a00fb57e2b47ee42ba2619c13d566a833c Mon Sep 17 00:00:00 2001 From: e218736 <147728997+e218736@users.noreply.github.com> Date: Mon, 12 Feb 2024 09:04:31 +0000 Subject: [PATCH 1/7] enable tooltips trigger on focus --- src/web/waiters/ControlsWaiter.mjs | 130 ++++++++++++++++------------- 1 file changed, 74 insertions(+), 56 deletions(-) diff --git a/src/web/waiters/ControlsWaiter.mjs b/src/web/waiters/ControlsWaiter.mjs index 6a0ef6f2..35941a13 100755 --- a/src/web/waiters/ControlsWaiter.mjs +++ b/src/web/waiters/ControlsWaiter.mjs @@ -6,12 +6,10 @@ import Utils from "../../core/Utils.mjs"; - /** * Waiter to handle events related to the CyberChef controls (i.e. Bake, Step, Save, Load etc.) */ class ControlsWaiter { - /** * ControlsWaiter constructor. * @@ -23,7 +21,6 @@ class ControlsWaiter { this.manager = manager; } - /** * Initialise Bootstrap components */ @@ -33,11 +30,10 @@ class ControlsWaiter { animation: false, container: "body", boundary: "viewport", - trigger: "hover" + trigger: "hover focus", }); } - /** * Checks or unchecks the Auto Bake checkbox based on the given value. * @@ -51,7 +47,6 @@ class ControlsWaiter { } } - /** * Handler to trigger baking. */ @@ -64,7 +59,6 @@ class ControlsWaiter { } } - /** * Handler for the 'Step through' command. Executes the next step of the recipe. */ @@ -72,7 +66,6 @@ class ControlsWaiter { this.app.step(); } - /** * Handler for changes made to the Auto Bake checkbox. */ @@ -80,7 +73,6 @@ class ControlsWaiter { this.app.autoBake_ = document.getElementById("auto-bake").checked; } - /** * Handler for the 'Clear recipe' command. Removes all operations from the recipe. */ @@ -88,7 +80,6 @@ class ControlsWaiter { this.manager.recipe.clearRecipe(); } - /** * Populates the save dialog box with a URL incorporating the recipe and input. * @@ -97,16 +88,23 @@ class ControlsWaiter { initialiseSaveLink(recipeConfig) { recipeConfig = recipeConfig || this.app.getRecipeConfig(); - const includeRecipe = document.getElementById("save-link-recipe-checkbox").checked; - const includeInput = document.getElementById("save-link-input-checkbox").checked; + const includeRecipe = document.getElementById( + "save-link-recipe-checkbox" + ).checked; + const includeInput = document.getElementById("save-link-input-checkbox") + .checked; const saveLinkEl = document.getElementById("save-link"); - const saveLink = this.generateStateUrl(includeRecipe, includeInput, null, recipeConfig); + const saveLink = this.generateStateUrl( + includeRecipe, + includeInput, + null, + recipeConfig + ); saveLinkEl.innerHTML = Utils.escapeHtml(Utils.truncate(saveLink, 120)); saveLinkEl.setAttribute("href", saveLink); } - /** * Generates a URL containing the current recipe and input state. * @@ -117,15 +115,24 @@ class ControlsWaiter { * @param {string} [baseURL] - The CyberChef URL, set to the current URL if not included * @returns {string} */ - generateStateUrl(includeRecipe, includeInput, input, recipeConfig, baseURL) { + generateStateUrl( + includeRecipe, + includeInput, + input, + recipeConfig, + baseURL + ) { recipeConfig = recipeConfig || this.app.getRecipeConfig(); - const link = baseURL || window.location.protocol + "//" + - window.location.host + - window.location.pathname; + const link = + baseURL || + window.location.protocol + + "//" + + window.location.host + + window.location.pathname; const recipeStr = Utils.generatePrettyRecipe(recipeConfig); - includeRecipe = includeRecipe && (recipeConfig.length > 0); + includeRecipe = includeRecipe && recipeConfig.length > 0; // If we don't get passed an input, get it from the current URI if (input === null && includeInput) { @@ -145,15 +152,17 @@ class ControlsWaiter { const params = [ includeRecipe ? ["recipe", recipeStr] : undefined, - includeInput && input.length ? ["input", Utils.escapeHtml(input)] : undefined, + includeInput && input.length + ? ["input", Utils.escapeHtml(input)] + : undefined, inputChrEnc !== 0 ? ["ienc", inputChrEnc] : undefined, outputChrEnc !== 0 ? ["oenc", outputChrEnc] : undefined, inputEOLSeq !== "\n" ? ["ieol", inputEOLSeq] : undefined, - outputEOLSeq !== "\n" ? ["oeol", outputEOLSeq] : undefined + outputEOLSeq !== "\n" ? ["oeol", outputEOLSeq] : undefined, ]; const hash = params - .filter(v => v) + .filter((v) => v) .map(([key, value]) => `${key}=${Utils.encodeURIFragment(value)}`) .join("&"); @@ -164,7 +173,6 @@ class ControlsWaiter { return link; } - /** * Handler for changes made to the save dialog text area. Re-initialises the save link. */ @@ -175,7 +183,6 @@ class ControlsWaiter { } catch (err) {} } - /** * Handler for the 'Save' command. Pops up the save dialog box. */ @@ -183,9 +190,15 @@ class ControlsWaiter { const recipeConfig = this.app.getRecipeConfig(); const recipeStr = JSON.stringify(recipeConfig); - document.getElementById("save-text-chef").value = Utils.generatePrettyRecipe(recipeConfig, true); - document.getElementById("save-text-clean").value = JSON.stringify(recipeConfig, null, 2) - .replace(/{\n\s+"/g, "{ \"") + document.getElementById( + "save-text-chef" + ).value = Utils.generatePrettyRecipe(recipeConfig, true); + document.getElementById("save-text-clean").value = JSON.stringify( + recipeConfig, + null, + 2 + ) + .replace(/{\n\s+"/g, '{ "') .replace(/\[\n\s{3,}/g, "[") .replace(/\n\s{3,}]/g, "]") .replace(/\s*\n\s*}/g, " }") @@ -196,7 +209,6 @@ class ControlsWaiter { $("#save-modal").modal(); } - /** * Handler for the save link recipe checkbox change event. */ @@ -204,7 +216,6 @@ class ControlsWaiter { this.initialiseSaveLink(); } - /** * Handler for the save link input checkbox change event. */ @@ -212,7 +223,6 @@ class ControlsWaiter { this.initialiseSaveLink(); } - /** * Handler for the 'Load' command. Pops up the load dialog box. */ @@ -221,7 +231,6 @@ class ControlsWaiter { $("#load-modal").modal(); } - /** * Saves the recipe specified in the save textarea to local storage. */ @@ -234,22 +243,27 @@ class ControlsWaiter { return false; } - const recipeName = Utils.escapeHtml(document.getElementById("save-name").value); - const recipeStr = document.querySelector("#save-texts .tab-pane.active textarea").value; + const recipeName = Utils.escapeHtml( + document.getElementById("save-name").value + ); + const recipeStr = document.querySelector( + "#save-texts .tab-pane.active textarea" + ).value; if (!recipeName) { this.app.alert("Please enter a recipe name", 3000); return; } - const savedRecipes = localStorage.savedRecipes ? - JSON.parse(localStorage.savedRecipes) : []; + const savedRecipes = localStorage.savedRecipes + ? JSON.parse(localStorage.savedRecipes) + : []; let recipeId = localStorage.recipeId || 0; savedRecipes.push({ id: ++recipeId, name: recipeName, - recipe: recipeStr + recipe: recipeStr, }); localStorage.savedRecipes = JSON.stringify(savedRecipes); @@ -258,7 +272,6 @@ class ControlsWaiter { this.app.alert(`Recipe saved as "${recipeName}".`, 3000); } - /** * Populates the list of saved recipes in the load dialog box from local storage. */ @@ -274,14 +287,17 @@ class ControlsWaiter { } // Add recipes to select - const savedRecipes = localStorage.savedRecipes ? - JSON.parse(localStorage.savedRecipes) : []; + const savedRecipes = localStorage.savedRecipes + ? JSON.parse(localStorage.savedRecipes) + : []; for (i = 0; i < savedRecipes.length; i++) { const opt = document.createElement("option"); opt.value = savedRecipes[i].id; // Unescape then re-escape in case localStorage has been corrupted - opt.innerHTML = Utils.escapeHtml(Utils.unescapeHtml(savedRecipes[i].name)); + opt.innerHTML = Utils.escapeHtml( + Utils.unescapeHtml(savedRecipes[i].name) + ); loadNameEl.appendChild(opt); } @@ -293,7 +309,6 @@ class ControlsWaiter { loadText.dispatchEvent(evt); } - /** * Removes the currently selected recipe from local storage. */ @@ -301,16 +316,16 @@ class ControlsWaiter { if (!this.app.isLocalStorageAvailable()) return false; const id = parseInt(document.getElementById("load-name").value, 10); - const rawSavedRecipes = localStorage.savedRecipes ? - JSON.parse(localStorage.savedRecipes) : []; + const rawSavedRecipes = localStorage.savedRecipes + ? JSON.parse(localStorage.savedRecipes) + : []; - const savedRecipes = rawSavedRecipes.filter(r => r.id !== id); + const savedRecipes = rawSavedRecipes.filter((r) => r.id !== id); localStorage.savedRecipes = JSON.stringify(savedRecipes); this.populateLoadRecipesList(); } - /** * Displays the selected recipe in the load text box. */ @@ -318,22 +333,24 @@ class ControlsWaiter { if (!this.app.isLocalStorageAvailable()) return false; const el = e.target; - const savedRecipes = localStorage.savedRecipes ? - JSON.parse(localStorage.savedRecipes) : []; + const savedRecipes = localStorage.savedRecipes + ? JSON.parse(localStorage.savedRecipes) + : []; const id = parseInt(el.value, 10); - const recipe = savedRecipes.find(r => r.id === id); + const recipe = savedRecipes.find((r) => r.id === id); document.getElementById("load-text").value = recipe.recipe; } - /** * Loads the selected recipe and populates the Recipe with its operations. */ loadButtonClick() { try { - const recipeConfig = Utils.parseRecipeConfig(document.getElementById("load-text").value); + const recipeConfig = Utils.parseRecipeConfig( + document.getElementById("load-text").value + ); this.app.setRecipeConfig(recipeConfig); this.app.autoBake(); @@ -343,7 +360,6 @@ class ControlsWaiter { } } - /** * Populates the bug report information box with useful technical info. * @@ -353,7 +369,13 @@ class ControlsWaiter { e.preventDefault(); const reportBugInfo = document.getElementById("report-bug-info"); - const saveLink = this.generateStateUrl(true, true, null, null, "https://gchq.github.io/CyberChef/"); + const saveLink = this.generateStateUrl( + true, + true, + null, + null, + "https://gchq.github.io/CyberChef/" + ); if (reportBugInfo) { reportBugInfo.innerHTML = `* Version: ${PKG_VERSION} @@ -366,7 +388,6 @@ ${navigator.userAgent} } } - /** * Shows the stale indicator to show that the input or recipe has changed * since the last bake. @@ -376,7 +397,6 @@ ${navigator.userAgent} staleIndicator.classList.remove("hidden"); } - /** * Hides the stale indicator to show that the input or recipe has not changed * since the last bake. @@ -386,7 +406,6 @@ ${navigator.userAgent} staleIndicator.classList.add("hidden"); } - /** * Switches the Bake button between 'Bake', 'Cancel' and 'Loading' functions. * @@ -429,7 +448,6 @@ ${navigator.userAgent} recList.style.bottom = controls.clientHeight + "px"; } - } export default ControlsWaiter; From 6543fee71a935b0461003bcb3155995d133de17f Mon Sep 17 00:00:00 2001 From: e218736 <147728997+e218736@users.noreply.github.com> Date: Tue, 13 Feb 2024 12:00:40 +0000 Subject: [PATCH 2/7] input/optput status bar elements with tooltips tabbable --- src/web/utils/statusBar.mjs | 185 +++++++++++++++++++++++------------- 1 file changed, 120 insertions(+), 65 deletions(-) diff --git a/src/web/utils/statusBar.mjs b/src/web/utils/statusBar.mjs index 6469379a..4d27cbff 100644 --- a/src/web/utils/statusBar.mjs +++ b/src/web/utils/statusBar.mjs @@ -4,14 +4,16 @@ * @license Apache-2.0 */ -import {showPanel} from "@codemirror/view"; -import {CHR_ENC_SIMPLE_LOOKUP, CHR_ENC_SIMPLE_REVERSE_LOOKUP} from "../../core/lib/ChrEnc.mjs"; +import { showPanel } from "@codemirror/view"; +import { + CHR_ENC_SIMPLE_LOOKUP, + CHR_ENC_SIMPLE_REVERSE_LOOKUP, +} from "../../core/lib/ChrEnc.mjs"; /** * A Status bar extension for CodeMirror */ class StatusBarPanel { - /** * StatusBarPanel constructor * @param {Object} opts @@ -42,7 +44,10 @@ class StatusBarPanel { dom.className = "cm-status-bar"; dom.setAttribute("data-help-title", `${this.label} status bar`); - dom.setAttribute("data-help", `This status bar provides information about data in the ${this.label}. Help topics are available for each of the components by activating help when hovering over them.`); + dom.setAttribute( + "data-help", + `This status bar provides information about data in the ${this.label}. Help topics are available for each of the components by activating help when hovering over them.` + ); lhs.innerHTML = this.constructLHS(); rhs.innerHTML = this.constructRHS(); @@ -50,12 +55,24 @@ class StatusBarPanel { dom.appendChild(rhs); // Event listeners - dom.querySelectorAll(".cm-status-bar-select-btn").forEach( - el => el.addEventListener("click", this.showDropUp.bind(this), false) + dom.querySelectorAll(".cm-status-bar-select-btn").forEach((el) => + el.addEventListener("click", this.showDropUp.bind(this), false) + ); + dom.querySelector(".eol-select").addEventListener( + "click", + this.eolSelectClick.bind(this), + false + ); + dom.querySelector(".chr-enc-select").addEventListener( + "click", + this.chrEncSelectClick.bind(this), + false + ); + dom.querySelector(".cm-status-bar-filter-input").addEventListener( + "keyup", + this.chrEncFilter.bind(this), + false ); - dom.querySelector(".eol-select").addEventListener("click", this.eolSelectClick.bind(this), false); - dom.querySelector(".chr-enc-select").addEventListener("click", this.chrEncSelectClick.bind(this), false); - dom.querySelector(".cm-status-bar-filter-input").addEventListener("keyup", this.chrEncFilter.bind(this), false); return dom; } @@ -93,14 +110,14 @@ class StatusBarPanel { e.preventDefault(); const eolLookup = { - "LF": "\u000a", - "VT": "\u000b", - "FF": "\u000c", - "CR": "\u000d", - "CRLF": "\u000d\u000a", - "NEL": "\u0085", - "LS": "\u2028", - "PS": "\u2029" + LF: "\u000a", + VT: "\u000b", + FF: "\u000c", + CR: "\u000d", + CRLF: "\u000d\u000a", + NEL: "\u0085", + LS: "\u2028", + PS: "\u2029", }; const eolval = eolLookup[e.target.getAttribute("data-val")]; @@ -161,7 +178,8 @@ class StatusBarPanel { // CodeMirror always counts line breaks as one character. // We want to show an accurate reading of how many bytes there are. if (state.lineBreak.length !== 1) { - docLength += (state.lineBreak.length * state.doc.lines) - state.doc.lines - 1; + docLength += + state.lineBreak.length * state.doc.lines - state.doc.lines - 1; } length.textContent = docLength; lines.textContent = state.doc.lines; @@ -173,9 +191,9 @@ class StatusBarPanel { * @param {boolean} selectionSet */ updateSelection(state, selectionSet) { - const selLen = state?.selection?.main ? - state.selection.main.to - state.selection.main.from : - 0; + const selLen = state?.selection?.main + ? state.selection.main.to - state.selection.main.from + : 0; const selInfo = this.dom.querySelector(".sel-info"), curOffsetInfo = this.dom.querySelector(".cur-offset-info"); @@ -193,11 +211,12 @@ class StatusBarPanel { if (state.lineBreak.length !== 1) { const fromLine = state.doc.lineAt(from).number; const toLine = state.doc.lineAt(to).number; - from += (state.lineBreak.length * fromLine) - fromLine - 1; - to += (state.lineBreak.length * toLine) - toLine - 1; + from += state.lineBreak.length * fromLine - fromLine - 1; + to += state.lineBreak.length * toLine - toLine - 1; } - if (selLen > 0) { // Range + if (selLen > 0) { + // Range const start = this.dom.querySelector(".sel-start-value"), end = this.dom.querySelector(".sel-end-value"), length = this.dom.querySelector(".sel-length-value"); @@ -207,7 +226,8 @@ class StatusBarPanel { start.textContent = from; end.textContent = to; length.textContent = to - from; - } else { // Position + } else { + // Position const offset = this.dom.querySelector(".cur-offset-value"); selInfo.style.display = "none"; @@ -231,7 +251,7 @@ class StatusBarPanel { "\u000d\u000a": ["CRLF", "Carriage Return + Line Feed"], "\u0085": ["NEL", "Next Line"], "\u2028": ["LS", "Line Separator"], - "\u2029": ["PS", "Paragraph Separator"] + "\u2029": ["PS", "Paragraph Separator"], }; const val = this.dom.querySelector(".eol-value"); @@ -239,11 +259,13 @@ class StatusBarPanel { const eolName = eolLookup[state.lineBreak]; val.textContent = eolName[0]; button.setAttribute("title", `End of line sequence:
${eolName[1]}`); - button.setAttribute("data-original-title", `End of line sequence:
${eolName[1]}`); + button.setAttribute( + "data-original-title", + `End of line sequence:
${eolName[1]}` + ); this.eolVal = state.lineBreak; } - /** * Sets the current character encoding of the document */ @@ -251,13 +273,21 @@ class StatusBarPanel { const chrEncVal = this.chrEncGetter(); if (chrEncVal === this.chrEncVal) return; - const name = CHR_ENC_SIMPLE_REVERSE_LOOKUP[chrEncVal] ? CHR_ENC_SIMPLE_REVERSE_LOOKUP[chrEncVal] : "Raw Bytes"; + const name = CHR_ENC_SIMPLE_REVERSE_LOOKUP[chrEncVal] + ? CHR_ENC_SIMPLE_REVERSE_LOOKUP[chrEncVal] + : "Raw Bytes"; const val = this.dom.querySelector(".chr-enc-value"); const button = val.closest(".cm-status-bar-select-btn"); val.textContent = name; - button.setAttribute("title", `${this.label} character encoding:
${name}`); - button.setAttribute("data-original-title", `${this.label} character encoding:
${name}`); + button.setAttribute( + "title", + `${this.label} character encoding:
${name}` + ); + button.setAttribute( + "data-original-title", + `${this.label} character encoding:
${name}` + ); this.chrEncVal = chrEncVal; } @@ -274,7 +304,9 @@ class StatusBarPanel { bakingTimeInfo.style.display = "inline-block"; bakingTime.textContent = this.timing.duration(this.tabNumGetter()); - const info = this.timing.printStages(this.tabNumGetter()).replace(/\n/g, "
"); + const info = this.timing + .printStages(this.tabNumGetter()) + .replace(/\n/g, "
"); bakingTimeInfo.setAttribute("data-original-title", info); } else { bakingTimeInfo.style.display = "none"; @@ -287,11 +319,11 @@ class StatusBarPanel { */ updateSizing(view) { const viewHeight = view.contentDOM.parentNode.clientHeight; - this.dom.querySelectorAll(".cm-status-bar-select-scroll").forEach( - el => { - el.style.maxHeight = (viewHeight - 50) + "px"; - } - ); + this.dom + .querySelectorAll(".cm-status-bar-select-scroll") + .forEach((el) => { + el.style.maxHeight = viewHeight - 50 + "px"; + }); } /** @@ -302,19 +334,27 @@ class StatusBarPanel { if (this.htmlOutput?.html === "") { // Enable all controls - this.dom.querySelectorAll(".disabled").forEach(el => { + this.dom.querySelectorAll(".disabled").forEach((el) => { el.classList.remove("disabled"); }); } else { // Disable chrenc, length, selection etc. - this.dom.querySelectorAll(".cm-status-bar-select-btn").forEach(el => { - el.classList.add("disabled"); - }); + this.dom + .querySelectorAll(".cm-status-bar-select-btn") + .forEach((el) => { + el.classList.add("disabled"); + }); - this.dom.querySelector(".stats-length-value").parentNode.classList.add("disabled"); - this.dom.querySelector(".stats-lines-value").parentNode.classList.add("disabled"); + this.dom + .querySelector(".stats-length-value") + .parentNode.classList.add("disabled"); + this.dom + .querySelector(".stats-lines-value") + .parentNode.classList.add("disabled"); this.dom.querySelector(".sel-info").classList.add("disabled"); - this.dom.querySelector(".cur-offset-info").classList.add("disabled"); + this.dom + .querySelector(".cur-offset-info") + .classList.add("disabled"); } } @@ -324,11 +364,11 @@ class StatusBarPanel { */ constructLHS() { return ` - + abc - + sort @@ -351,28 +391,35 @@ class StatusBarPanel { * @returns {string} */ constructRHS() { - const chrEncOptions = Object.keys(CHR_ENC_SIMPLE_LOOKUP).map(name => - `${name}` - ).join(""); + const chrEncOptions = Object.keys(CHR_ENC_SIMPLE_LOOKUP) + .map( + (name) => + `${name}` + ) + .join(""); let chrEncHelpText = "", eolHelpText = ""; if (this.label === "Input") { - chrEncHelpText = "The input character encoding defines how the input text is encoded into bytes which are then processed by the Recipe.

The 'Raw bytes' option attempts to treat the input as individual bytes in the range 0-255. If it detects any characters with Unicode values above 255, it will treat the entire input as UTF-8. 'Raw bytes' is usually the best option if you are inputting binary data, such as a file."; - eolHelpText = "The End of Line Sequence defines which bytes are considered EOL terminators. Pressing the return key will enter this value into the input and create a new line.

Changing the EOL sequence will not modify any existing data in the input but may change how previously entered line breaks are displayed. Lines added while a different EOL terminator was set may not now result in a new line, but may be displayed as control characters instead."; + chrEncHelpText = + "The input character encoding defines how the input text is encoded into bytes which are then processed by the Recipe.

The 'Raw bytes' option attempts to treat the input as individual bytes in the range 0-255. If it detects any characters with Unicode values above 255, it will treat the entire input as UTF-8. 'Raw bytes' is usually the best option if you are inputting binary data, such as a file."; + eolHelpText = + "The End of Line Sequence defines which bytes are considered EOL terminators. Pressing the return key will enter this value into the input and create a new line.

Changing the EOL sequence will not modify any existing data in the input but may change how previously entered line breaks are displayed. Lines added while a different EOL terminator was set may not now result in a new line, but may be displayed as control characters instead."; } else { - chrEncHelpText = "The output character encoding defines how the output bytes are decoded into text which can be displayed to you.

The 'Raw bytes' option treats the output data as individual bytes in the range 0-255."; - eolHelpText = "The End of Line Sequence defines which bytes are considered EOL terminators.

Changing this value will not modify the value of the output, but may change how certain bytes are displayed and whether they result in a new line being created."; + chrEncHelpText = + "The output character encoding defines how the output bytes are decoded into text which can be displayed to you.

The 'Raw bytes' option treats the output data as individual bytes in the range 0-255."; + eolHelpText = + "The End of Line Sequence defines which bytes are considered EOL terminators.

Changing this value will not modify the value of the output, but may change how certain bytes are displayed and whether they result in a new line being created."; } return ` -