mirror of
https://github.com/gchq/CyberChef
synced 2025-01-25 02:35:02 +00:00
1cf83c2485
Re-enable go to tab button. Active tab is now autobaked on load completion. Handle (ish) loaderWorker errors. Improve load performance.
933 lines
30 KiB
JavaScript
Executable file
933 lines
30 KiB
JavaScript
Executable file
/**
|
|
* @author n1474335 [n1474335@gmail.com]
|
|
* @author j433866 [j433866@gmail.com]
|
|
* @copyright Crown Copyright 2016
|
|
* @license Apache-2.0
|
|
*/
|
|
|
|
import Utils from "../core/Utils";
|
|
import FileSaver from "file-saver";
|
|
import zip from "zlibjs/bin/zip.min";
|
|
|
|
const Zlib = zip.Zlib;
|
|
|
|
/**
|
|
* Waiter to handle events related to the output
|
|
*/
|
|
class OutputWaiter {
|
|
|
|
/**
|
|
* OutputWaiter constructor.
|
|
*
|
|
* @param {App} app - The main view object for CyberChef.
|
|
* @param {Manager} manager - The CyberChef event manager
|
|
*/
|
|
constructor(app, manager) {
|
|
this.app = app;
|
|
this.manager = manager;
|
|
|
|
this.outputs = {};
|
|
this.activeTab = -1;
|
|
|
|
this.maxTabs = 4; // Calculate this
|
|
}
|
|
|
|
/**
|
|
* Calculates the maximum number of tabs to display
|
|
*/
|
|
calcMaxTabs() {
|
|
const numTabs = Math.floor((document.getElementById("IO").offsetWidth - 75) / 120);
|
|
this.maxTabs = numTabs;
|
|
}
|
|
|
|
/**
|
|
* Gets the output for the specified input number
|
|
*
|
|
* @param {number} inputNum
|
|
* @returns {string | ArrayBuffer}
|
|
*/
|
|
getOutput(inputNum) {
|
|
if (this.outputs[inputNum] === undefined || this.outputs[inputNum] === null) return -1;
|
|
|
|
if (this.outputs[inputNum].data === null) return "";
|
|
|
|
if (typeof this.outputs[inputNum].data.dish.value === "string") {
|
|
return this.outputs[inputNum].data.dish.value;
|
|
} else {
|
|
return this.outputs[inputNum].data.dish.value || "";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the output string or FileBuffer for the active input
|
|
*
|
|
* @returns {string | ArrayBuffer}
|
|
*/
|
|
getActive() {
|
|
return this.getOutput(this.getActiveTab());
|
|
}
|
|
|
|
/**
|
|
* Adds a new output to the output array.
|
|
* Creates a new tab if we have less than maxtabs tabs open
|
|
*
|
|
* @param {number} inputNum
|
|
* @param {boolean} [changeTab=true]
|
|
*/
|
|
addOutput(inputNum, changeTab = true) {
|
|
const output = this.getOutput(inputNum);
|
|
if (output !== -1) {
|
|
// Remove the output if it already exists
|
|
delete this.outputs[inputNum];
|
|
}
|
|
const newOutput = {
|
|
data: null,
|
|
inputNum: inputNum,
|
|
// statusMessage: `Input ${inputNum} has not been baked yet.`,
|
|
statusMessage: "",
|
|
error: null,
|
|
status: "inactive"
|
|
};
|
|
|
|
this.outputs[inputNum] = newOutput;
|
|
|
|
// add new tab
|
|
this.addTab(inputNum, changeTab);
|
|
}
|
|
|
|
/**
|
|
* Updates the value for the output in the output array.
|
|
* If this is the active output tab, updates the output textarea
|
|
*
|
|
* @param {Object} data
|
|
* @param {number} inputNum
|
|
*/
|
|
updateOutputValue(data, inputNum) {
|
|
if (this.getOutput(inputNum) === -1) {
|
|
this.addOutput(inputNum);
|
|
}
|
|
|
|
this.outputs[inputNum].data = data;
|
|
|
|
// set output here
|
|
this.set(inputNum);
|
|
}
|
|
|
|
/**
|
|
* Updates the status message for the output in the output array.
|
|
* If this is the active output tab, updates the output textarea
|
|
*
|
|
* @param {string} statusMessage
|
|
* @param {number} inputNum
|
|
*/
|
|
updateOutputMessage(statusMessage, inputNum) {
|
|
if (this.getOutput(inputNum) === -1) return;
|
|
this.outputs[inputNum].statusMessage = statusMessage;
|
|
this.set(inputNum);
|
|
}
|
|
|
|
/**
|
|
* Updates the error value for the output in the output array.
|
|
* If this is the active output tab, calls app.handleError.
|
|
* Otherwise, the error will be handled when the output is switched to
|
|
*
|
|
* @param {Error} error
|
|
* @param {number} inputNum
|
|
*/
|
|
updateOutputError(error, inputNum) {
|
|
if (this.getOutput(inputNum) === -1) return;
|
|
|
|
this.outputs[inputNum].error = error;
|
|
|
|
// call handle error here
|
|
// or make the error handling part of set()
|
|
this.set(inputNum);
|
|
}
|
|
|
|
/**
|
|
* Updates the status value for the output in the output array
|
|
*
|
|
* @param {string} status
|
|
* @param {number} inputNum
|
|
*/
|
|
updateOutputStatus(status, inputNum) {
|
|
if (this.getOutput(inputNum) === -1) return;
|
|
this.outputs[inputNum].status = status;
|
|
|
|
this.set(inputNum);
|
|
}
|
|
|
|
/**
|
|
* Removes an output from the output array.
|
|
*
|
|
* @param {number} inputNum
|
|
*/
|
|
removeOutput(inputNum) {
|
|
if (this.getOutput(inputNum) === -1) return;
|
|
|
|
delete (this.outputs[inputNum]);
|
|
}
|
|
|
|
/**
|
|
* Sets the output in the output textarea.
|
|
*
|
|
* @param {number} inputNum
|
|
*/
|
|
set(inputNum) {
|
|
const output = this.outputs[inputNum];
|
|
if (output === undefined || output === null) return;
|
|
|
|
if (typeof inputNum !== "number") inputNum = parseInt(inputNum, 10);
|
|
|
|
const outputText = document.getElementById("output-text");
|
|
const outputHtml = document.getElementById("output-html");
|
|
const outputFile = document.getElementById("output-file");
|
|
const outputHighlighter = document.getElementById("output-highlighter");
|
|
const inputHighlighter = document.getElementById("input-highlighter");
|
|
// If inactive, show blank
|
|
// If pending or baking, show loader and status message
|
|
// If error, style the tab and handle the error
|
|
// If done, display the output if it's the active tab
|
|
if (output.status === "inactive" || output.status === "stale") {
|
|
this.manager.controls.showStaleIndicator();
|
|
} else {
|
|
this.manager.controls.hideStaleIndicator();
|
|
}
|
|
|
|
if (output.status === "inactive") {
|
|
// An output is inactive when it has been created but has not been baked at all
|
|
// show a blank here
|
|
if (inputNum === this.getActiveTab()) {
|
|
this.toggleLoader(false);
|
|
outputText.style.display = "block";
|
|
outputHtml.style.display = "none";
|
|
outputFile.style.display = "none";
|
|
outputHighlighter.display = "block";
|
|
inputHighlighter.display = "block";
|
|
|
|
outputText.value = "";
|
|
outputHtml.innerHTML = "";
|
|
|
|
}
|
|
} else if (output.status === "pending" || output.status === "baking") {
|
|
// show the loader and the status message if it's being shown
|
|
// otherwise don't do anything
|
|
if (inputNum === this.getActiveTab()) {
|
|
this.toggleLoader(true);
|
|
document.querySelector("#output-loader .loading-msg").textContent = output.statusMessage;
|
|
}
|
|
|
|
} else if (output.status === "error") {
|
|
// style the tab if it's being shown
|
|
// run app.handleError()
|
|
if (inputNum === this.getActiveTab()) {
|
|
this.toggleLoader(false);
|
|
}
|
|
} else if (output.status === "baked") {
|
|
// Display the output if it's the active tab
|
|
this.displayTabInfo(inputNum);
|
|
if (inputNum === this.getActiveTab()) {
|
|
this.toggleLoader(false);
|
|
this.closeFile();
|
|
let scriptElements, lines, length;
|
|
const duration = output.data.duration;
|
|
|
|
switch (output.data.type) {
|
|
case "html":
|
|
outputText.style.display = "none";
|
|
outputHtml.style.display = "block";
|
|
outputFile.style.display = "none";
|
|
outputHighlighter.style.display = "none";
|
|
inputHighlighter.style.display = "none";
|
|
|
|
outputText.value = "";
|
|
outputHtml.innerHTML = output.data.result;
|
|
|
|
// Execute script sections
|
|
scriptElements = outputHtml.querySelectorAll("script");
|
|
for (let i = 0; i < scriptElements.length; i++) {
|
|
try {
|
|
eval(scriptElements[i].innerHTML); // eslint-disable-line no-eval
|
|
} catch (err) {
|
|
log.error(err);
|
|
}
|
|
}
|
|
length = output.data.dish.value.length;
|
|
|
|
break;
|
|
case "ArrayBuffer":
|
|
outputText.style.display = "block";
|
|
outputHtml.style.display = "none";
|
|
outputHighlighter.display = "none";
|
|
inputHighlighter.display = "none";
|
|
|
|
outputText.value = "";
|
|
outputHtml.innerHTML = "";
|
|
|
|
length = output.data.result.length;
|
|
this.setFile(output.data.result);
|
|
break;
|
|
case "string":
|
|
default:
|
|
outputText.style.display = "block";
|
|
outputHtml.style.display = "none";
|
|
outputFile.style.display = "none";
|
|
outputHighlighter.display = "block";
|
|
inputHighlighter.display = "block";
|
|
|
|
outputText.value = Utils.printable(output.data.result, true);
|
|
outputHtml.innerHTML = "";
|
|
|
|
lines = output.data.result.count("\n") + 1;
|
|
length = output.data.result.length;
|
|
break;
|
|
}
|
|
this.setOutputInfo(length, lines, duration);
|
|
this.backgroundMagic();
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Shows file details
|
|
*
|
|
* @param {ArrayBuffer} buf
|
|
*/
|
|
setFile(buf) {
|
|
const file = new File([buf], "output.dat");
|
|
|
|
// Display file overlay in output area with details
|
|
const fileOverlay = document.getElementById("output-file"),
|
|
fileSize = document.getElementById("output-file-size"),
|
|
outputText = document.getElementById("output-text"),
|
|
fileSlice = buf.slice(0, 4096);
|
|
|
|
fileOverlay.style.display = "block";
|
|
fileSize.textContent = file.size.toLocaleString() + " bytes";
|
|
|
|
outputText.classList.add("blur");
|
|
outputText.value = Utils.printable(Utils.arrayBufferToStr(fileSlice));
|
|
}
|
|
|
|
/**
|
|
* Clears output file details
|
|
*/
|
|
closeFile() {
|
|
document.getElementById("output-file").style.display = "none";
|
|
document.getElementById("output-text").classList.remove("blur");
|
|
}
|
|
|
|
/**
|
|
* Save bombe object then remove it from the DOM so that it does not cause performance issues.
|
|
*/
|
|
saveBombe() {
|
|
this.bombeEl = document.getElementById("bombe");
|
|
this.bombeEl.parentNode.removeChild(this.bombeEl);
|
|
}
|
|
|
|
/**
|
|
* Shows or hides the output loading screen.
|
|
* The animated Bombe SVG, whilst quite aesthetically pleasing, is reasonably CPU
|
|
* intensive, so we remove it from the DOM when not in use. We only show it if the
|
|
* recipe is taking longer than 200ms. We add it to the DOM just before that so that
|
|
* it is ready to fade in without stuttering.
|
|
*
|
|
* @param {boolean} value - true == show loader
|
|
*/
|
|
toggleLoader(value) {
|
|
clearTimeout(this.appendBombeTimeout);
|
|
clearTimeout(this.outputLoaderTimeout);
|
|
|
|
const outputLoader = document.getElementById("output-loader"),
|
|
outputElement = document.getElementById("output-text"),
|
|
animation = document.getElementById("output-loader-animation");
|
|
|
|
if (value) {
|
|
this.manager.controls.hideStaleIndicator();
|
|
|
|
// Start a timer to add the Bombe to the DOM just before we make it
|
|
// visible so that there is no stuttering
|
|
this.appendBombeTimeout = setTimeout(function() {
|
|
animation.appendChild(this.bombeEl);
|
|
}.bind(this), 150);
|
|
|
|
// Show the loading screen
|
|
this.outputLoaderTimeout = setTimeout(function() {
|
|
outputElement.disabled = true;
|
|
outputLoader.style.visibility = "visible";
|
|
outputLoader.style.opacity = 1;
|
|
}, 200);
|
|
} else {
|
|
// Remove the Bombe from the DOM to save resources
|
|
this.outputLoaderTimeout = setTimeout(function () {
|
|
try {
|
|
animation.removeChild(this.bombeEl);
|
|
} catch (err) {}
|
|
}.bind(this), 500);
|
|
outputElement.disabled = false;
|
|
outputLoader.style.opacity = 0;
|
|
outputLoader.style.visibility = "hidden";
|
|
// this.setStatusMsg("");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler for save click events.
|
|
* Saves the current output to a file.
|
|
*/
|
|
saveClick() {
|
|
this.downloadFile();
|
|
}
|
|
|
|
/**
|
|
* Handler for file download events.
|
|
*/
|
|
async downloadFile() {
|
|
const fileName = window.prompt("Please enter a filename: ", "download.dat");
|
|
const file = new File([this.getActive()], fileName);
|
|
FileSaver.saveAs(file, fileName, false);
|
|
}
|
|
|
|
/**
|
|
* Handler for save all click event
|
|
* Saves all outputs to a single archvie file
|
|
*/
|
|
saveAllClick() {
|
|
this.downloadAllFiles();
|
|
}
|
|
|
|
/**
|
|
* Handler for download all files events.
|
|
*/
|
|
async downloadAllFiles() {
|
|
const fileName = window.prompt("Please enter a filename: ", "download.zip");
|
|
const fileExt = window.prompt("Please enter a file extension for the files: ", ".txt");
|
|
const zip = new Zlib.Zip();
|
|
for (let i = 0; i < this.outputs.length; i++) {
|
|
const name = Utils.strToByteArray(this.outputs[i].inputNum + fileExt);
|
|
let out = this.getOutput(this.outputs[i].inputNum);
|
|
if (typeof out === "string") {
|
|
out = Utils.strToUtf8ByteArray(out);
|
|
}
|
|
out = new Uint8Array(out);
|
|
// options.filename = Utils.strToByteArray(this.outputs[i].inputNum + ".dat");
|
|
zip.addFile(out, {filename: name});
|
|
}
|
|
const file = new File([zip.compress()], fileName);
|
|
FileSaver.saveAs(file, fileName, false);
|
|
}
|
|
|
|
/**
|
|
* Adds a new output tab.
|
|
*
|
|
* @param {number} inputNum
|
|
* @param {boolean} [changeTab=true]
|
|
*/
|
|
addTab(inputNum, changeTab = true) {
|
|
const tabsWrapper = document.getElementById("output-tabs");
|
|
const numTabs = tabsWrapper.children.length;
|
|
|
|
if (numTabs < this.maxTabs) {
|
|
// Create a new tab element
|
|
const newTab = this.createTabElement(inputNum);
|
|
|
|
tabsWrapper.appendChild(newTab);
|
|
|
|
if (numTabs > 0) {
|
|
tabsWrapper.parentElement.style.display = "block";
|
|
|
|
document.getElementById("output-wrapper").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
|
|
document.getElementById("output-highlighter").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
|
|
document.getElementById("output-file").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
|
|
document.getElementById("output-loader").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
|
|
|
|
document.getElementById("save-all-to-file").style.display = "inline-block";
|
|
}
|
|
}
|
|
|
|
if (changeTab) {
|
|
this.changeTab(inputNum, false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Changes the active tab
|
|
*
|
|
* @param {number} inputNum
|
|
* @param {boolean} [changeInput = false]
|
|
*/
|
|
changeTab(inputNum, changeInput = false) {
|
|
const currentNum = this.getActiveTab();
|
|
if (this.getOutput(inputNum) === -1) return;
|
|
|
|
const tabsWrapper = document.getElementById("output-tabs");
|
|
const tabs = tabsWrapper.children;
|
|
|
|
let found = false;
|
|
for (let i = 0; i < tabs.length; i++) {
|
|
if (tabs.item(i).getAttribute("inputNum") === inputNum.toString()) {
|
|
tabs.item(i).classList.add("active-output-tab");
|
|
this.activeTab = inputNum;
|
|
found = true;
|
|
} else {
|
|
tabs.item(i).classList.remove("active-output-tab");
|
|
}
|
|
}
|
|
if (!found) {
|
|
let direction = "right";
|
|
if (currentNum > inputNum) {
|
|
direction = "left";
|
|
}
|
|
|
|
const newOutputs = this.getNearbyNums(inputNum, direction);
|
|
for (let i = 0; i < newOutputs.length; i++) {
|
|
tabs.item(i).setAttribute("inputNum", newOutputs[i].toString());
|
|
this.displayTabInfo(newOutputs[i]);
|
|
if (newOutputs[i] === inputNum) {
|
|
this.activeTab = inputNum;
|
|
tabs.item(i).classList.add("active-output-tab");
|
|
}
|
|
}
|
|
}
|
|
|
|
this.set(inputNum);
|
|
|
|
if (changeInput) {
|
|
this.manager.input.changeTab(inputNum, false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler for changing tabs event
|
|
*
|
|
* @param {event} mouseEvent
|
|
*/
|
|
changeTabClick(mouseEvent) {
|
|
if (!mouseEvent.target) return;
|
|
const tabNum = mouseEvent.target.parentElement.getAttribute("inputNum");
|
|
if (tabNum) {
|
|
this.changeTab(parseInt(tabNum, 10), this.app.options.syncTabs);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler for changing to the left tab
|
|
*/
|
|
changeTabLeft() {
|
|
const currentTab = this.getActiveTab();
|
|
this.changeTab(this.getPreviousInputNum(currentTab), this.app.options.syncTabs);
|
|
}
|
|
|
|
/**
|
|
* Handler for changing to the right tab
|
|
*/
|
|
changeTabRight() {
|
|
const currentTab = this.getActiveTab();
|
|
this.changeTab(this.getNextInputNum(currentTab), this.app.options.syncTabs);
|
|
}
|
|
|
|
/**
|
|
* Handler for go to tab button clicked
|
|
*/
|
|
goToTab() {
|
|
const tabNum = parseInt(window.prompt("Enter tab number:", this.getActiveTab().toString()), 10);
|
|
if (this.getOutputIndex(tabNum) >= 0) {
|
|
this.changeTab(tabNum, this.app.options.syncTabs);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates a list of the nearby inputNums
|
|
* @param inputNum
|
|
* @param direction
|
|
*/
|
|
getNearbyNums(inputNum, direction) {
|
|
const nums = [];
|
|
for (let i = 0; i < this.maxTabs; i++) {
|
|
let newNum;
|
|
if (i === 0) {
|
|
newNum = inputNum;
|
|
} else {
|
|
switch (direction) {
|
|
case "left":
|
|
newNum = this.getNextInputNum(nums[i - 1]);
|
|
if (newNum === nums[i - 1]) {
|
|
direction = "right";
|
|
newNum = this.getPreviousInputNum(nums[i - 1]);
|
|
}
|
|
break;
|
|
case "right":
|
|
newNum = this.getPreviousInputNum(nums[i - 1]);
|
|
if (newNum === nums[i - 1]) {
|
|
direction = "left";
|
|
newNum = this.getNextInputNum(nums[i - 1]);
|
|
}
|
|
}
|
|
}
|
|
if (!nums.includes(newNum) && (newNum > 0)) {
|
|
nums.push(newNum);
|
|
}
|
|
}
|
|
nums.sort(function(a, b) {
|
|
return a - b;
|
|
});
|
|
return nums;
|
|
}
|
|
|
|
/**
|
|
* Gets the largest inputNum
|
|
*
|
|
* @returns {number}
|
|
*/
|
|
getLargestInputNum() {
|
|
let largest = 0;
|
|
const inputNums = Object.keys(this.outputs);
|
|
for (let i = 0; i < inputNums.length; i++) {
|
|
const iNum = parseInt(inputNums[i], 10);
|
|
if (iNum > largest) {
|
|
largest = iNum;
|
|
}
|
|
}
|
|
return largest;
|
|
}
|
|
|
|
/**
|
|
* Gets the smallest inputNum
|
|
*
|
|
* @returns {number}
|
|
*/
|
|
getSmallestInputNum() {
|
|
let smallest = this.getLargestInputNum();
|
|
const inputNums = Object.keys(this.outputs);
|
|
for (let i = 0; i < inputNums.length; i++) {
|
|
const iNum = parseInt(inputNums[i], 10);
|
|
if (iNum < smallest) {
|
|
smallest = iNum;
|
|
}
|
|
}
|
|
return smallest;
|
|
}
|
|
|
|
/**
|
|
* Gets the previous inputNum
|
|
*
|
|
* @param {number} inputNum - The current input number
|
|
* @returns {number}
|
|
*/
|
|
getPreviousInputNum(inputNum) {
|
|
let num = this.getSmallestInputNum();
|
|
const inputNums = Object.keys(this.outputs);
|
|
for (let i = 0; i < inputNums.length; i++) {
|
|
const iNum = parseInt(inputNums[i], 10);
|
|
if (iNum < inputNum) {
|
|
if (iNum > num) {
|
|
num = iNum;
|
|
}
|
|
}
|
|
}
|
|
return num;
|
|
}
|
|
|
|
/**
|
|
* Gets the next inputNum
|
|
*
|
|
* @param {number} inputNum - The current input number
|
|
* @returns {number}
|
|
*/
|
|
getNextInputNum(inputNum) {
|
|
let num = this.getLargestInputNum();
|
|
const inputNums = Object.keys(this.outputs);
|
|
for (let i = 0; i < inputNums.length; i++) {
|
|
const iNum = parseInt(inputNums[i], 10);
|
|
if (iNum > inputNum) {
|
|
if (iNum < num) {
|
|
num = iNum;
|
|
}
|
|
}
|
|
}
|
|
return num;
|
|
}
|
|
|
|
/**
|
|
* Removes a tab and it's corresponding output
|
|
*
|
|
* @param {number} inputNum
|
|
*/
|
|
removeTab(inputNum) {
|
|
let activeTab = this.getActiveTab();
|
|
if (this.getOutput(inputNum) === -1) return;
|
|
|
|
const tabElement = this.getTabItem(inputNum);
|
|
|
|
this.removeOutput(inputNum);
|
|
|
|
if (tabElement !== null) {
|
|
// find new tab number?
|
|
if (inputNum === activeTab) {
|
|
activeTab = this.getPreviousInputNum(activeTab);
|
|
if (activeTab === this.getActiveTab()) {
|
|
activeTab = this.getNextInputNum(activeTab);
|
|
}
|
|
}
|
|
this.refreshTabs(activeTab);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Redraw the entire tab bar to remove any outdated tabs
|
|
* @param {number} activeTab
|
|
*/
|
|
refreshTabs(activeTab) {
|
|
const tabsList = document.getElementById("output-tabs");
|
|
let newInputs = this.getNearbyNums(activeTab, "right");
|
|
if (newInputs.length < this.maxTabs) {
|
|
newInputs = this.getNearbyNums(activeTab, "left");
|
|
}
|
|
|
|
for (let i = tabsList.children.length - 1; i >= 0; i--) {
|
|
tabsList.children.item(i).remove();
|
|
}
|
|
|
|
for (let i = 0; i < newInputs.length; i++) {
|
|
tabsList.appendChild(this.createTabElement(newInputs[i]));
|
|
this.displayTabInfo(newInputs[i]);
|
|
}
|
|
|
|
if (newInputs.length > 1) {
|
|
tabsList.parentElement.style.display = "block";
|
|
|
|
document.getElementById("output-wrapper").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
|
|
document.getElementById("output-highlighter").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
|
|
document.getElementById("output-file").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
|
|
document.getElementById("output-loader").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
|
|
|
|
document.getElementById("save-all-to-file").style.display = "inline-block";
|
|
|
|
} else {
|
|
tabsList.parentElement.style.display = "none";
|
|
|
|
document.getElementById("output-wrapper").style.height = "calc(100% - var(--title-height))";
|
|
document.getElementById("output-highlighter").style.height = "calc(100% - var(--title-height))";
|
|
document.getElementById("output-file").style.height = "calc(100% - var(--title-height))";
|
|
document.getElementById("output-loader").style.height = "calc(100% - var(--title-height))";
|
|
|
|
document.getElementById("save-all-to-file").style.display = "none";
|
|
}
|
|
|
|
this.changeTab(activeTab);
|
|
|
|
}
|
|
|
|
/**
|
|
* Creates a new tab element to be added to the tab bar
|
|
*
|
|
* @param {number} inputNum
|
|
*/
|
|
createTabElement(inputNum) {
|
|
const newTab = document.createElement("li");
|
|
newTab.setAttribute("inputNum", inputNum.toString());
|
|
|
|
const newTabContent = document.createElement("div");
|
|
newTabContent.classList.add("output-tab-content");
|
|
newTabContent.innerText = `Tab ${inputNum.toString()}`;
|
|
|
|
// Do we want remove tab button on output?
|
|
newTab.appendChild(newTabContent);
|
|
|
|
return newTab;
|
|
}
|
|
|
|
/**
|
|
* Gets the number of the current active tab
|
|
*
|
|
* @returns {number}
|
|
*/
|
|
getActiveTab() {
|
|
return this.activeTab;
|
|
}
|
|
|
|
/**
|
|
* Gets the li element for a tab
|
|
*
|
|
* @param {number} inputNum
|
|
*/
|
|
getTabItem(inputNum) {
|
|
const tabs = document.getElementById("output-tabs").children;
|
|
for (let i = 0; i < tabs.length; i++) {
|
|
if (parseInt(tabs.item(i).getAttribute("inputNum"), 10) === inputNum) {
|
|
return tabs.item(i);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Display output information in the tab header
|
|
*
|
|
* @param {number} inputNum
|
|
*/
|
|
displayTabInfo(inputNum) {
|
|
const tabItem = this.getTabItem(inputNum);
|
|
|
|
if (!tabItem) return;
|
|
|
|
const tabContent = tabItem.firstElementChild;
|
|
|
|
tabContent.innerText = `Tab ${inputNum}`;
|
|
|
|
}
|
|
|
|
/**
|
|
* Displays information about the output.
|
|
*
|
|
* @param {number} length - The length of the current output string
|
|
* @param {number} lines - The number of the lines in the current output string
|
|
* @param {number} duration - The length of time (ms) it took to generate the output
|
|
*/
|
|
setOutputInfo(length, lines, duration) {
|
|
if (!length) return;
|
|
let width = length.toString().length;
|
|
width = width < 4 ? 4 : width;
|
|
|
|
const lengthStr = length.toString().padStart(width, " ").replace(/ /g, " ");
|
|
const timeStr = (duration.toString() + "ms").padStart(width, " ").replace(/ /g, " ");
|
|
|
|
let msg = "time: " + timeStr + "<br>length: " + lengthStr;
|
|
|
|
if (typeof lines === "number") {
|
|
const linesStr = lines.toString().padStart(width, " ").replace(/ /g, " ");
|
|
msg += "<br>lines: " + linesStr;
|
|
}
|
|
|
|
document.getElementById("output-info").innerHTML = msg;
|
|
document.getElementById("input-selection-info").innerHTML = "";
|
|
document.getElementById("output-selection-info").innerHTML = "";
|
|
}
|
|
|
|
/**
|
|
* Triggers the BackgroundWorker to attempt Magic on the current output.
|
|
*/
|
|
backgroundMagic() {
|
|
this.hideMagicButton();
|
|
if (!this.app.options.autoMagic || this.getActive()) return;
|
|
const sample = this.getActive().slice(0, 1000) || "";
|
|
|
|
if (sample.length) {
|
|
this.manager.background.magic(sample);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handles the results of a background Magic call.
|
|
*
|
|
* @param {Object[]} options
|
|
*/
|
|
backgroundMagicResult(options) {
|
|
if (!options.length ||
|
|
!options[0].recipe.length)
|
|
return;
|
|
|
|
const currentRecipeConfig = this.app.getRecipeConfig();
|
|
const newRecipeConfig = currentRecipeConfig.concat(options[0].recipe);
|
|
const opSequence = options[0].recipe.map(o => o.op).join(", ");
|
|
|
|
this.showMagicButton(opSequence, options[0].data, newRecipeConfig);
|
|
}
|
|
|
|
/**
|
|
* Handler for Magic click events.
|
|
*
|
|
* Loads the Magic recipe.
|
|
*
|
|
* @fires Manager#statechange
|
|
*/
|
|
magicClick() {
|
|
const magicButton = document.getElementById("magic");
|
|
this.app.setRecipeConfig(JSON.parse(magicButton.getAttribute("data-recipe")));
|
|
window.dispatchEvent(this.manager.statechange);
|
|
this.hideMagicButton();
|
|
}
|
|
|
|
/**
|
|
* Displays the Magic button with a title and adds a link to a complete recipe.
|
|
*
|
|
* @param {string} opSequence
|
|
* @param {string} result
|
|
* @param {Object[]} recipeConfig
|
|
*/
|
|
showMagicButton(opSequence, result, recipeConfig) {
|
|
const magicButton = document.getElementById("magic");
|
|
magicButton.setAttribute("data-original-title", `<i>${opSequence}</i> will produce <span class="data-text">"${Utils.escapeHtml(Utils.truncate(result), 30)}"</span>`);
|
|
magicButton.setAttribute("data-recipe", JSON.stringify(recipeConfig), null, "");
|
|
magicButton.classList.remove("hidden");
|
|
}
|
|
|
|
|
|
/**
|
|
* Hides the Magic button and resets its values.
|
|
*/
|
|
hideMagicButton() {
|
|
const magicButton = document.getElementById("magic");
|
|
magicButton.classList.add("hidden");
|
|
magicButton.setAttribute("data-recipe", "");
|
|
magicButton.setAttribute("data-original-title", "Magic!");
|
|
}
|
|
|
|
|
|
/**
|
|
* Handler for file slice display events.
|
|
*/
|
|
displayFileSlice() {
|
|
const startTime = new Date().getTime(),
|
|
showFileOverlay = document.getElementById("show-file-overlay"),
|
|
sliceFromEl = document.getElementById("output-file-slice-from"),
|
|
sliceToEl = document.getElementById("output-file-slice-to"),
|
|
sliceFrom = parseInt(sliceFromEl.value, 10),
|
|
sliceTo = parseInt(sliceToEl.value, 10),
|
|
str = Utils.arrayBufferToStr(this.getActive().slice(sliceFrom, sliceTo));
|
|
|
|
document.getElementById("output-text").classList.remove("blur");
|
|
showFileOverlay.style.display = "block";
|
|
|
|
}
|
|
|
|
|
|
/**
|
|
* Handler for copy click events.
|
|
* Copies the output to the clipboard
|
|
*/
|
|
copyClick() {
|
|
const output = this.getActive();
|
|
|
|
// Create invisible textarea to populate with the raw dish string (not the printable version that
|
|
// contains dots instead of the actual bytes)
|
|
const textarea = document.createElement("textarea");
|
|
textarea.style.position = "fixed";
|
|
textarea.style.top = 0;
|
|
textarea.style.left = 0;
|
|
textarea.style.width = 0;
|
|
textarea.style.height = 0;
|
|
textarea.style.border = "none";
|
|
|
|
textarea.value = output;
|
|
document.body.appendChild(textarea);
|
|
|
|
let success = false;
|
|
try {
|
|
textarea.select();
|
|
success = output && document.execCommand("copy");
|
|
} catch (err) {
|
|
success = false;
|
|
}
|
|
|
|
if (success) {
|
|
this.app.alert("Copied raw output successfully.", 2000);
|
|
} else {
|
|
this.app.alert("Sorry, the output could not be copied.", 3000);
|
|
}
|
|
|
|
// Clean up
|
|
document.body.removeChild(textarea);
|
|
}
|
|
}
|
|
|
|
export default OutputWaiter;
|