2018-05-15 19:36:45 +02:00
|
|
|
/**
|
|
|
|
* @author n1474335 [n1474335@gmail.com]
|
2019-04-02 17:58:36 +02:00
|
|
|
* @author j433866 [j433866@gmail.com]
|
2018-05-15 19:36:45 +02:00
|
|
|
* @copyright Crown Copyright 2016
|
|
|
|
* @license Apache-2.0
|
|
|
|
*/
|
|
|
|
|
|
|
|
import Utils from "../core/Utils";
|
|
|
|
import FileSaver from "file-saver";
|
2019-04-03 13:00:47 +02:00
|
|
|
import zip from "zlibjs/bin/zip.min";
|
|
|
|
|
|
|
|
const Zlib = zip.Zlib;
|
2018-05-15 19:36:45 +02:00
|
|
|
|
|
|
|
/**
|
2019-04-02 17:58:36 +02:00
|
|
|
* Waiter to handle events related to the output
|
|
|
|
*/
|
2018-05-15 19:36:45 +02:00
|
|
|
class OutputWaiter {
|
|
|
|
|
|
|
|
/**
|
|
|
|
* OutputWaiter constructor.
|
|
|
|
*
|
|
|
|
* @param {App} app - The main view object for CyberChef.
|
2019-04-02 17:58:36 +02:00
|
|
|
* @param {Manager} manager - The CyberChef event manager
|
2018-05-15 19:36:45 +02:00
|
|
|
*/
|
|
|
|
constructor(app, manager) {
|
|
|
|
this.app = app;
|
|
|
|
this.manager = manager;
|
|
|
|
|
2019-03-27 10:05:10 +01:00
|
|
|
this.outputs = [];
|
2019-04-02 17:58:36 +02:00
|
|
|
this.maxTabs = 4; // Calculate this
|
2018-05-15 19:36:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-04-02 17:58:36 +02:00
|
|
|
* Gets the output for the specified input number
|
2018-05-15 19:36:45 +02:00
|
|
|
*
|
2019-04-02 17:58:36 +02:00
|
|
|
* @param {number} inputNum
|
2019-04-03 13:00:47 +02:00
|
|
|
* @returns {string | ArrayBuffer}
|
2018-05-15 19:36:45 +02:00
|
|
|
*/
|
2019-04-02 17:58:36 +02:00
|
|
|
getOutput(inputNum) {
|
|
|
|
const index = this.getOutputIndex(inputNum);
|
|
|
|
if (index === -1) return -1;
|
2018-05-15 19:36:45 +02:00
|
|
|
|
2019-04-03 13:00:47 +02:00
|
|
|
log.error(this.outputs[index]);
|
|
|
|
if (typeof this.outputs[index].data.dish.value === "string") {
|
|
|
|
return this.outputs[index].data.dish.value;
|
2019-04-02 17:58:36 +02:00
|
|
|
} else {
|
2019-04-03 13:00:47 +02:00
|
|
|
return this.outputs[index].data.dish.value || "";
|
2019-04-02 17:58:36 +02:00
|
|
|
}
|
|
|
|
}
|
2018-05-15 19:36:45 +02:00
|
|
|
|
2019-03-27 10:05:10 +01:00
|
|
|
/**
|
2019-04-02 17:58:36 +02:00
|
|
|
* Gets the index of the output for the specified input number
|
2019-03-27 10:05:10 +01:00
|
|
|
*
|
2019-04-02 17:58:36 +02:00
|
|
|
* @param {number} inputNum
|
|
|
|
* @returns {number}
|
2019-03-27 10:05:10 +01:00
|
|
|
*/
|
2019-04-02 17:58:36 +02:00
|
|
|
getOutputIndex(inputNum) {
|
|
|
|
for (let i = 0; i < this.outputs.length; i++) {
|
|
|
|
if (this.outputs[i].inputNum === inputNum) {
|
|
|
|
return i;
|
2019-03-27 10:05:10 +01:00
|
|
|
}
|
|
|
|
}
|
2019-04-02 17:58:36 +02:00
|
|
|
return -1;
|
2019-03-27 10:05:10 +01:00
|
|
|
}
|
|
|
|
|
2018-05-15 19:36:45 +02:00
|
|
|
/**
|
2019-04-02 17:58:36 +02:00
|
|
|
* Gets the output string or FileBuffer for the active input
|
2018-05-15 19:36:45 +02:00
|
|
|
*
|
2019-04-02 17:58:36 +02:00
|
|
|
* @returns {string | ArrayBuffer}
|
2018-05-15 19:36:45 +02:00
|
|
|
*/
|
2019-04-02 17:58:36 +02:00
|
|
|
getActive() {
|
2019-04-03 13:00:47 +02:00
|
|
|
return this.getOutput(this.getActiveTab());
|
2018-05-15 19:36:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-04-02 17:58:36 +02:00
|
|
|
* Adds a new output to the output array.
|
|
|
|
* Creates a new tab if we have less than maxtabs tabs open
|
2018-05-15 19:36:45 +02:00
|
|
|
*
|
2019-04-02 17:58:36 +02:00
|
|
|
* @param {number} inputNum
|
|
|
|
* @param {boolean} [changeTab=true]
|
2018-05-15 19:36:45 +02:00
|
|
|
*/
|
2019-04-02 17:58:36 +02:00
|
|
|
addOutput(inputNum, changeTab = true) {
|
|
|
|
const index = this.getOutputIndex(inputNum);
|
|
|
|
if (index !== -1) {
|
|
|
|
// Remove the output if it already exists
|
|
|
|
this.outputs.splice(index, 1);
|
|
|
|
}
|
|
|
|
const newOutput = {
|
|
|
|
data: null,
|
|
|
|
inputNum: inputNum,
|
|
|
|
// statusMessage: `Input ${inputNum} has not been baked yet.`,
|
|
|
|
statusMessage: "",
|
|
|
|
error: null,
|
|
|
|
status: "inactive"
|
|
|
|
};
|
|
|
|
|
|
|
|
this.outputs.push(newOutput);
|
|
|
|
|
|
|
|
// add new tab
|
|
|
|
this.addTab(inputNum, changeTab);
|
|
|
|
return this.outputs.indexOf(newOutput);
|
2018-05-15 19:36:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-04-02 17:58:36 +02:00
|
|
|
* 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
|
2018-05-15 19:36:45 +02:00
|
|
|
*/
|
2019-04-02 17:58:36 +02:00
|
|
|
updateOutputValue(data, inputNum) {
|
|
|
|
let index = this.getOutputIndex(inputNum);
|
|
|
|
if (index === -1) {
|
|
|
|
index = this.addOutput(inputNum);
|
|
|
|
}
|
2018-05-15 19:36:45 +02:00
|
|
|
|
2019-04-02 17:58:36 +02:00
|
|
|
this.outputs[index].data = data;
|
2018-05-15 19:36:45 +02:00
|
|
|
|
2019-04-02 17:58:36 +02:00
|
|
|
// set output here
|
|
|
|
this.set(inputNum);
|
2018-05-15 19:36:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-04-02 17:58:36 +02:00
|
|
|
* 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
|
2018-05-15 19:36:45 +02:00
|
|
|
*/
|
2019-04-02 17:58:36 +02:00
|
|
|
updateOutputMessage(statusMessage, inputNum) {
|
|
|
|
const index = this.getOutputIndex(inputNum);
|
|
|
|
if (index === -1) return;
|
2018-05-15 19:36:45 +02:00
|
|
|
|
2019-04-02 17:58:36 +02:00
|
|
|
this.outputs[index].statusMessage = statusMessage;
|
|
|
|
this.set(inputNum);
|
2018-05-15 19:36:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-04-02 17:58:36 +02:00
|
|
|
* 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
|
2018-05-15 19:36:45 +02:00
|
|
|
*
|
2019-04-02 17:58:36 +02:00
|
|
|
* @param {Error} error
|
|
|
|
* @param {number} inputNum
|
2018-05-15 19:36:45 +02:00
|
|
|
*/
|
2019-04-02 17:58:36 +02:00
|
|
|
updateOutputError(error, inputNum) {
|
|
|
|
const index = this.getOutputIndex(inputNum);
|
|
|
|
if (index === -1) return;
|
2018-05-15 19:36:45 +02:00
|
|
|
|
2019-04-02 17:58:36 +02:00
|
|
|
this.outputs[index].error = error;
|
|
|
|
|
|
|
|
// call handle error here
|
|
|
|
// or make the error handling part of set()
|
|
|
|
this.set(inputNum);
|
|
|
|
}
|
2018-05-15 19:36:45 +02:00
|
|
|
|
|
|
|
/**
|
2019-04-02 17:58:36 +02:00
|
|
|
* Updates the status value for the output in the output array
|
2018-05-15 19:36:45 +02:00
|
|
|
*
|
2019-04-02 17:58:36 +02:00
|
|
|
* @param {string} status
|
|
|
|
* @param {number} inputNum
|
2018-05-15 19:36:45 +02:00
|
|
|
*/
|
2019-04-02 17:58:36 +02:00
|
|
|
updateOutputStatus(status, inputNum) {
|
|
|
|
const index = this.getOutputIndex(inputNum);
|
|
|
|
if (index === -1) return;
|
2018-05-15 19:36:45 +02:00
|
|
|
|
2019-04-02 17:58:36 +02:00
|
|
|
this.outputs[index].status = status;
|
2018-05-15 19:36:45 +02:00
|
|
|
|
2019-04-02 17:58:36 +02:00
|
|
|
this.set(inputNum);
|
2018-05-15 19:36:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-04-02 17:58:36 +02:00
|
|
|
* Removes an output from the output array.
|
|
|
|
*
|
|
|
|
* @param {number} inputNum
|
2018-05-15 19:36:45 +02:00
|
|
|
*/
|
2019-04-02 17:58:36 +02:00
|
|
|
removeOutput(inputNum) {
|
|
|
|
const index = this.getOutputIndex(inputNum);
|
|
|
|
if (index === -1) return;
|
2018-05-15 19:36:45 +02:00
|
|
|
|
2019-04-02 17:58:36 +02:00
|
|
|
this.outputs.splice(index, 1);
|
|
|
|
}
|
2018-05-15 19:36:45 +02:00
|
|
|
|
|
|
|
/**
|
2019-04-02 17:58:36 +02:00
|
|
|
* Sets the output in the output textarea.
|
|
|
|
*
|
|
|
|
* @param {number} inputNum
|
2018-05-15 19:36:45 +02:00
|
|
|
*/
|
2019-04-02 17:58:36 +02:00
|
|
|
set(inputNum) {
|
|
|
|
const outputIndex = this.getOutputIndex(inputNum);
|
|
|
|
if (outputIndex === -1) return;
|
|
|
|
const output = this.outputs[outputIndex];
|
|
|
|
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") {
|
|
|
|
// 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";
|
2018-05-15 19:36:45 +02:00
|
|
|
|
2019-04-02 17:58:36 +02:00
|
|
|
outputText.value = "";
|
|
|
|
outputHtml.innerHTML = "";
|
2018-05-15 19:36:45 +02:00
|
|
|
|
2019-04-02 17:58:36 +02:00
|
|
|
}
|
|
|
|
} 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);
|
2018-05-15 19:36:45 +02:00
|
|
|
|
2019-04-02 17:58:36 +02:00
|
|
|
document.querySelector("#output-loader .loading-msg").textContent = output.statusMessage;
|
|
|
|
}
|
2018-05-15 19:36:45 +02:00
|
|
|
|
2019-04-02 17:58:36 +02:00
|
|
|
} 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;
|
|
|
|
|
|
|
|
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);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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.byteLength;
|
|
|
|
|
|
|
|
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;
|
2018-05-15 19:36:45 +02:00
|
|
|
}
|
2019-04-02 17:58:36 +02:00
|
|
|
}
|
2018-05-15 19:36:45 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-04-02 17:58:36 +02:00
|
|
|
* Shows file details
|
|
|
|
*
|
|
|
|
* @param {ArrayBuffer} buf
|
2018-05-15 19:36:45 +02:00
|
|
|
*/
|
2019-04-02 17:58:36 +02:00
|
|
|
setFile(buf) {
|
|
|
|
const file = new File([buf], "output.dat");
|
2018-05-15 19:36:45 +02:00
|
|
|
|
2019-04-02 17:58:36 +02:00
|
|
|
// 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);
|
2018-05-15 19:36:45 +02:00
|
|
|
|
2019-04-02 17:58:36 +02:00
|
|
|
fileOverlay.style.display = "block";
|
|
|
|
fileSize.textContent = file.size.toLocaleString() + " bytes";
|
2018-05-15 19:36:45 +02:00
|
|
|
|
2019-04-02 17:58:36 +02:00
|
|
|
outputText.classList.add("blur");
|
|
|
|
outputText.value = Utils.printable(Utils.arrayBufferToStr(fileSlice));
|
2018-05-15 19:36:45 +02:00
|
|
|
}
|
|
|
|
|
2019-04-02 17:58:36 +02:00
|
|
|
/**
|
|
|
|
* Clears output file details
|
|
|
|
*/
|
|
|
|
closeFile() {
|
|
|
|
document.getElementById("output-file").style.display = "none";
|
|
|
|
document.getElementById("output-text").classList.remove("blur");
|
|
|
|
}
|
2018-05-15 19:36:45 +02:00
|
|
|
|
|
|
|
/**
|
2019-01-16 13:29:34 +01:00
|
|
|
* Save bombe object then remove it from the DOM so that it does not cause performance issues.
|
2019-01-15 20:03:17 +01:00
|
|
|
*/
|
|
|
|
saveBombe() {
|
2019-01-16 13:29:34 +01:00
|
|
|
this.bombeEl = document.getElementById("bombe");
|
|
|
|
this.bombeEl.parentNode.removeChild(this.bombeEl);
|
2019-01-15 20:03:17 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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.
|
2018-05-15 19:36:45 +02:00
|
|
|
*
|
2019-01-15 20:03:17 +01:00
|
|
|
* @param {boolean} value - true == show loader
|
2018-05-15 19:36:45 +02:00
|
|
|
*/
|
|
|
|
toggleLoader(value) {
|
2019-01-15 20:03:17 +01:00
|
|
|
clearTimeout(this.appendBombeTimeout);
|
|
|
|
clearTimeout(this.outputLoaderTimeout);
|
|
|
|
|
2018-05-15 19:36:45 +02:00
|
|
|
const outputLoader = document.getElementById("output-loader"),
|
2019-01-15 20:03:17 +01:00
|
|
|
outputElement = document.getElementById("output-text"),
|
2019-01-16 13:29:34 +01:00
|
|
|
animation = document.getElementById("output-loader-animation");
|
2018-05-15 19:36:45 +02:00
|
|
|
|
|
|
|
if (value) {
|
|
|
|
this.manager.controls.hideStaleIndicator();
|
2019-01-15 20:03:17 +01:00
|
|
|
|
|
|
|
// 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() {
|
2019-01-16 13:29:34 +01:00
|
|
|
animation.appendChild(this.bombeEl);
|
2019-01-15 20:03:17 +01:00
|
|
|
}.bind(this), 150);
|
|
|
|
|
|
|
|
// Show the loading screen
|
|
|
|
this.outputLoaderTimeout = setTimeout(function() {
|
2018-05-15 19:36:45 +02:00
|
|
|
outputElement.disabled = true;
|
|
|
|
outputLoader.style.visibility = "visible";
|
|
|
|
outputLoader.style.opacity = 1;
|
|
|
|
}.bind(this), 200);
|
|
|
|
} else {
|
2019-01-15 20:03:17 +01:00
|
|
|
// Remove the Bombe from the DOM to save resources
|
|
|
|
this.outputLoaderTimeout = setTimeout(function () {
|
|
|
|
try {
|
2019-01-16 13:29:34 +01:00
|
|
|
animation.removeChild(this.bombeEl);
|
2019-01-15 20:03:17 +01:00
|
|
|
} catch (err) {}
|
|
|
|
}.bind(this), 500);
|
2018-05-15 19:36:45 +02:00
|
|
|
outputElement.disabled = false;
|
|
|
|
outputLoader.style.opacity = 0;
|
|
|
|
outputLoader.style.visibility = "hidden";
|
2019-04-02 17:58:36 +02:00
|
|
|
// this.setStatusMsg("");
|
2018-05-15 19:36:45 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-03 13:00:47 +02:00
|
|
|
/**
|
|
|
|
* Handler for save click events.
|
|
|
|
* Saves the current output to a file.
|
|
|
|
*/
|
|
|
|
saveClick() {
|
|
|
|
this.downloadFile(this.getActiveTab());
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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);
|
|
|
|
}
|
|
|
|
|
2018-05-15 19:36:45 +02:00
|
|
|
/**
|
2019-04-02 17:58:36 +02:00
|
|
|
* Adds a new output tab.
|
2018-05-15 19:36:45 +02:00
|
|
|
*
|
2019-04-02 17:58:36 +02:00
|
|
|
* @param {number} inputNum
|
|
|
|
* @param {boolean} [changeTab=true]
|
2018-05-15 19:36:45 +02:00
|
|
|
*/
|
2019-04-02 17:58:36 +02:00
|
|
|
addTab(inputNum, changeTab = true) {
|
|
|
|
const tabsWrapper = document.getElementById("output-tabs");
|
|
|
|
const numTabs = tabsWrapper.children.length;
|
2018-05-15 19:36:45 +02:00
|
|
|
|
2019-04-02 17:58:36 +02:00
|
|
|
if (numTabs < this.maxTabs) {
|
|
|
|
// Create a new tab element
|
|
|
|
const newTab = this.createTabElement(inputNum);
|
2018-05-15 19:36:45 +02:00
|
|
|
|
2019-04-02 17:58:36 +02:00
|
|
|
tabsWrapper.appendChild(newTab);
|
2018-05-15 19:36:45 +02:00
|
|
|
|
2019-04-02 17:58:36 +02:00
|
|
|
if (numTabs > 0) {
|
|
|
|
tabsWrapper.parentElement.style.display = "block";
|
2019-04-03 13:00:47 +02:00
|
|
|
|
|
|
|
const tabButtons = document.getElementsByClassName("output-tab-buttons");
|
|
|
|
for (let i = 0; i < tabButtons.length; i++) {
|
|
|
|
tabButtons.item(i).style.display = "inline-block";
|
|
|
|
}
|
2018-05-15 19:36:45 +02:00
|
|
|
|
2019-04-02 17:58:36 +02:00
|
|
|
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))";
|
|
|
|
}
|
|
|
|
}
|
2018-05-15 19:36:45 +02:00
|
|
|
|
2019-04-02 17:58:36 +02:00
|
|
|
if (changeTab) {
|
2019-04-03 13:00:47 +02:00
|
|
|
this.changeTab(inputNum, false);
|
2019-04-02 17:58:36 +02:00
|
|
|
}
|
2018-05-15 19:36:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-04-02 17:58:36 +02:00
|
|
|
* Changes the active tab
|
2018-05-15 19:36:45 +02:00
|
|
|
*
|
2019-04-02 17:58:36 +02:00
|
|
|
* @param {number} inputNum
|
2019-04-03 13:00:47 +02:00
|
|
|
* @param {boolean} [changeInput = false]
|
2018-05-15 19:36:45 +02:00
|
|
|
*/
|
2019-04-03 13:00:47 +02:00
|
|
|
changeTab(inputNum, changeInput = false) {
|
2019-04-02 17:58:36 +02:00
|
|
|
const currentNum = this.getActiveTab();
|
|
|
|
if (this.getOutputIndex(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");
|
|
|
|
found = true;
|
|
|
|
} else {
|
|
|
|
tabs.item(i).classList.remove("active-output-tab");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (!found) {
|
|
|
|
let direction = "right";
|
|
|
|
if (currentNum > inputNum) {
|
|
|
|
direction = "left";
|
|
|
|
}
|
2018-05-15 19:36:45 +02:00
|
|
|
|
2019-04-02 17:58:36 +02:00
|
|
|
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) {
|
2019-04-03 13:00:47 +02:00
|
|
|
tabs.item(i).classList.add("active-output-tab");
|
2019-04-02 17:58:36 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2018-05-15 19:36:45 +02:00
|
|
|
|
2019-04-02 17:58:36 +02:00
|
|
|
this.set(inputNum);
|
2019-04-03 13:00:47 +02:00
|
|
|
|
|
|
|
if (changeInput) {
|
|
|
|
this.manager.input.changeTab(inputNum, false);
|
|
|
|
}
|
2019-04-02 17:58:36 +02:00
|
|
|
}
|
2018-06-03 18:33:13 +02:00
|
|
|
|
|
|
|
/**
|
2019-04-02 17:58:36 +02:00
|
|
|
* Handler for changing tabs event
|
|
|
|
*
|
|
|
|
* @param {event} mouseEvent
|
2018-06-03 18:33:13 +02:00
|
|
|
*/
|
2019-04-02 17:58:36 +02:00
|
|
|
changeTabClick(mouseEvent) {
|
|
|
|
if (!mouseEvent.srcElement) return;
|
|
|
|
const tabNum = mouseEvent.srcElement.parentElement.getAttribute("inputNum");
|
|
|
|
if (tabNum) {
|
2019-04-03 13:00:47 +02:00
|
|
|
this.changeTab(parseInt(tabNum, 10), this.app.options.syncTabs);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handler for changing to the left tab
|
|
|
|
*/
|
|
|
|
changeTabLeft() {
|
|
|
|
const currentTab = this.getActiveTab();
|
|
|
|
const currentOutput = this.getOutputIndex(currentTab);
|
|
|
|
if (currentOutput > 0) {
|
|
|
|
this.changeTab(this.getPreviousInputNum(currentTab), this.app.options.syncTabs);
|
|
|
|
} else {
|
|
|
|
this.changeTab(this.getSmallestInputNum(), 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)) {
|
|
|
|
this.changeTab(tabNum, this.app.options.syncTabs);
|
2018-06-03 18:33:13 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-04-02 17:58:36 +02:00
|
|
|
* Generates a list of the nearby inputNums
|
2018-06-03 18:33:13 +02:00
|
|
|
*
|
2019-04-02 17:58:36 +02:00
|
|
|
* @param {number} inputNum
|
|
|
|
* @param {string} direction
|
2018-06-03 18:33:13 +02:00
|
|
|
*/
|
2019-04-02 17:58:36 +02:00
|
|
|
getNearbyNums(inputNum, direction) {
|
|
|
|
const nums = [];
|
|
|
|
if (direction === "left") {
|
|
|
|
let reachedEnd = false;
|
|
|
|
for (let i = 0; i < this.maxTabs; i++) {
|
|
|
|
let newNum;
|
|
|
|
if (i === 0) {
|
|
|
|
newNum = inputNum;
|
|
|
|
} else {
|
|
|
|
newNum = this.getNextInputNum(nums[i-1]);
|
|
|
|
}
|
|
|
|
if (newNum === nums[i-1]) {
|
|
|
|
reachedEnd = true;
|
|
|
|
nums.sort(function(a, b) {
|
|
|
|
return b - a;
|
|
|
|
});
|
2019-04-03 17:05:10 +02:00
|
|
|
break;
|
2019-04-02 17:58:36 +02:00
|
|
|
}
|
|
|
|
if (reachedEnd) {
|
|
|
|
newNum = this.getPreviousInputNum(nums[i-1]);
|
|
|
|
}
|
|
|
|
if (newNum >= 0) {
|
|
|
|
nums.push(newNum);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
let reachedEnd = false;
|
|
|
|
for (let i = 0; i < this.maxTabs; i++) {
|
|
|
|
let newNum;
|
|
|
|
if (i === 0) {
|
|
|
|
newNum = inputNum;
|
|
|
|
} else {
|
|
|
|
if (!reachedEnd) {
|
|
|
|
newNum = this.getPreviousInputNum(nums[i-1]);
|
|
|
|
}
|
|
|
|
if (newNum === nums[i-1]) {
|
|
|
|
reachedEnd = true;
|
|
|
|
nums.sort(function(a, b) {
|
|
|
|
return b - a;
|
|
|
|
});
|
2019-04-03 17:05:10 +02:00
|
|
|
break;
|
2019-04-02 17:58:36 +02:00
|
|
|
}
|
|
|
|
if (reachedEnd) {
|
|
|
|
newNum = this.getNextInputNum(nums[i-1]);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (newNum >= 0) {
|
|
|
|
nums.push(newNum);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
nums.sort(function(a, b) {
|
|
|
|
return a - b;
|
|
|
|
});
|
|
|
|
return nums;
|
2018-07-27 15:37:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-04-02 17:58:36 +02:00
|
|
|
* Gets the largest inputNum
|
2018-07-27 15:37:38 +02:00
|
|
|
*
|
2019-04-02 17:58:36 +02:00
|
|
|
* @returns {number}
|
2018-07-27 15:37:38 +02:00
|
|
|
*/
|
2019-04-02 17:58:36 +02:00
|
|
|
getLargestInputNum() {
|
|
|
|
let largest = 0;
|
|
|
|
for (let i = 0; i < this.outputs.length; i++) {
|
|
|
|
if (this.outputs[i].inputNum > largest) {
|
|
|
|
largest = this.outputs[i].inputNum;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return largest;
|
2018-07-27 15:37:38 +02:00
|
|
|
}
|
|
|
|
|
2019-04-03 13:00:47 +02:00
|
|
|
/**
|
|
|
|
* Gets the smallest inputNum
|
|
|
|
*
|
|
|
|
* @returns {number}
|
|
|
|
*/
|
|
|
|
getSmallestInputNum() {
|
|
|
|
let smallest = this.getLargestInputNum();
|
|
|
|
for (let i = 0; i < this.outputs.length; i++) {
|
|
|
|
if (this.outputs[i].inputNum < smallest) {
|
|
|
|
smallest = this.outputs[i].inputNum;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return smallest;
|
|
|
|
}
|
|
|
|
|
2018-07-27 15:37:38 +02:00
|
|
|
/**
|
2019-04-02 17:58:36 +02:00
|
|
|
* Gets the previous inputNum
|
2018-07-27 15:37:38 +02:00
|
|
|
*
|
2019-04-02 17:58:36 +02:00
|
|
|
* @param {number} inputNum - The current input number
|
|
|
|
* @returns {number}
|
2018-07-27 15:37:38 +02:00
|
|
|
*/
|
2019-04-02 17:58:36 +02:00
|
|
|
getPreviousInputNum(inputNum) {
|
2019-04-03 13:00:47 +02:00
|
|
|
let num = this.getSmallestInputNum();
|
2019-04-02 17:58:36 +02:00
|
|
|
for (let i = 0; i < this.outputs.length; i++) {
|
|
|
|
if (this.outputs[i].inputNum < inputNum) {
|
|
|
|
if (this.outputs[i].inputNum > num) {
|
|
|
|
num = this.outputs[i].inputNum;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return num;
|
2018-07-27 15:37:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-04-02 17:58:36 +02:00
|
|
|
* Gets the next inputNum
|
|
|
|
*
|
|
|
|
* @param {number} inputNum - The current input number
|
|
|
|
* @returns {number}
|
2018-07-27 15:37:38 +02:00
|
|
|
*/
|
2019-04-02 17:58:36 +02:00
|
|
|
getNextInputNum(inputNum) {
|
|
|
|
let num = this.getLargestInputNum();
|
|
|
|
for (let i = 0; i < this.outputs.length; i++) {
|
|
|
|
if (this.outputs[i].inputNum > inputNum) {
|
|
|
|
if (this.outputs[i].inputNum < num) {
|
|
|
|
num = this.outputs[i].inputNum;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return num;
|
2018-06-03 18:33:13 +02:00
|
|
|
}
|
|
|
|
|
2019-03-09 07:25:27 +01:00
|
|
|
/**
|
2019-04-02 17:58:36 +02:00
|
|
|
* Removes a tab and it's corresponding output
|
2019-03-09 07:25:27 +01:00
|
|
|
*
|
2019-04-02 17:58:36 +02:00
|
|
|
* @param {number} inputNum
|
2019-03-09 07:25:27 +01:00
|
|
|
*/
|
2019-04-02 17:58:36 +02:00
|
|
|
removeTab(inputNum) {
|
2019-04-03 13:00:47 +02:00
|
|
|
let activeTab = this.getActiveTab();
|
2019-04-02 17:58:36 +02:00
|
|
|
if (this.getOutputIndex(inputNum) === -1) return;
|
2019-03-09 07:25:27 +01:00
|
|
|
|
2019-04-02 17:58:36 +02:00
|
|
|
const tabElement = this.getTabItem(inputNum);
|
2019-03-09 07:25:27 +01:00
|
|
|
|
2019-04-02 17:58:36 +02:00
|
|
|
this.removeOutput(inputNum);
|
|
|
|
|
|
|
|
if (tabElement !== null) {
|
|
|
|
// find new tab number?
|
2019-04-03 13:00:47 +02:00
|
|
|
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";
|
|
|
|
|
|
|
|
const tabButtons = document.getElementsByClassName("output-tab-buttons");
|
|
|
|
for (let i = 0; i < tabButtons.length; i++) {
|
|
|
|
tabButtons.item(i).style.display = "inline-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))";
|
|
|
|
|
|
|
|
} else {
|
|
|
|
tabsList.parentElement.style.display = "none";
|
|
|
|
|
|
|
|
const tabButtons = document.getElementsByClassName("output-tab-buttons");
|
|
|
|
for (let i = 0; i < tabButtons.length; i++) {
|
|
|
|
tabButtons.item(i).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))";
|
2019-04-02 17:58:36 +02:00
|
|
|
}
|
2019-04-03 13:00:47 +02:00
|
|
|
|
|
|
|
this.changeTab(activeTab);
|
|
|
|
|
2019-03-09 07:25:27 +01:00
|
|
|
}
|
|
|
|
|
2019-03-27 10:05:10 +01:00
|
|
|
/**
|
2019-04-02 17:58:36 +02:00
|
|
|
* Creates a new tab element to be added to the tab bar
|
2019-03-27 10:05:10 +01:00
|
|
|
*
|
2019-04-02 17:58:36 +02:00
|
|
|
* @param {number} inputNum
|
2019-03-27 10:05:10 +01:00
|
|
|
*/
|
2019-04-02 17:58:36 +02:00
|
|
|
createTabElement(inputNum) {
|
2019-03-27 10:05:10 +01:00
|
|
|
const newTab = document.createElement("li");
|
2019-04-02 17:58:36 +02:00
|
|
|
newTab.setAttribute("inputNum", inputNum.toString());
|
2019-03-27 10:05:10 +01:00
|
|
|
|
|
|
|
const newTabContent = document.createElement("div");
|
|
|
|
newTabContent.classList.add("output-tab-content");
|
2019-04-02 17:58:36 +02:00
|
|
|
newTabContent.innerText = `Tab ${inputNum.toString()}`;
|
2019-03-27 10:05:10 +01:00
|
|
|
|
2019-04-02 17:58:36 +02:00
|
|
|
// Do we want remove tab button on output?
|
2019-03-27 10:05:10 +01:00
|
|
|
newTab.appendChild(newTabContent);
|
2019-04-02 17:58:36 +02:00
|
|
|
|
|
|
|
return newTab;
|
2019-03-27 10:05:10 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-04-02 17:58:36 +02:00
|
|
|
* Gets the number of the current active tab
|
2019-03-27 10:05:10 +01:00
|
|
|
*
|
2019-04-02 17:58:36 +02:00
|
|
|
* @returns {number}
|
2019-03-27 10:05:10 +01:00
|
|
|
*/
|
2019-04-02 17:58:36 +02:00
|
|
|
getActiveTab() {
|
2019-03-27 14:48:54 +01:00
|
|
|
const activeTabs = document.getElementsByClassName("active-output-tab");
|
2019-04-02 17:58:36 +02:00
|
|
|
if (activeTabs.length > 0) {
|
|
|
|
const activeTab = activeTabs.item(0);
|
|
|
|
const tabNum = activeTab.getAttribute("inputNum");
|
|
|
|
return parseInt(tabNum, 10);
|
2019-03-27 14:48:54 +01:00
|
|
|
}
|
2019-04-02 17:58:36 +02:00
|
|
|
return -1;
|
2019-03-27 14:48:54 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-04-02 17:58:36 +02:00
|
|
|
* Gets the li element for a tab
|
2019-03-27 14:48:54 +01:00
|
|
|
*
|
2019-04-02 17:58:36 +02:00
|
|
|
* @param {number} inputNum
|
2019-03-27 14:48:54 +01:00
|
|
|
*/
|
2019-04-02 17:58:36 +02:00
|
|
|
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);
|
|
|
|
}
|
2019-03-27 14:48:54 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-04-02 17:58:36 +02:00
|
|
|
* Display output information in the tab header
|
2019-03-27 14:48:54 +01:00
|
|
|
*
|
2019-04-02 17:58:36 +02:00
|
|
|
* @param {number} inputNum
|
2019-03-27 14:48:54 +01:00
|
|
|
*/
|
2019-04-02 17:58:36 +02:00
|
|
|
displayTabInfo(inputNum) {
|
|
|
|
const tabItem = this.getTabItem(inputNum);
|
2019-03-27 14:48:54 +01:00
|
|
|
|
2019-04-02 17:58:36 +02:00
|
|
|
if (!tabItem) return;
|
2019-03-27 14:48:54 +01:00
|
|
|
|
2019-04-02 17:58:36 +02:00
|
|
|
const tabContent = tabItem.firstElementChild;
|
2019-03-27 14:48:54 +01:00
|
|
|
|
2019-04-02 17:58:36 +02:00
|
|
|
tabContent.innerText = `Tab ${inputNum}`;
|
2019-03-27 14:48:54 +01:00
|
|
|
|
2019-03-27 10:05:10 +01:00
|
|
|
}
|
2018-05-15 19:36:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
export default OutputWaiter;
|