diff --git a/src/web/App.mjs b/src/web/App.mjs index 21d05d91..be1cc4df 100755 --- a/src/web/App.mjs +++ b/src/web/App.mjs @@ -306,9 +306,9 @@ class App { } /** - * Set desktop layout + * Set desktop splitters */ - setDesktopLayout(minimise){ + setDesktopSplitter(minimise){ if (this.columnSplitter) this.columnSplitter.destroy(); if (this.ioSplitter) this.ioSplitter.destroy(); @@ -327,14 +327,12 @@ class App { gutterSize: 4, minSize: minimise ? [0, 0] : [50, 50] }); - - this.adjustComponentSizes(); } /** - * Set mobile layout + * Set mobile splitters */ - setMobileLayout() { + setMobileSplitter() { if (this.columnSplitter) this.columnSplitter.destroy(); if (this.ioSplitter) this.ioSplitter.destroy(); @@ -345,7 +343,6 @@ class App { }); this.ioSplitter = Split(["#input", "#output"], { - sizes: [45,55], direction: "vertical", gutterSize: 0, }); @@ -875,30 +872,47 @@ class App { } /** + * Set desktop UI ( on init and on window resize events ) + * * @param {boolean} minimise */ setDesktopUI(minimise){ - // enable tooltips on desktop as normal $("[data-toggle=tooltip]").tooltip("enable"); - this.setDesktopLayout(minimise); - // repopulate to enable popovers and drag events + this.setDesktopSplitter(minimise); + this.adjustComponentSizes(); this.populateOperationsList(); - /** - * We don't want to display any checkmarks on desktop, so we clear them. - * It has no effect on the recipe list, it's purely a visual indicator - */ this.manager.recipe.clearAllSelectedClasses(); } + /** + * Set mobile UI ( on init and on window resize events ) + */ setMobileUI(){ - // tooltips on mobile are reducing UX, so we disable it $("[data-toggle=tooltip]").tooltip("disable"); - this.setMobileLayout(); - // repopulate to disable popovers and drag events + this.setMobileSplitter(); + this.divideAvailableSpace(); this.populateOperationsList(); - // restore the appropriate checkmarks this.manager.recipe.updateSelectedOperations(); } + + /** + * Due to variable available heights on mobile devices ( due to the + * address bar etc. ), we need to calculate the available space and + * set some heights programmatically. + * + * The numbers 40, 70 and 90 refer to divs with fixed heights, + * that is: #banner, #operations and #controls. -2 is accounting for + * some borders. + * Be mindful to update these accordingly in the stylesheets + * ( themes/_structure ) if you want to make changes. + */ + divideAvailableSpace( isMobile ){ + const remainingSpace = window.innerHeight - (40+70+90-2); // banner, operations, controls height + borders + + ["recipe", "input", "output"].forEach(( div ) => { + document.getElementById(div).style.height = `${remainingSpace/3}px`; + }); + } } export default App; diff --git a/src/web/Manager.mjs b/src/web/Manager.mjs index 3c8506fa..f1b5d8bc 100755 --- a/src/web/Manager.mjs +++ b/src/web/Manager.mjs @@ -62,7 +62,7 @@ class Manager { // Define Waiter objects to handle various areas this.timing = new TimingWaiter(this.app, this); this.worker = new WorkerWaiter(this.app, this); - this.window = new WindowWaiter(this.app); + this.window = new WindowWaiter(this.app, this); this.controls = new ControlsWaiter(this.app, this); this.recipe = new RecipeWaiter(this.app, this); this.ops = new OperationsWaiter(this.app, this); @@ -141,16 +141,10 @@ class Manager { document.getElementById("load-button").addEventListener("click", this.controls.loadButtonClick.bind(this.controls)); document.getElementById("support").addEventListener("click", this.controls.supportButtonClick.bind(this.controls)); this.addMultiEventListeners("#save-texts textarea", "keyup paste", this.controls.saveTextChange, this.controls); - /** - * A note for the Controls listeners below: - * - * I would strongly prefer to just add one listener to all elements with the .btn-maximise class, - * but for a reason I have not been able to uncover ( something to do with addDynamicListener? ) click events - * don't properly bubble and the hitbox to maximise is unacceptably tiny - */ - document.getElementById("maximise-recipe").addEventListener("click", this.controls.handlePaneMaximising.bind(this.controls)) - document.getElementById("maximise-input").addEventListener("click", this.controls.handlePaneMaximising.bind(this.controls)) - document.getElementById("maximise-output").addEventListener("click", this.controls.handlePaneMaximising.bind(this.controls)) + // A note for the Maximise Controls listeners below: click events via addDynamicListener don't properly bubble and the hit box to maximise is unacceptably tiny, hence this solution + document.getElementById("maximise-recipe").addEventListener("click", this.controls.onMaximiseButtonClick.bind(this.controls)) + document.getElementById("maximise-input").addEventListener("click", this.controls.onMaximiseButtonClick.bind(this.controls)) + 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); diff --git a/src/web/TODO.md b/src/web/TODO.md index 8e3ca00c..322a8980 100644 --- a/src/web/TODO.md +++ b/src/web/TODO.md @@ -4,16 +4,19 @@ --- #### Mobile UI ( on real device ): -- test *thoroughly* with keyboard popping up because that messes with view-heights on mobile probably and might make it a very frustrating experience -- test drag and drop etc. Regular mobile events / UX -- view-heights not correct due to variable taskbar on mobile devices +- need long press checks on mobile to add favourites ( recipe is done ) > + - check on window resizing + +- raw bites dropup thingy is unusable +- shannon entropy thingies + +- backspace on fs view should close max view. Keep making the same mistake and navigating away when for instance recipe is expanded and double click the window to fs > resolve. Reset layout -- need long press checks on mobile to add favourites and switch ingredient order -- raw bites dropdown thingy is unusable ### Desktop UI: ### General UI: -- fix up key / tab events so UI can be navigated comfortably with keys ( inc. visual focus feedback ). Probably a lot of work though +- fix up key / tab events so UI can be navigated comfortably with keys ( inc. visual focus feedback ). Probably a lot of +work though ### JS: - `core/Recipe.mjs`, `core/lib/Magic.js` return imports to original @@ -21,6 +24,7 @@ ### Misc: - Gruntfile revert dev config - check for lingering @TODO across code -- comb through CSS and improve organisation for better DevX. Ask repo owners to open another issue perhaps and just redo all of the stylesheets ( preferably with SASS ) +- comb through CSS and improve organisation for better DevX. Ask repo owners to open another issue perhaps and just +redo all of the stylesheets ( preferably with SASS ) - delete this file when done :) diff --git a/src/web/stylesheets/components/_controls.css b/src/web/stylesheets/components/_controls.css index 914aea5c..2f952975 100644 --- a/src/web/stylesheets/components/_controls.css +++ b/src/web/stylesheets/components/_controls.css @@ -7,8 +7,6 @@ */ #controls { - position: fixed; - bottom: 0; width: 100%; padding: 10px 0; border-top: 1px solid var(--primary-border-colour); diff --git a/src/web/stylesheets/components/io/_status-bar.css b/src/web/stylesheets/components/io/_status-bar.css index ea0bad12..12db2b6b 100644 --- a/src/web/stylesheets/components/io/_status-bar.css +++ b/src/web/stylesheets/components/io/_status-bar.css @@ -1,5 +1,4 @@ /* Status bar */ - .cm-panel input::placeholder { font-size: 12px !important; } @@ -7,7 +6,6 @@ .ͼ2 .cm-panels, .ͼ2 .cm-side-panels { background-color: var(--secondary-background-colour); - /*border-color: var(--primary-border-colour);*/ color: var(--primary-font-colour); border-bottom: 1px solid var(--primary-border-colour); } @@ -50,6 +48,7 @@ display: inline-block; } +/*@TODO: update this one for mobile*/ /* Dropup content (Hidden by Default) */ .cm-status-bar-select-content { display: none; @@ -97,6 +96,8 @@ display: block; } + +/*@TODO: and update this one for mobile*/ .cm-status-bar-select-scroll { overflow-y: auto; max-height: 300px; diff --git a/src/web/stylesheets/components/operations/_operations.css b/src/web/stylesheets/components/operations/_operations.css index afecbc38..e6419982 100644 --- a/src/web/stylesheets/components/operations/_operations.css +++ b/src/web/stylesheets/components/operations/_operations.css @@ -15,7 +15,7 @@ #operations-dropdown { position: absolute; - top: 40px; /* the height of the input[type="search"] with pos relative */ + top: 41px; width: 100%; height: auto; max-height: 60vh; diff --git a/src/web/stylesheets/layout/_structure.css b/src/web/stylesheets/layout/_structure.css index eeec19aa..4c783168 100755 --- a/src/web/stylesheets/layout/_structure.css +++ b/src/web/stylesheets/layout/_structure.css @@ -8,8 +8,6 @@ #banner { height: var(--banner-height); } #operations { height: var(--operations-height); } -#recipe { height: var(--recipe-height); } -#IO { height: var(--io-height); } #controls { height: var(--controls-height); } #banner, @@ -25,24 +23,18 @@ } #workspace-wrapper { - height: var(--workspace-height); margin-top: var(--banner-height); } #controls { + position: fixed; bottom: 0; } -#IO { - padding-bottom: var(--controls-height); -} - #recipe.maximised-pane, #input.maximised-pane, #output.maximised-pane { position: fixed; - min-height: calc( 100vh - var(--banner-height )); - height: auto; top: var(--banner-height); left: 0; right: 0; @@ -54,10 +46,6 @@ } @media only screen and ( min-width: 768px ) { - #IO { - padding-bottom: 0; - } - #recipe { padding-bottom: var(--controls-height); } @@ -67,7 +55,7 @@ } #workspace-wrapper { - height: calc( 100vh - var(--banner-height)); + height: calc(100vh - var(--banner-height)); } #operations-dropdown { diff --git a/src/web/stylesheets/themes/_structure.css b/src/web/stylesheets/themes/_structure.css index ffd41411..7cc8d75f 100644 --- a/src/web/stylesheets/themes/_structure.css +++ b/src/web/stylesheets/themes/_structure.css @@ -1,9 +1,17 @@ :root { + /* Fixed heights */ --banner-height: 40px; --controls-height: 70px; - --workspace-height: calc( 100vh - var(--banner-height) - var(--controls-height)); - --recipe-height: 22vh; - --operations-height: 89px; - /* io gets the remaining space */ - --io-height: calc(100vh - var(--banner-height) - var(--controls-height) - var(--recipe-height) - var(--operations-height)); + /*initial mobile height*/ + --operations-height: 90px; } + +/** + * A note: + * + * Heights of #recipe, #input and #output are set programmatically + * in App.js > divideAvailableSpace(), please see the docs for that + * function for more information. + */ + + diff --git a/src/web/waiters/ControlsWaiter.mjs b/src/web/waiters/ControlsWaiter.mjs index 740be758..1b5190d0 100755 --- a/src/web/waiters/ControlsWaiter.mjs +++ b/src/web/waiters/ControlsWaiter.mjs @@ -419,90 +419,75 @@ ${navigator.userAgent} } } - /** - * Handle the maximising and resetting to default state of - * panels. + * Maximise control button click handler. * - * On mobile UI, #recipe, #input and #output can be maximised, - * on desktop UI it's available only for #output + * The buttons have IDs like 'maximise-input', 'maximise-output' etc. We grab the + * to-be-maximised pane ID itself by stripping the pane ID from the button ID. * * @param {Event} e */ - handlePaneMaximising(e){ - // the event target btn can be one of ( currently ) 3 'maximiser' buttons - const btn = e.target.classList.contains("btn-maximise") ? e.target : e.target.parentNode; - // find the parent ( target ) pane to be maximised that belongs to the btn - const pane = this.resolveMaximiserParentPane( btn.id ) + onMaximiseButtonClick(e) { + // the target pane is not already maximised because it does not have the 'maximised-pane' class.. + const maximise = !document.getElementById(e.currentTarget.id.replace('maximise-', '')).classList.contains("maximised-pane"); + this.setPaneMaximised(e.currentTarget.id.replace('maximise-', ''), maximise); + } - if (btn.getAttribute("data-original-title") === "Maximise pane") { - this.maximisePane(btn,pane); + /** + * Handle the maximising ( and resetting to default state ) of + * panes. + * + * @param {string} paneId + * @param {boolean} maximise + */ + setPaneMaximised( paneId, maximise ){ + const pane = document.getElementById( paneId ); + const btn = document.getElementById( `maximise-${paneId}` ); + + this.setMaximiseControlButton(btn, maximise); + this.setPaneMaximisedClasses(pane, maximise); + + if ( maximise ) { + pane.style.height = `${window.innerHeight - 40}px`; } else { - this.resetPane(btn, pane); + if ( window.innerWidth < this.app.breakpoint ){ + this.app.divideAvailableSpace(); + } } } /** - * Get the parent pane of the 'maximise' button / icon that was - * clicked through the buttons' ID + * Set and remove the appropriate classes on maximise / minimise actions * - * @param {string} id + * @param {HTMLElement} pane + * @param {boolean} maximise */ - resolveMaximiserParentPane(id){ - switch(id) { - case "maximise-recipe": - return document.getElementById("recipe"); - case "maximise-input": - return document.getElementById("input"); - case "maximise-output": - return document.getElementById("output"); + setPaneMaximisedClasses(pane, maximise) { + if ( maximise ) { + pane.classList.add("top-zindex"); + pane.classList.add("maximised-pane"); + } else { + pane.classList.remove("top-zindex"); + pane.classList.remove("maximised-pane"); } } - /** - * Set the pane from default state to Maximised - * - * @param {HTMLElement} btn - * @param {HTMLElement} pane - */ - maximisePane(btn, pane){ - this.togglePane(pane,true); - this.toggleIcon(btn, true); - } /** - * Reset the pane from Maximised state to default + * Set the correct icon and data title attribute text based on + * the 'maximise' flag * * @param {HTMLElement} btn - * @param {HTMLElement} pane + * @param {boolean} maximise */ - resetPane(btn, pane) { - this.togglePane(pane, false); - this.toggleIcon(btn, false); - } - - /** - * Toggle the pane to or from maximised size, - * based on the 'isMaximised' flag - * - * @param {HTMLElement} pane - * @param {boolean} isMaximised - */ - togglePane(pane, isMaximised) { - isMaximised ? pane.classList.add("top-zindex") : pane.classList.remove("top-zindex"); - isMaximised ? pane.classList.add("maximised-pane") : pane.classList.remove("maximised-pane"); - } - - /** - * Toggle the 'maximise' icon and attribute text based on - * the 'isMaximised' flag - * - * @param {HTMLElement} btn - * @param {boolean} isMaximised - */ - toggleIcon(btn, isMaximised ) { - btn.querySelector("i").innerHTML = isMaximised ? "fullscreen_exit" : "fullscreen"; - $(btn).attr("data-original-title", isMaximised ? "Reset pane" : "Maximise pane"); + setMaximiseControlButton(btn, maximise ) { + if ( maximise ) { + btn.querySelector("i").innerHTML = "fullscreen_exit"; + btn.setAttribute("data-original-title", "Reset pane"); + } else { + btn.querySelector("i").innerHTML = "fullscreen"; + btn.setAttribute("data-original-title", "Maximise pane"); + } } } diff --git a/src/web/waiters/WindowWaiter.mjs b/src/web/waiters/WindowWaiter.mjs index 45e40182..da3a4899 100755 --- a/src/web/waiters/WindowWaiter.mjs +++ b/src/web/waiters/WindowWaiter.mjs @@ -15,28 +15,61 @@ class WindowWaiter { * WindowWaiter constructor. * * @param {App} app - The main view object for CyberChef. + * @param {Manager} manager - The CyberChef event manager. */ - constructor(app) { + constructor(app, manager) { this.app = app; + this.manager = manager; } /** * Handler for window resize events. + * * Resets adjustable component sizes after 200ms (so that continuous resizing doesn't cause * continuous resetting). */ windowResize() { if ( window.innerWidth >= this.app.breakpoint ) { - this.app.setDesktopUI(false); + this.onResizeToDesktop(); } else { - this.app.setMobileUI(); + this.onResizeToMobile(); + } + + // #output can be maximised on all screen sizes, so if it was open while resizing, + // it can be kept maximised until minimised manually + if ( document.getElementById("output").classList.contains("maximised-pane") ) { + this.manager.controls.setPaneMaximised( "output", true ); } debounce(this.app.adjustComponentSizes, 200, "windowResize", this.app, [])(); } + onResizeToDesktop(){ + this.app.setDesktopUI(false); + + // if a window is resized past breakpoint while #recipe or #input is maximised, + // the maximised pane is set to its default ( non-maximised ) state + ["recipe", "input"].forEach( paneId => this.manager.controls.setPaneMaximised(paneId, false)); + + // to prevent #recipe from keeping the height set in divideAvailableSpace + document.getElementById("recipe").style.height = "100%"; + } + + + onResizeToMobile(){ + this.app.setMobileUI(); + + // when mobile devices' keyboards pop up, it triggers a window resize event. Here + // we keep the maximised panes open until the minimise button is clicked / tapped + ["recipe", "input", "output"] + .map( paneId => document.getElementById(paneId)) + .filter( pane => pane.classList.contains("maximised-pane")) + .forEach( pane => this.manager.controls.setPaneMaximised(pane.id, true)); + } + + /** * Handler for window blur events. * Saves the current time so that we can calculate how long the window was unfocused for when