Add responsive phone and tablet layouts alongside desktop UI.
Use container-width layout modes so CyberChef works in embeds, with Recipe/Data navigation, operations sheet/drawer, tap-to-add, and touch reorder controls. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
49b855c761
commit
cddda865ae
@ -66,6 +66,8 @@ class App {
|
|||||||
setup() {
|
setup() {
|
||||||
document.dispatchEvent(this.manager.appstart);
|
document.dispatchEvent(this.manager.appstart);
|
||||||
|
|
||||||
|
// Splitters are created for desktop; ResponsiveLayoutWaiter destroys them
|
||||||
|
// immediately when the container is phone/tablet sized.
|
||||||
this.initialiseSplitter();
|
this.initialiseSplitter();
|
||||||
this.loadLocalStorage();
|
this.loadLocalStorage();
|
||||||
this.manager.options.applyPreferredColorScheme();
|
this.manager.options.applyPreferredColorScheme();
|
||||||
@ -309,14 +311,47 @@ class App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Destroys Split.js instances and removes leftover gutters.
|
||||||
|
* Used when switching to mobile/tablet layouts.
|
||||||
|
*/
|
||||||
|
destroySplitters() {
|
||||||
|
if (this.columnSplitter) {
|
||||||
|
this.columnSplitter.destroy();
|
||||||
|
this.columnSplitter = null;
|
||||||
|
}
|
||||||
|
if (this.ioSplitter) {
|
||||||
|
this.ioSplitter.destroy();
|
||||||
|
this.ioSplitter = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.querySelectorAll("#workspace-wrapper > .gutter, #IO > .gutter").forEach(g => g.remove());
|
||||||
|
|
||||||
|
// Clear inline sizes Split.js may have left behind
|
||||||
|
["operations", "recipe", "IO", "input", "output"].forEach(id => {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (el) {
|
||||||
|
el.style.width = "";
|
||||||
|
el.style.height = "";
|
||||||
|
el.style.flex = "";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sets up the adjustable splitter to allow the user to resize areas of the page.
|
* Sets up the adjustable splitter to allow the user to resize areas of the page.
|
||||||
*
|
*
|
||||||
* @param {boolean} [minimise=false] - Set this flag if attempting to minimise frames to 0 width
|
* @param {boolean} [minimise=false] - Set this flag if attempting to minimise frames to 0 width
|
||||||
*/
|
*/
|
||||||
initialiseSplitter(minimise=false) {
|
initialiseSplitter(minimise=false) {
|
||||||
if (this.columnSplitter) this.columnSplitter.destroy();
|
this.destroySplitters();
|
||||||
if (this.ioSplitter) this.ioSplitter.destroy();
|
|
||||||
|
// Do not create splitters while in a touch layout — CSS owns the layout.
|
||||||
|
if (this.manager.responsive && this.manager.responsive.isTouchLayout()) {
|
||||||
|
this.adjustComponentSizes();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.columnSplitter = Split(["#operations", "#recipe", "#IO"], {
|
this.columnSplitter = Split(["#operations", "#recipe", "#IO"], {
|
||||||
sizes: [20, 30, 50],
|
sizes: [20, 30, 50],
|
||||||
@ -629,6 +664,10 @@ class App {
|
|||||||
* Resets the splitter positions to default.
|
* Resets the splitter positions to default.
|
||||||
*/
|
*/
|
||||||
resetLayout() {
|
resetLayout() {
|
||||||
|
if (!this.columnSplitter || !this.ioSplitter) {
|
||||||
|
this.adjustComponentSizes();
|
||||||
|
return;
|
||||||
|
}
|
||||||
this.columnSplitter.setSizes([20, 30, 50]);
|
this.columnSplitter.setSizes([20, 30, 50]);
|
||||||
this.ioSplitter.setSizes([50, 50]);
|
this.ioSplitter.setSizes([50, 50]);
|
||||||
this.adjustComponentSizes();
|
this.adjustComponentSizes();
|
||||||
|
|||||||
@ -90,6 +90,8 @@ class HTMLOperation {
|
|||||||
|
|
||||||
html += `</div>
|
html += `</div>
|
||||||
<div class="recip-icons">
|
<div class="recip-icons">
|
||||||
|
<i class="material-icons op-reorder-up responsive-only" title="Move up" aria-label="Move operation up">arrow_upward</i>
|
||||||
|
<i class="material-icons op-reorder-down responsive-only" title="Move down" aria-label="Move operation down">arrow_downward</i>
|
||||||
<i class="material-icons breakpoint" title="Set breakpoint" break="false" data-help-title="Setting breakpoints" data-help="Setting a breakpoint on an operation will cause execution of the Recipe to pause when it reaches that operation.">pause</i>
|
<i class="material-icons breakpoint" title="Set breakpoint" break="false" data-help-title="Setting breakpoints" data-help="Setting a breakpoint on an operation will cause execution of the Recipe to pause when it reaches that operation.">pause</i>
|
||||||
<i class="material-icons disable-icon" title="Disable operation" disabled="false" data-help-title="Disabling operations" data-help="Disabling an operation will prevent it from being executed when the Recipe is baked. Execution will skip over the disabled operation and continue with subsequent operations.">not_interested</i>
|
<i class="material-icons disable-icon" title="Disable operation" disabled="false" data-help-title="Disabling operations" data-help="Disabling an operation will prevent it from being executed when the Recipe is baked. Execution will skip over the disabled operation and continue with subsequent operations.">not_interested</i>
|
||||||
<i class="material-icons hide-args-icon" title="Hide operation's arguments" hide-args="false" data-help-title="Hide operation's arguments" data-help="Hiding an operation's argument will save space in the Recipe window. Execution will still take place with the selected argument options.">keyboard_arrow_up</i>
|
<i class="material-icons hide-args-icon" title="Hide operation's arguments" hide-args="false" data-help-title="Hide operation's arguments" data-help="Hiding an operation's argument will save space in the Recipe window. Execution will still take place with the selected argument options.">keyboard_arrow_up</i>
|
||||||
|
|||||||
@ -18,6 +18,7 @@ import BindingsWaiter from "./waiters/BindingsWaiter.mjs";
|
|||||||
import BackgroundWorkerWaiter from "./waiters/BackgroundWorkerWaiter.mjs";
|
import BackgroundWorkerWaiter from "./waiters/BackgroundWorkerWaiter.mjs";
|
||||||
import TabWaiter from "./waiters/TabWaiter.mjs";
|
import TabWaiter from "./waiters/TabWaiter.mjs";
|
||||||
import TimingWaiter from "./waiters/TimingWaiter.mjs";
|
import TimingWaiter from "./waiters/TimingWaiter.mjs";
|
||||||
|
import ResponsiveLayoutWaiter from "./waiters/ResponsiveLayoutWaiter.mjs";
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -74,6 +75,7 @@ class Manager {
|
|||||||
this.seasonal = new SeasonalWaiter(this.app, this);
|
this.seasonal = new SeasonalWaiter(this.app, this);
|
||||||
this.bindings = new BindingsWaiter(this.app, this);
|
this.bindings = new BindingsWaiter(this.app, this);
|
||||||
this.background = new BackgroundWorkerWaiter(this.app, this);
|
this.background = new BackgroundWorkerWaiter(this.app, this);
|
||||||
|
this.responsive = new ResponsiveLayoutWaiter(this.app, this);
|
||||||
|
|
||||||
// Object to store dynamic handlers to fire on elements that may not exist yet
|
// Object to store dynamic handlers to fire on elements that may not exist yet
|
||||||
this.dynamicHandlers = {};
|
this.dynamicHandlers = {};
|
||||||
@ -95,6 +97,7 @@ class Manager {
|
|||||||
this.bindings.updateKeybList();
|
this.bindings.updateKeybList();
|
||||||
this.background.registerChefWorker();
|
this.background.registerChefWorker();
|
||||||
this.seasonal.load();
|
this.seasonal.load();
|
||||||
|
this.responsive.setup();
|
||||||
|
|
||||||
this.confirmWaitersLoaded();
|
this.confirmWaitersLoaded();
|
||||||
}
|
}
|
||||||
@ -148,6 +151,7 @@ class Manager {
|
|||||||
// Operations
|
// Operations
|
||||||
this.addMultiEventListener("#search", "keyup paste search", this.ops.searchOperations, this.ops);
|
this.addMultiEventListener("#search", "keyup paste search", this.ops.searchOperations, this.ops);
|
||||||
this.addDynamicListener(".op-list li.operation", "dblclick", this.ops.operationDblclick, this.ops);
|
this.addDynamicListener(".op-list li.operation", "dblclick", this.ops.operationDblclick, this.ops);
|
||||||
|
this.addDynamicListener(".op-list li.operation", "click", this.ops.operationClick, this.ops);
|
||||||
document.getElementById("edit-favourites").addEventListener("click", this.ops.editFavouritesClick.bind(this.ops));
|
document.getElementById("edit-favourites").addEventListener("click", this.ops.editFavouritesClick.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));
|
||||||
@ -160,6 +164,8 @@ class Manager {
|
|||||||
this.addDynamicListener(".hide-args-icon", "click", this.recipe.hideArgsClick, this.recipe);
|
this.addDynamicListener(".hide-args-icon", "click", this.recipe.hideArgsClick, this.recipe);
|
||||||
this.addDynamicListener(".disable-icon", "click", this.recipe.disableClick, this.recipe);
|
this.addDynamicListener(".disable-icon", "click", this.recipe.disableClick, this.recipe);
|
||||||
this.addDynamicListener(".breakpoint", "click", this.recipe.breakpointClick, this.recipe);
|
this.addDynamicListener(".breakpoint", "click", this.recipe.breakpointClick, this.recipe);
|
||||||
|
this.addDynamicListener(".op-reorder-up", "click", this.recipe.moveOpUp, this.recipe);
|
||||||
|
this.addDynamicListener(".op-reorder-down", "click", this.recipe.moveOpDown, this.recipe);
|
||||||
this.addDynamicListener("#rec-list li.operation", "dblclick", this.recipe.operationDblclick, this.recipe);
|
this.addDynamicListener("#rec-list li.operation", "dblclick", this.recipe.operationDblclick, this.recipe);
|
||||||
this.addDynamicListener("#rec-list li.operation > div", "dblclick", this.recipe.operationChildDblclick, this.recipe);
|
this.addDynamicListener("#rec-list li.operation > div", "dblclick", this.recipe.operationChildDblclick, this.recipe);
|
||||||
this.addDynamicListener("#rec-list .dropdown-menu.toggle-dropdown a", "click", this.recipe.dropdownToggleClick, this.recipe);
|
this.addDynamicListener("#rec-list .dropdown-menu.toggle-dropdown a", "click", this.recipe.dropdownToggleClick, this.recipe);
|
||||||
|
|||||||
@ -24,6 +24,7 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>CyberChef</title>
|
<title>CyberChef</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, viewport-fit=cover">
|
||||||
|
|
||||||
<meta name="copyright" content="Crown Copyright 2016-<%= htmlWebpackPlugin.options.compileYear %>" />
|
<meta name="copyright" content="Crown Copyright 2016-<%= htmlWebpackPlugin.options.compileYear %>" />
|
||||||
<meta name="description" content="The Cyber Swiss Army Knife - a web app for encryption, encoding, compression and data analysis" />
|
<meta name="description" content="The Cyber Swiss Army Knife - a web app for encryption, encoding, compression and data analysis" />
|
||||||
@ -145,7 +146,10 @@
|
|||||||
<button type="button" aria-label="Edit Favourites" class="btn btn-warning bmd-btn-icon" id="edit-favourites" data-toggle="tooltip" title="Edit favourites">
|
<button type="button" aria-label="Edit Favourites" class="btn btn-warning bmd-btn-icon" id="edit-favourites" data-toggle="tooltip" title="Edit favourites">
|
||||||
<i class="material-icons" aria-hidden="true">star</i>
|
<i class="material-icons" aria-hidden="true">star</i>
|
||||||
</button>
|
</button>
|
||||||
<div tabindex="0" id="content-wrapper">
|
<div tabindex="0" id="content-wrapper" class="cyberchef-root" data-layout="desktop" data-mobile-pane="recipe" data-data-segment="input">
|
||||||
|
<div id="ops-overlay" hidden aria-hidden="true">
|
||||||
|
<div id="ops-scrim"></div>
|
||||||
|
</div>
|
||||||
<div id="banner" class="row">
|
<div id="banner" class="row">
|
||||||
<div class="col" style="text-align: left; padding-left: 10px;">
|
<div class="col" style="text-align: left; padding-left: 10px;">
|
||||||
<a href="#" 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>">Download CyberChef <i class="material-icons">file_download</i></a>
|
<a href="#" 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>">Download CyberChef <i class="material-icons">file_download</i></a>
|
||||||
@ -163,12 +167,26 @@
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col" style="text-align: right; padding-right: 0;">
|
<div class="col" style="text-align: right; padding-right: 0;">
|
||||||
|
<button type="button" id="tablet-ops-toggle" class="btn btn-primary btn-sm tablet-only" aria-label="Open operations" aria-expanded="false" aria-controls="operations">
|
||||||
|
<i class="material-icons" aria-hidden="true">menu</i> Ops
|
||||||
|
</button>
|
||||||
<a href="#" id="options" 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.">Options <i class="material-icons">settings</i></a>
|
<a href="#" id="options" 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.">Options <i class="material-icons">settings</i></a>
|
||||||
<a href="#" id="support" data-toggle="modal" data-target="#support-modal" 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.">About / Support <i class="material-icons">help</i></a>
|
<a href="#" id="support" data-toggle="modal" data-target="#support-modal" 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.">About / Support <i class="material-icons">help</i></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div id="workspace-wrapper">
|
<div id="workspace-wrapper">
|
||||||
<div id="operations" class="split split-horizontal no-select">
|
<div id="operations" class="split split-horizontal no-select" role="dialog" aria-label="Operations">
|
||||||
|
<div id="ops-sheet-header" class="mobile-only responsive-chrome">
|
||||||
|
<div style="width:100%;">
|
||||||
|
<div class="ops-handle" aria-hidden="true"></div>
|
||||||
|
<div style="display:flex;align-items:center;justify-content:space-between;">
|
||||||
|
<strong>Operations</strong>
|
||||||
|
<button type="button" id="ops-close" class="btn btn-primary bmd-btn-icon" aria-label="Close operations">
|
||||||
|
<i class="material-icons" aria-hidden="true">close</i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="title no-select" 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>">
|
<div class="title no-select" 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>">
|
||||||
Operations
|
Operations
|
||||||
<span class="op-count"></span>
|
<span class="op-count"></span>
|
||||||
@ -222,6 +240,10 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="split split-horizontal" id="IO">
|
<div class="split split-horizontal" id="IO">
|
||||||
|
<div id="data-segment-bar" class="mobile-only" role="tablist" aria-label="Input and output">
|
||||||
|
<button type="button" id="seg-input" role="tab" aria-selected="true" class="active">Input</button>
|
||||||
|
<button type="button" id="seg-output" role="tab" aria-selected="false">Output</button>
|
||||||
|
</div>
|
||||||
<div id="input" class="split no-select" data-help-title="Input pane" data-help="<p>Input data can be entered by typing it in, pasting it in, dragging it in, or using the 'Load file' or 'Load folder' buttons.</p><p>CyberChef does its best to represent data as accurately as possible to ensure you know exactly what you are working with. Non-printable characters are represented using control character pictures, for example a null byte (0x00) is displayed like this: <span title='Control character null' aria-label='Control character null' class='cm-specialChar'>␀</span>.</p>">
|
<div id="input" class="split no-select" data-help-title="Input pane" data-help="<p>Input data can be entered by typing it in, pasting it in, dragging it in, or using the 'Load file' or 'Load folder' buttons.</p><p>CyberChef does its best to represent data as accurately as possible to ensure you know exactly what you are working with. Non-printable characters are represented using control character pictures, for example a null byte (0x00) is displayed like this: <span title='Control character null' aria-label='Control character null' class='cm-specialChar'>␀</span>.</p>">
|
||||||
<div class="title no-select">
|
<div class="title no-select">
|
||||||
<label for="input-text">Input</label>
|
<label for="input-text">Input</label>
|
||||||
@ -341,6 +363,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<nav id="mobile-nav" class="mobile-only" role="tablist" aria-label="Main navigation">
|
||||||
|
<button type="button" id="nav-recipe" role="tab" aria-selected="true" class="active" data-pane="recipe">
|
||||||
|
<i class="material-icons" aria-hidden="true">list_alt</i>
|
||||||
|
Recipe
|
||||||
|
</button>
|
||||||
|
<button type="button" id="nav-ops-fab" aria-label="Add operation" aria-expanded="false" aria-controls="operations">
|
||||||
|
<i class="material-icons" aria-hidden="true">add</i>
|
||||||
|
</button>
|
||||||
|
<button type="button" id="nav-data" role="tab" aria-selected="false" data-pane="data">
|
||||||
|
<i class="material-icons" aria-hidden="true">swap_horiz</i>
|
||||||
|
Data
|
||||||
|
</button>
|
||||||
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal fade" id="save-modal" tabindex="-1" role="dialog">
|
<div class="modal fade" id="save-modal" tabindex="-1" role="dialog">
|
||||||
|
|||||||
@ -34,6 +34,7 @@
|
|||||||
@import "./layout/_operations.css";
|
@import "./layout/_operations.css";
|
||||||
@import "./layout/_recipe.css";
|
@import "./layout/_recipe.css";
|
||||||
@import "./layout/_structure.css";
|
@import "./layout/_structure.css";
|
||||||
|
@import "./layout/_responsive.css";
|
||||||
|
|
||||||
/* Operations */
|
/* Operations */
|
||||||
@import "./operations/diff.css";
|
@import "./operations/diff.css";
|
||||||
|
|||||||
382
src/web/stylesheets/layout/_responsive.css
Normal file
382
src/web/stylesheets/layout/_responsive.css
Normal file
@ -0,0 +1,382 @@
|
|||||||
|
/**
|
||||||
|
* Responsive phone / tablet layout for CyberChef.
|
||||||
|
* Driven by [data-layout] on #content-wrapper (set via ResizeObserver).
|
||||||
|
*
|
||||||
|
* @author blepman
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
/* ---- Chrome defaults (hidden on desktop) ---- */
|
||||||
|
|
||||||
|
.responsive-chrome,
|
||||||
|
.mobile-only,
|
||||||
|
.tablet-only,
|
||||||
|
.responsive-only {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#ops-overlay {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Shared non-desktop ---- */
|
||||||
|
|
||||||
|
[data-layout="mobile"] .responsive-only,
|
||||||
|
[data-layout="tablet"] .responsive-only {
|
||||||
|
display: inline-flex !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="mobile"] .gutter,
|
||||||
|
[data-layout="tablet"] .gutter {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="mobile"] #workspace-wrapper,
|
||||||
|
[data-layout="tablet"] #workspace-wrapper {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="mobile"] #operations.split,
|
||||||
|
[data-layout="mobile"] #recipe.split,
|
||||||
|
[data-layout="mobile"] #IO.split,
|
||||||
|
[data-layout="tablet"] #operations.split,
|
||||||
|
[data-layout="tablet"] #recipe.split,
|
||||||
|
[data-layout="tablet"] #IO.split {
|
||||||
|
float: none;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="mobile"] #banner .col:first-child,
|
||||||
|
[data-layout="tablet"] #banner .col:first-child {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="mobile"] #banner,
|
||||||
|
[data-layout="tablet"] #banner {
|
||||||
|
min-height: 40px;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="mobile"] #workspace-wrapper,
|
||||||
|
[data-layout="tablet"] #workspace-wrapper {
|
||||||
|
top: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Reorder / touch affordances */
|
||||||
|
.op-reorder-up,
|
||||||
|
.op-reorder-down {
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 20px !important;
|
||||||
|
margin-left: 2px;
|
||||||
|
min-width: 44px;
|
||||||
|
min-height: 44px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.op-list li.operation {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Tablet (768–1023) ---- */
|
||||||
|
|
||||||
|
[data-layout="tablet"] .tablet-only {
|
||||||
|
display: inline-flex !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="tablet"] #recipe {
|
||||||
|
flex: 1 1 45%;
|
||||||
|
min-width: 0;
|
||||||
|
height: 100%;
|
||||||
|
border-right: 1px solid var(--primary-border-colour);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="tablet"] #IO {
|
||||||
|
flex: 1 1 55%;
|
||||||
|
min-width: 0;
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="tablet"] #IO #input,
|
||||||
|
[data-layout="tablet"] #IO #output {
|
||||||
|
flex: 1 1 50%;
|
||||||
|
width: 100%;
|
||||||
|
height: 50%;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="tablet"] #operations {
|
||||||
|
position: fixed;
|
||||||
|
top: 40px;
|
||||||
|
left: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: min(360px, 90%);
|
||||||
|
z-index: 1200;
|
||||||
|
background: var(--primary-background-colour);
|
||||||
|
border-right: 1px solid var(--primary-border-colour);
|
||||||
|
transform: translateX(-105%);
|
||||||
|
transition: transform 0.2s ease;
|
||||||
|
box-shadow: 2px 0 12px rgba(0, 0, 0, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="tablet"].ops-open #operations,
|
||||||
|
body[data-layout="tablet"] #content-wrapper.ops-open #operations {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="tablet"] #ops-overlay:not([hidden]) {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
inset: 40px 0 0 0;
|
||||||
|
z-index: 1100;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="tablet"] #ops-scrim {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="tablet"] #tablet-ops-toggle {
|
||||||
|
display: inline-flex !important;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
margin-right: 8px;
|
||||||
|
min-height: 44px;
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="tablet"] #data-segment-bar,
|
||||||
|
[data-layout="tablet"] #mobile-nav {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ---- Mobile (<768) ---- */
|
||||||
|
|
||||||
|
[data-layout="mobile"] .mobile-only {
|
||||||
|
display: flex !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="mobile"] #workspace-wrapper {
|
||||||
|
flex-direction: column;
|
||||||
|
bottom: 56px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="mobile"] #recipe,
|
||||||
|
[data-layout="mobile"] #IO {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
width: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
height: 100%;
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="mobile"][data-mobile-pane="recipe"] #recipe,
|
||||||
|
#content-wrapper[data-layout="mobile"][data-mobile-pane="recipe"] #recipe {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="mobile"][data-mobile-pane="data"] #IO,
|
||||||
|
#content-wrapper[data-layout="mobile"][data-mobile-pane="data"] #IO {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="mobile"] #IO #input,
|
||||||
|
[data-layout="mobile"] #IO #output {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
|
display: none;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="mobile"][data-data-segment="input"] #IO #input,
|
||||||
|
#content-wrapper[data-layout="mobile"][data-data-segment="input"] #IO #input {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="mobile"][data-data-segment="output"] #IO #output,
|
||||||
|
#content-wrapper[data-layout="mobile"][data-data-segment="output"] #IO #output {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="mobile"] #data-segment-bar {
|
||||||
|
display: flex !important;
|
||||||
|
flex: 0 0 auto;
|
||||||
|
gap: 0;
|
||||||
|
border-bottom: 1px solid var(--primary-border-colour);
|
||||||
|
background: var(--secondary-background-colour);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="mobile"] #data-segment-bar button {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 44px;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--primary-font-colour);
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="mobile"] #data-segment-bar button.active {
|
||||||
|
border-bottom: 3px solid var(--primary-font-colour);
|
||||||
|
background: var(--primary-background-colour);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="mobile"][data-mobile-pane="recipe"] #data-segment-bar {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Sticky Bake bar sits above bottom nav */
|
||||||
|
[data-layout="mobile"] #controls,
|
||||||
|
[data-layout="mobile"] #controls.mobile-controls-bar {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 56px;
|
||||||
|
z-index: 900;
|
||||||
|
padding: 8px 4px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="mobile"] #rec-list {
|
||||||
|
padding-bottom: 88px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="mobile"] #IO #input-wrapper,
|
||||||
|
[data-layout="mobile"] #IO #output-wrapper {
|
||||||
|
padding-bottom: 88px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="mobile"] #operations {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
top: auto;
|
||||||
|
height: 85dvh;
|
||||||
|
max-height: 85vh;
|
||||||
|
z-index: 1300;
|
||||||
|
background: var(--primary-background-colour);
|
||||||
|
border-radius: 12px 12px 0 0;
|
||||||
|
transform: translateY(105%);
|
||||||
|
transition: transform 0.25s ease;
|
||||||
|
box-shadow: 0 -4px 20px rgba(0, 0, 0, 0.25);
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="mobile"].ops-open #operations,
|
||||||
|
#content-wrapper[data-layout="mobile"].ops-open #operations {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="mobile"] #ops-overlay:not([hidden]) {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
inset: 0;
|
||||||
|
z-index: 1200;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="mobile"] #ops-scrim {
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="mobile"] #ops-sheet-header {
|
||||||
|
display: flex !important;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 8px 12px;
|
||||||
|
border-bottom: 1px solid var(--primary-border-colour);
|
||||||
|
min-height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="mobile"] #ops-sheet-header .ops-handle {
|
||||||
|
width: 40px;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 2px;
|
||||||
|
background: var(--secondary-border-colour);
|
||||||
|
margin: 4px auto 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="mobile"] #mobile-nav {
|
||||||
|
display: flex !important;
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
height: 56px;
|
||||||
|
z-index: 1000;
|
||||||
|
background: var(--secondary-background-colour);
|
||||||
|
border-top: 1px solid var(--primary-border-colour);
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: space-around;
|
||||||
|
padding-bottom: env(safe-area-inset-bottom, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="mobile"] #mobile-nav button {
|
||||||
|
flex: 1;
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--primary-font-colour);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
min-height: 44px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="mobile"] #mobile-nav button.active {
|
||||||
|
color: var(--input-highlight-colour, #4caf50);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="mobile"] #mobile-nav .material-icons {
|
||||||
|
font-size: 22px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="mobile"] #nav-ops-fab {
|
||||||
|
flex: 0 0 56px;
|
||||||
|
width: 56px;
|
||||||
|
height: 56px;
|
||||||
|
margin-top: -12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--primary-font-colour);
|
||||||
|
color: var(--primary-background-colour);
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="mobile"] #nav-ops-fab .material-icons {
|
||||||
|
font-size: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
[data-layout="mobile"] #tablet-ops-toggle {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-reduced-motion: reduce) {
|
||||||
|
[data-layout="mobile"] #operations,
|
||||||
|
[data-layout="tablet"] #operations {
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Fallback when container queries are unsupported: mirror via body attribute already set */
|
||||||
|
@supports (container-type: inline-size) {
|
||||||
|
#content-wrapper.cyberchef-root {
|
||||||
|
container-type: inline-size;
|
||||||
|
container-name: cyberchef;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -229,9 +229,46 @@ class OperationsWaiter {
|
|||||||
* @param {event} e
|
* @param {event} e
|
||||||
*/
|
*/
|
||||||
operationDblclick(e) {
|
operationDblclick(e) {
|
||||||
const li = e.target;
|
const li = e.target.closest("li.operation");
|
||||||
|
if (!li) return;
|
||||||
|
|
||||||
this.manager.recipe.addOperation(li.textContent);
|
this.manager.recipe.addOperation(this.operationNameFromListItem(li));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Tap-to-add on phone/tablet layouts (single click).
|
||||||
|
* Desktop keeps double-click / drag-and-drop.
|
||||||
|
*
|
||||||
|
* @param {event} e
|
||||||
|
*/
|
||||||
|
operationClick(e) {
|
||||||
|
if (!this.manager.responsive || !this.manager.responsive.isTouchLayout()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ignore clicks on remove icons (favourites editor)
|
||||||
|
if (e.target.closest(".remove-icon")) return;
|
||||||
|
|
||||||
|
const li = e.target.closest("li.operation");
|
||||||
|
if (!li || !li.closest(".op-list")) return;
|
||||||
|
|
||||||
|
e.preventDefault();
|
||||||
|
this.manager.recipe.addOperation(this.operationNameFromListItem(li));
|
||||||
|
this.manager.responsive.afterOperationAdded();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract operation name from a list item without icon ligature text.
|
||||||
|
*
|
||||||
|
* @param {Element} li
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
operationNameFromListItem(li) {
|
||||||
|
const clone = li.cloneNode(true);
|
||||||
|
clone.querySelectorAll("i").forEach(icon => icon.remove());
|
||||||
|
return clone.textContent.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -1531,6 +1531,13 @@ class OutputWaiter {
|
|||||||
* Resizes the output frame to be as large as possible, or restores it to its original size.
|
* Resizes the output frame to be as large as possible, or restores it to its original size.
|
||||||
*/
|
*/
|
||||||
maximiseOutputClick(e) {
|
maximiseOutputClick(e) {
|
||||||
|
if (this.manager.responsive && this.manager.responsive.isTouchLayout()) {
|
||||||
|
// On phone/tablet, switch to the Data/Output view instead of Split.js maximise.
|
||||||
|
this.manager.responsive.showMobilePane("data");
|
||||||
|
this.manager.responsive.showDataSegment("output");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const el = e.target.id === "maximise-output" ? e.target : e.target.parentNode;
|
const el = e.target.id === "maximise-output" ? e.target : e.target.parentNode;
|
||||||
|
|
||||||
if (el.getAttribute("data-original-title").indexOf("Maximise") === 0) {
|
if (el.getAttribute("data-original-title").indexOf("Maximise") === 0) {
|
||||||
|
|||||||
@ -463,6 +463,36 @@ class RecipeWaiter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move a recipe operation one step up (touch layout affordance).
|
||||||
|
*
|
||||||
|
* @param {event} e
|
||||||
|
*/
|
||||||
|
moveOpUp(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const li = e.target.closest("li.operation");
|
||||||
|
if (!li || !li.previousElementSibling) return;
|
||||||
|
li.parentNode.insertBefore(li, li.previousElementSibling);
|
||||||
|
window.dispatchEvent(this.manager.statechange);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Move a recipe operation one step down (touch layout affordance).
|
||||||
|
*
|
||||||
|
* @param {event} e
|
||||||
|
*/
|
||||||
|
moveOpDown(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
const li = e.target.closest("li.operation");
|
||||||
|
if (!li || !li.nextElementSibling) return;
|
||||||
|
li.parentNode.insertBefore(li.nextElementSibling, li);
|
||||||
|
window.dispatchEvent(this.manager.statechange);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handler for operation dropdown events from toggleString arguments.
|
* Handler for operation dropdown events from toggleString arguments.
|
||||||
* Sets the selected option as the name of the button.
|
* Sets the selected option as the name of the button.
|
||||||
|
|||||||
283
src/web/waiters/ResponsiveLayoutWaiter.mjs
Normal file
283
src/web/waiters/ResponsiveLayoutWaiter.mjs
Normal file
@ -0,0 +1,283 @@
|
|||||||
|
/**
|
||||||
|
* Responsive phone / tablet layout for CyberChef.
|
||||||
|
* Uses container width (ResizeObserver) so iframe embeds behave correctly.
|
||||||
|
*
|
||||||
|
* @author blepman
|
||||||
|
* @license Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
const BREAKPOINT_MOBILE = 768;
|
||||||
|
const BREAKPOINT_TABLET = 1024;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Manages layout modes: mobile | tablet | desktop.
|
||||||
|
*/
|
||||||
|
class ResponsiveLayoutWaiter {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {App} app
|
||||||
|
* @param {Manager} manager
|
||||||
|
*/
|
||||||
|
constructor(app, manager) {
|
||||||
|
this.app = app;
|
||||||
|
this.manager = manager;
|
||||||
|
this.root = null;
|
||||||
|
this.observer = null;
|
||||||
|
this.currentMode = null;
|
||||||
|
this.mobilePane = "recipe"; // recipe | data
|
||||||
|
this.dataSegment = "input"; // input | output
|
||||||
|
this.opsOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialise observer and chrome after DOM is ready.
|
||||||
|
*/
|
||||||
|
setup() {
|
||||||
|
this.root = document.getElementById("content-wrapper");
|
||||||
|
if (!this.root) return;
|
||||||
|
|
||||||
|
this.root.classList.add("cyberchef-root");
|
||||||
|
this.root.style.containerType = "inline-size";
|
||||||
|
this.root.style.containerName = "cyberchef";
|
||||||
|
|
||||||
|
this.bindChrome();
|
||||||
|
|
||||||
|
if (typeof ResizeObserver !== "undefined") {
|
||||||
|
this.observer = new ResizeObserver(entries => {
|
||||||
|
const width = entries[0].contentRect.width;
|
||||||
|
this.applyMode(this.modeFromWidth(width));
|
||||||
|
});
|
||||||
|
this.observer.observe(this.root);
|
||||||
|
this.applyMode(this.modeFromWidth(this.root.getBoundingClientRect().width));
|
||||||
|
} else {
|
||||||
|
const onResize = () => {
|
||||||
|
this.applyMode(this.modeFromWidth(window.innerWidth));
|
||||||
|
};
|
||||||
|
window.addEventListener("resize", onResize);
|
||||||
|
onResize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {number} width
|
||||||
|
* @returns {"mobile"|"tablet"|"desktop"}
|
||||||
|
*/
|
||||||
|
modeFromWidth(width) {
|
||||||
|
if (width < BREAKPOINT_MOBILE) return "mobile";
|
||||||
|
if (width < BREAKPOINT_TABLET) return "tablet";
|
||||||
|
return "desktop";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wire mobile/tablet chrome controls.
|
||||||
|
*/
|
||||||
|
bindChrome() {
|
||||||
|
const navRecipe = document.getElementById("nav-recipe");
|
||||||
|
const navData = document.getElementById("nav-data");
|
||||||
|
const navOps = document.getElementById("nav-ops-fab");
|
||||||
|
const opsClose = document.getElementById("ops-close");
|
||||||
|
const opsScrim = document.getElementById("ops-scrim");
|
||||||
|
const segInput = document.getElementById("seg-input");
|
||||||
|
const segOutput = document.getElementById("seg-output");
|
||||||
|
|
||||||
|
if (navRecipe) {
|
||||||
|
navRecipe.addEventListener("click", () => this.showMobilePane("recipe"));
|
||||||
|
}
|
||||||
|
if (navData) {
|
||||||
|
navData.addEventListener("click", () => this.showMobilePane("data"));
|
||||||
|
}
|
||||||
|
if (navOps) {
|
||||||
|
navOps.addEventListener("click", () => this.toggleOps(true));
|
||||||
|
}
|
||||||
|
if (opsClose) {
|
||||||
|
opsClose.addEventListener("click", () => this.toggleOps(false));
|
||||||
|
}
|
||||||
|
if (opsScrim) {
|
||||||
|
opsScrim.addEventListener("click", () => this.toggleOps(false));
|
||||||
|
}
|
||||||
|
if (segInput) {
|
||||||
|
segInput.addEventListener("click", () => this.showDataSegment("input"));
|
||||||
|
}
|
||||||
|
if (segOutput) {
|
||||||
|
segOutput.addEventListener("click", () => this.showDataSegment("output"));
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener("keydown", e => {
|
||||||
|
if (e.key === "Escape" && this.opsOpen) {
|
||||||
|
this.toggleOps(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const tabletOps = document.getElementById("tablet-ops-toggle");
|
||||||
|
if (tabletOps) {
|
||||||
|
tabletOps.addEventListener("click", () => this.toggleOps(!this.opsOpen));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch layout mode and manage Split.js.
|
||||||
|
*
|
||||||
|
* @param {"mobile"|"tablet"|"desktop"} mode
|
||||||
|
*/
|
||||||
|
applyMode(mode) {
|
||||||
|
if (mode === this.currentMode || !this.root) return;
|
||||||
|
|
||||||
|
const prev = this.currentMode;
|
||||||
|
this.currentMode = mode;
|
||||||
|
this.root.dataset.layout = mode;
|
||||||
|
document.body.dataset.layout = mode;
|
||||||
|
|
||||||
|
this.placeControls(mode);
|
||||||
|
|
||||||
|
if (mode === "desktop") {
|
||||||
|
this.toggleOps(false);
|
||||||
|
if (prev && prev !== "desktop") {
|
||||||
|
this.app.initialiseSplitter();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
this.app.destroySplitters();
|
||||||
|
if (mode === "mobile") {
|
||||||
|
this.showMobilePane(this.mobilePane);
|
||||||
|
this.showDataSegment(this.dataSegment);
|
||||||
|
} else {
|
||||||
|
this.showDataSegment("both");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.app.adjustComponentSizes();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* On mobile the Bake bar must stay visible across panes, so move #controls
|
||||||
|
* out of #recipe (which is display:none when Data is active).
|
||||||
|
*
|
||||||
|
* @param {"mobile"|"tablet"|"desktop"} mode
|
||||||
|
*/
|
||||||
|
placeControls(mode) {
|
||||||
|
const controls = document.getElementById("controls");
|
||||||
|
const recipe = document.getElementById("recipe");
|
||||||
|
const workspace = document.getElementById("workspace-wrapper");
|
||||||
|
if (!controls || !recipe || !workspace) return;
|
||||||
|
|
||||||
|
if (mode === "mobile") {
|
||||||
|
if (controls.parentElement !== workspace) {
|
||||||
|
workspace.appendChild(controls);
|
||||||
|
}
|
||||||
|
controls.classList.add("mobile-controls-bar");
|
||||||
|
} else if (controls.parentElement !== recipe) {
|
||||||
|
recipe.appendChild(controls);
|
||||||
|
controls.classList.remove("mobile-controls-bar");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {"recipe"|"data"} pane
|
||||||
|
*/
|
||||||
|
showMobilePane(pane) {
|
||||||
|
this.mobilePane = pane;
|
||||||
|
if (this.root) {
|
||||||
|
this.root.dataset.mobilePane = pane;
|
||||||
|
}
|
||||||
|
|
||||||
|
const navRecipe = document.getElementById("nav-recipe");
|
||||||
|
const navData = document.getElementById("nav-data");
|
||||||
|
if (navRecipe) {
|
||||||
|
navRecipe.classList.toggle("active", pane === "recipe");
|
||||||
|
navRecipe.setAttribute("aria-selected", pane === "recipe" ? "true" : "false");
|
||||||
|
}
|
||||||
|
if (navData) {
|
||||||
|
navData.classList.toggle("active", pane === "data");
|
||||||
|
navData.setAttribute("aria-selected", pane === "data" ? "true" : "false");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.app.adjustComponentSizes();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {"input"|"output"|"both"} segment
|
||||||
|
*/
|
||||||
|
showDataSegment(segment) {
|
||||||
|
this.dataSegment = segment === "both" ? "input" : segment;
|
||||||
|
if (this.root) {
|
||||||
|
this.root.dataset.dataSegment = segment;
|
||||||
|
}
|
||||||
|
|
||||||
|
const segInput = document.getElementById("seg-input");
|
||||||
|
const segOutput = document.getElementById("seg-output");
|
||||||
|
if (segInput && segment !== "both") {
|
||||||
|
segInput.classList.toggle("active", segment === "input");
|
||||||
|
segInput.setAttribute("aria-selected", segment === "input" ? "true" : "false");
|
||||||
|
}
|
||||||
|
if (segOutput && segment !== "both") {
|
||||||
|
segOutput.classList.toggle("active", segment === "output");
|
||||||
|
segOutput.setAttribute("aria-selected", segment === "output" ? "true" : "false");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.app.adjustComponentSizes();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open / close operations sheet (mobile) or drawer (tablet).
|
||||||
|
*
|
||||||
|
* @param {boolean} open
|
||||||
|
*/
|
||||||
|
toggleOps(open) {
|
||||||
|
this.opsOpen = open;
|
||||||
|
if (this.root) {
|
||||||
|
this.root.classList.toggle("ops-open", open);
|
||||||
|
}
|
||||||
|
|
||||||
|
const overlay = document.getElementById("ops-overlay");
|
||||||
|
if (overlay) {
|
||||||
|
overlay.hidden = !open;
|
||||||
|
overlay.setAttribute("aria-hidden", open ? "false" : "true");
|
||||||
|
}
|
||||||
|
|
||||||
|
const ops = document.getElementById("operations");
|
||||||
|
if (ops) {
|
||||||
|
ops.setAttribute("aria-hidden", open || this.currentMode === "desktop" ? "false" : "true");
|
||||||
|
if (open) {
|
||||||
|
const search = document.getElementById("search");
|
||||||
|
if (search) search.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const fab = document.getElementById("nav-ops-fab");
|
||||||
|
if (fab) {
|
||||||
|
fab.setAttribute("aria-expanded", open ? "true" : "false");
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabletOps = document.getElementById("tablet-ops-toggle");
|
||||||
|
if (tabletOps) {
|
||||||
|
tabletOps.setAttribute("aria-expanded", open ? "true" : "false");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* After adding an operation from the list, keep sheet open for multi-add.
|
||||||
|
*/
|
||||||
|
afterOperationAdded() {
|
||||||
|
if (this.currentMode === "mobile") {
|
||||||
|
this.showMobilePane("recipe");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
isTouchLayout() {
|
||||||
|
return this.currentMode === "mobile" || this.currentMode === "tablet";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ResponsiveLayoutWaiter;
|
||||||
Loading…
x
Reference in New Issue
Block a user