mirror of
https://github.com/gchq/CyberChef.git
synced 2024-11-16 17:08:31 +01:00
6d9a14feed
Autobake when all inputs have loaded. Improve load experience.
1312 lines
39 KiB
JavaScript
1312 lines
39 KiB
JavaScript
/**
|
|
* @author n1474335 [n1474335@gmail.com]
|
|
* @author j433866 [j433866@gmail.com]
|
|
* @copyright Crown Copyright 2019
|
|
* @license Apache-2.0
|
|
*/
|
|
|
|
import LoaderWorker from "worker-loader?inline&fallback=false!./LoaderWorker";
|
|
import InputWorker from "worker-loader?inline&fallback=false!./InputWorker";
|
|
import Utils from "../core/Utils";
|
|
import { toBase64 } from "../core/lib/Base64";
|
|
import { isImage } from "../core/lib/FileType";
|
|
|
|
|
|
/**
|
|
* Waiter to handle events related to the input.
|
|
*/
|
|
class InputWaiter {
|
|
|
|
/**
|
|
* InputWaiter 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;
|
|
|
|
// Define keys that don't change the input so we don't have to autobake when they are pressed
|
|
this.badKeys = [
|
|
16, //Shift
|
|
17, //Ctrl
|
|
18, //Alt
|
|
19, //Pause
|
|
20, //Caps
|
|
27, //Esc
|
|
33, 34, 35, 36, //PgUp, PgDn, End, Home
|
|
37, 38, 39, 40, //Directional
|
|
44, //PrntScrn
|
|
91, 92, //Win
|
|
93, //Context
|
|
112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, //F1-12
|
|
144, //Num
|
|
145, //Scroll
|
|
];
|
|
|
|
this.inputWorker = null;
|
|
this.loaderWorkers = [];
|
|
this.workerId = 0;
|
|
this.maxWorkers = navigator.hardwareConcurrency || 4;
|
|
this.maxTabs = 4;
|
|
}
|
|
|
|
/**
|
|
* Calculates the maximum number of tabs to display
|
|
*/
|
|
calcMaxTabs() {
|
|
const numTabs = Math.floor((document.getElementById("IO").offsetWidth - 75) / 120);
|
|
this.maxTabs = (numTabs > 1) ? numTabs : 2;
|
|
if (this.inputWorker) {
|
|
this.inputWorker.postMessage({
|
|
action: "updateMaxTabs",
|
|
data: this.maxTabs
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Terminates any existing workers and sets up a new InputWorker and LoaderWorker
|
|
*/
|
|
setupInputWorker() {
|
|
if (this.inputWorker !== null) {
|
|
this.inputWorker.terminate();
|
|
this.inputWorker = null;
|
|
}
|
|
|
|
for (let i = this.loaderWorkers.length - 1; i >= 0; i--) {
|
|
this.removeLoaderWorker(this.loaderWorkers[i]);
|
|
}
|
|
|
|
log.debug("Adding new InputWorker");
|
|
this.inputWorker = new InputWorker();
|
|
this.inputWorker.postMessage({
|
|
action: "updateMaxWorkers",
|
|
data: this.maxWorkers
|
|
});
|
|
this.inputWorker.postMessage({
|
|
action: "updateMaxTabs",
|
|
data: this.maxTabs
|
|
});
|
|
this.inputWorker.postMessage({
|
|
action: "setLogLevel",
|
|
data: log.getLevel()
|
|
});
|
|
this.inputWorker.addEventListener("message", this.handleInputWorkerMessage.bind(this));
|
|
|
|
|
|
}
|
|
|
|
/**
|
|
* Activates a loaderWorker and sends it to the InputWorker
|
|
*/
|
|
activateLoaderWorker() {
|
|
const workerIdx = this.addLoaderWorker();
|
|
if (workerIdx === -1) return;
|
|
|
|
const workerObj = this.loaderWorkers[workerIdx];
|
|
this.inputWorker.postMessage({
|
|
action: "loaderWorkerReady",
|
|
data: {
|
|
id: workerObj.id
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Adds a new loaderWorker
|
|
*
|
|
* @returns {number} The index of the created worker
|
|
*/
|
|
addLoaderWorker() {
|
|
if (this.loaderWorkers.length === this.maxWorkers) {
|
|
return -1;
|
|
}
|
|
log.debug("Adding new LoaderWorker.");
|
|
const newWorker = new LoaderWorker();
|
|
const workerId = this.workerId++;
|
|
newWorker.addEventListener("message", this.handleLoaderMessage.bind(this));
|
|
newWorker.postMessage({id: workerId});
|
|
const newWorkerObj = {
|
|
worker: newWorker,
|
|
id: workerId
|
|
};
|
|
this.loaderWorkers.push(newWorkerObj);
|
|
return this.loaderWorkers.indexOf(newWorkerObj);
|
|
}
|
|
|
|
/**
|
|
* Removes a loaderworker
|
|
*
|
|
* @param {Object} workerObj
|
|
*/
|
|
removeLoaderWorker(workerObj) {
|
|
const idx = this.loaderWorkers.indexOf(workerObj);
|
|
if (idx === -1) {
|
|
return;
|
|
}
|
|
log.debug(`Terminating worker ${this.loaderWorkers[idx].id}`);
|
|
this.loaderWorkers[idx].worker.terminate();
|
|
this.loaderWorkers.splice(idx, 1);
|
|
}
|
|
|
|
/**
|
|
* Finds and returns the object for the loaderWorker of a given id
|
|
*
|
|
* @param {number} id
|
|
*/
|
|
getLoaderWorker(id) {
|
|
const idx = this.getLoaderWorkerIndex(id);
|
|
if (idx === -1) return;
|
|
return this.loaderWorkers[idx];
|
|
}
|
|
|
|
/**
|
|
* Gets the index for the loaderWorker of a given id
|
|
*
|
|
* @param {number} id
|
|
*/
|
|
getLoaderWorkerIndex(id) {
|
|
for (let i = 0; i < this.loaderWorkers.length; i++) {
|
|
if (this.loaderWorkers[i].id === id) {
|
|
return i;
|
|
}
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Sends an input to be loaded to the loaderWorker
|
|
*
|
|
* @param {object} inputData
|
|
* @param {File} inputData.file
|
|
* @param {number} inputData.inputNum
|
|
* @param {number} inputData.workerId
|
|
*/
|
|
loadInput(inputData) {
|
|
const idx = this.getLoaderWorkerIndex(inputData.workerId);
|
|
if (idx === -1) return;
|
|
this.loaderWorkers[idx].worker.postMessage({
|
|
file: inputData.file,
|
|
inputNum: inputData.inputNum
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handler for messages sent back by the loaderWorker
|
|
*
|
|
* @param {MessageEvent} e
|
|
*/
|
|
handleLoaderMessage(e) {
|
|
const r = e.data;
|
|
|
|
if (r.hasOwnProperty("fileBuffer")) {
|
|
this.inputWorker.postMessage({
|
|
action: "loaderWorkerMessage",
|
|
data: r
|
|
}, [r.fileBuffer]);
|
|
} else {
|
|
this.inputWorker.postMessage({
|
|
action: "loaderWorkerMessage",
|
|
data: r
|
|
});
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Handler for messages sent back by the inputWorker
|
|
*
|
|
* @param {MessageEvent} e
|
|
*/
|
|
handleInputWorkerMessage(e) {
|
|
const r = e.data;
|
|
|
|
if (!r.hasOwnProperty("action")) {
|
|
log.error("No action");
|
|
return;
|
|
}
|
|
|
|
log.debug(`Receiving ${r.action} from InputWorker.`);
|
|
|
|
switch (r.action) {
|
|
case "activateLoaderWorker":
|
|
this.activateLoaderWorker();
|
|
break;
|
|
case "loadInput":
|
|
this.loadInput(r.data);
|
|
break;
|
|
case "terminateLoaderWorker":
|
|
this.removeLoaderWorker(this.getLoaderWorker(r.data));
|
|
break;
|
|
case "refreshTabs":
|
|
this.refreshTabs(r.data.nums, r.data.activeTab);
|
|
break;
|
|
case "changeTab":
|
|
this.changeTab(r.data, this.app.options.syncTabs);
|
|
break;
|
|
case "updateTabHeader":
|
|
this.updateTabHeader(r.data);
|
|
break;
|
|
case "updateFileProgress":
|
|
this.updateFileProgress(r.data.inputNum, r.data.progress);
|
|
break;
|
|
case "loadingInfo":
|
|
this.showLoadingInfo(r.data, true);
|
|
break;
|
|
case "setInput":
|
|
this.set(r.data.inputObj, r.data.silent);
|
|
break;
|
|
case "inputAdded":
|
|
this.inputAdded(r.data.changeTab, r.data.inputNum);
|
|
break;
|
|
case "addInputs":
|
|
this.addInputs(r.data);
|
|
break;
|
|
case "queueInput":
|
|
this.manager.worker.queueInput(r.data);
|
|
break;
|
|
case "bake":
|
|
this.app.bake(false);
|
|
break;
|
|
case "displayTabSearchResults":
|
|
this.displayTabSearchResults(r.data);
|
|
break;
|
|
case "setUrl":
|
|
this.setUrl(r.data);
|
|
break;
|
|
case "inputSwitch":
|
|
this.manager.output.inputSwitch(r.data);
|
|
break;
|
|
default:
|
|
log.error(`Unknown action ${r.action}.`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Gets the input for all tabs
|
|
*/
|
|
getAll() {
|
|
this.manager.controls.toggleBakeButtonFunction(false, true);
|
|
this.inputWorker.postMessage({
|
|
action: "getAll"
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Sets the input in the input area
|
|
*
|
|
* @param {object} inputData
|
|
* @param {number} inputData.inputNum
|
|
* @param {string | object} inputData.input
|
|
* @param {string} inputData.name
|
|
* @param {string} inputData.size
|
|
* @param {string} inputData.type
|
|
* @param {number} inputData.progress
|
|
* @param {boolean} [silent=false]
|
|
*/
|
|
async set(inputData, silent=false) {
|
|
return new Promise(function(resolve, reject) {
|
|
const activeTab = this.getActiveTab();
|
|
if (inputData.inputNum !== activeTab) return;
|
|
|
|
const inputText = document.getElementById("input-text");
|
|
|
|
if (typeof inputData.input === "string") {
|
|
inputText.value = inputData.input;
|
|
// close file
|
|
const fileOverlay = document.getElementById("input-file"),
|
|
fileName = document.getElementById("input-file-name"),
|
|
fileSize = document.getElementById("input-file-size"),
|
|
fileType = document.getElementById("input-file-type"),
|
|
fileLoaded = document.getElementById("input-file-loaded");
|
|
|
|
fileOverlay.style.display = "none";
|
|
fileName.textContent = "";
|
|
fileSize.textContent = "";
|
|
fileType.textContent = "";
|
|
fileLoaded.textContent = "";
|
|
|
|
inputText.style.overflow = "auto";
|
|
inputText.classList.remove("blur");
|
|
|
|
const lines = inputData.input.length < (this.app.options.ioDisplayThreshold * 1024) ?
|
|
inputData.input.count("\n") + 1 : null;
|
|
this.setInputInfo(inputData.input.length, lines);
|
|
} else {
|
|
this.setFile(inputData);
|
|
}
|
|
|
|
if (!silent) window.dispatchEvent(this.manager.statechange);
|
|
}.bind(this));
|
|
}
|
|
|
|
/**
|
|
* Shows file details
|
|
*
|
|
* @param inputData
|
|
*/
|
|
setFile(inputData) {
|
|
const activeTab = this.getActiveTab();
|
|
if (inputData.inputNum !== activeTab) return;
|
|
|
|
const fileOverlay = document.getElementById("input-file"),
|
|
fileName = document.getElementById("input-file-name"),
|
|
fileSize = document.getElementById("input-file-size"),
|
|
fileType = document.getElementById("input-file-type"),
|
|
fileLoaded = document.getElementById("input-file-loaded");
|
|
|
|
fileOverlay.style.display = "block";
|
|
fileName.textContent = inputData.name;
|
|
fileSize.textContent = inputData.size + " bytes";
|
|
fileType.textContent = inputData.type;
|
|
if (inputData.status === "error") {
|
|
fileLoaded.textContent = "Error";
|
|
fileLoaded.style.color = "#FF0000";
|
|
} else {
|
|
fileLoaded.style.color = "";
|
|
fileLoaded.textContent = inputData.progress + "%";
|
|
}
|
|
|
|
this.displayFilePreview(inputData);
|
|
}
|
|
|
|
|
|
/**
|
|
* Reset the input thumbnail to the default icon
|
|
*/
|
|
resetFileThumb() {
|
|
const fileThumb = document.getElementById("input-file-thumbnail");
|
|
fileThumb.src = require("./static/images/file-128x128.png");
|
|
}
|
|
|
|
/**
|
|
* Shows a chunk of the file in the input behind the file overlay
|
|
*
|
|
* @param {Object} inputData
|
|
* @param {number} inputData.inputNum
|
|
* @param {ArrayBuffer} inputData.input
|
|
*/
|
|
displayFilePreview(inputData) {
|
|
const activeTab = this.getActiveTab(),
|
|
input = inputData.input,
|
|
inputText = document.getElementById("input-text"),
|
|
fileThumb = document.getElementById("input-file-thumbnail");
|
|
if (inputData.inputNum !== activeTab) return;
|
|
inputText.style.overflow = "hidden";
|
|
inputText.classList.add("blur");
|
|
inputText.value = Utils.printable(Utils.arrayBufferToStr(input.slice(0, 4096)));
|
|
|
|
// Display image in thumbnail if we want
|
|
if (this.app.options.imagePreview) {
|
|
const inputArr = new Uint8Array(input),
|
|
type = isImage(inputArr);
|
|
if (type && type !== "image/tiff" && inputArr.byteLength <= 512000) {
|
|
const blob = new Blob([inputArr], {type: type}),
|
|
url = URL.createObjectURL(blob);
|
|
fileThumb.src = url;
|
|
} else {
|
|
this.resetFileThumb();
|
|
}
|
|
} else {
|
|
this.resetFileThumb();
|
|
}
|
|
|
|
}
|
|
|
|
/**
|
|
* Updates the displayed input progress for a file
|
|
*
|
|
* @param {number} inputNum
|
|
* @param {number} progress
|
|
*/
|
|
updateFileProgress(inputNum, progress) {
|
|
const activeTab = this.getActiveTab();
|
|
if (inputNum !== activeTab) return;
|
|
|
|
const fileLoaded = document.getElementById("input-file-loaded");
|
|
if (progress === "error") {
|
|
fileLoaded.textContent = "Error";
|
|
fileLoaded.style.color = "#FF0000";
|
|
} else {
|
|
fileLoaded.textContent = progress + "%";
|
|
fileLoaded.style.color = "";
|
|
}
|
|
|
|
if (progress === 100) {
|
|
this.inputWorker.postMessage({
|
|
action: "setInput",
|
|
data: {
|
|
inputNum: inputNum,
|
|
silent: true
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates the input value for the specified inputNum
|
|
*
|
|
* @param {number} inputNum
|
|
* @param {string | ArrayBuffer} value
|
|
*/
|
|
updateInputValue(inputNum, value) {
|
|
let includeInput = false;
|
|
const recipeStr = toBase64(value, "A-Za-z0-9+/"); // B64 alphabet with no padding
|
|
if (recipeStr.length > 0 && recipeStr.length <= 68267) {
|
|
includeInput = true;
|
|
}
|
|
this.setUrl({
|
|
includeInput: includeInput,
|
|
input: recipeStr
|
|
});
|
|
|
|
if (typeof value === "string") {
|
|
this.inputWorker.postMessage({
|
|
action: "updateInputValue",
|
|
data: {
|
|
inputNum: inputNum,
|
|
value: value
|
|
}
|
|
});
|
|
} else {
|
|
this.inputWorker.postMessage({
|
|
action: "updateInputValue",
|
|
data: {
|
|
inputNum: inputNum,
|
|
value: value
|
|
}
|
|
}, [value]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Updates the .data property for the input of the specified inputNum
|
|
*
|
|
* @param {number} inputNum
|
|
* @param {object} inputData
|
|
*/
|
|
updateInputObj(inputNum, inputData) {
|
|
if (typeof inputData === "string") {
|
|
this.inputWorker.postMessage({
|
|
action: "updateInputObj",
|
|
data: {
|
|
inputNum: inputNum,
|
|
data: inputData
|
|
}
|
|
});
|
|
} else {
|
|
this.inputWorker.postMessage({
|
|
action: "updateInputObj",
|
|
data: {
|
|
inputNum: inputNum,
|
|
data: inputData
|
|
}
|
|
}, [inputData.fileBuffer]);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Displays information about the input.
|
|
*
|
|
* @param {number} length - The length of the current input string
|
|
* @param {number} lines - The number of the lines in the current input string
|
|
*/
|
|
setInputInfo(length, lines) {
|
|
let width = length.toString().length.toLocaleString();
|
|
width = width < 2 ? 2 : width;
|
|
|
|
const lengthStr = length.toString().padStart(width, " ").replace(/ /g, " ");
|
|
let msg = "length: " + lengthStr;
|
|
|
|
if (typeof lines === "number") {
|
|
const linesStr = lines.toString().padStart(width, " ").replace(/ /g, " ");
|
|
msg += "<br>lines: " + linesStr;
|
|
}
|
|
|
|
document.getElementById("input-info").innerHTML = msg;
|
|
|
|
}
|
|
|
|
/**
|
|
* Handler for input change events
|
|
*
|
|
* @param {event} e
|
|
*
|
|
* @fires Manager#statechange
|
|
*/
|
|
inputChange(e) {
|
|
// Ignore this function if the input is a file
|
|
const fileOverlay = document.getElementById("input-file");
|
|
if (fileOverlay.style.display === "block") return;
|
|
|
|
// Remove highlighting from input and output panes as the offsets might be different now
|
|
this.manager.highlighter.removeHighlights();
|
|
|
|
const textArea = document.getElementById("input-text");
|
|
const value = (textArea.value !== undefined) ? textArea.value : "";
|
|
const activeTab = this.getActiveTab();
|
|
|
|
this.app.progress = 0;
|
|
|
|
const lines = value.length < (this.app.options.ioDisplayThreshold * 1024) ?
|
|
(value.count("\n") + 1) : null;
|
|
this.setInputInfo(value.length, lines);
|
|
this.updateInputValue(activeTab, value);
|
|
this.updateTabHeader({inputNum: activeTab, input: value});
|
|
|
|
if (e && this.badKeys.indexOf(e.keyCode) < 0) {
|
|
// Fire the statechange event as the input has been modified
|
|
window.dispatchEvent(this.manager.statechange);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler for input paste events
|
|
* Checks that the size of the input is below the display limit, otherwise treats it as a file/blob
|
|
*
|
|
* @param {event} e
|
|
*/
|
|
inputPaste(e) {
|
|
const pastedData = e.clipboardData.getData("Text");
|
|
if (pastedData.length < (this.app.options.ioDisplayThreshold * 1024)) {
|
|
// inputChange() was being fired before the input value
|
|
// was set, so add it here instead
|
|
e.preventDefault();
|
|
document.getElementById("input-text").value += pastedData;
|
|
this.inputChange(e);
|
|
} else {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
const file = new File([pastedData], "PastedData", {
|
|
type: "text/plain",
|
|
lastModified: Date.now()
|
|
});
|
|
|
|
this.loadUIFiles([file]);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Handler for input dragover events.
|
|
* Gives the user a visual cue to show that items can be dropped here.
|
|
*
|
|
* @param {event} e
|
|
*/
|
|
inputDragover(e) {
|
|
// This will be set if we're dragging an operation
|
|
if (e.dataTransfer.effectAllowed === "move")
|
|
return false;
|
|
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
e.target.closest("#input-text,#input-file").classList.add("dropping-file");
|
|
}
|
|
|
|
/**
|
|
* Handler for input dragleave events.
|
|
* Removes the visual cue.
|
|
*
|
|
* @param {event} e
|
|
*/
|
|
inputDragleave(e) {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
e.target.closest("#input-text,#input-file").classList.remove("dropping-file");
|
|
}
|
|
|
|
/**
|
|
* Handler for input drop events.
|
|
* Loads the dragged data.
|
|
*
|
|
* @param {event} e
|
|
*/
|
|
inputDrop(e) {
|
|
// This will be set if we're dragging an operation
|
|
if (e.dataTransfer.effectAllowed === "move")
|
|
return false;
|
|
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
|
|
const text = e.dataTransfer.getData("Text");
|
|
|
|
e.target.closest("#input-text,#input-file").classList.remove("dropping-file");
|
|
|
|
if (text) {
|
|
// close file
|
|
// set text output
|
|
return;
|
|
}
|
|
|
|
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
|
|
this.loadUIFiles(e.dataTransfer.files);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler for open input button events
|
|
* Loads the opened data into the input textarea
|
|
*
|
|
* @param {event} e
|
|
*/
|
|
inputOpen(e) {
|
|
e.preventDefault();
|
|
|
|
if (e.target.files.length > 0) {
|
|
this.loadUIFiles(e.target.files);
|
|
e.target.value = "";
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Load files from the UI into the inputWorker
|
|
*
|
|
* @param files
|
|
*/
|
|
loadUIFiles(files) {
|
|
const numFiles = files.length;
|
|
const activeTab = this.getActiveTab();
|
|
log.debug(`Loading ${numFiles} files.`);
|
|
// Show something in the UI to make it clear we're loading files
|
|
|
|
this.hideLoadingMessage();
|
|
this.showLoadingInfo({
|
|
pending: numFiles,
|
|
loading: 0,
|
|
loaded: 0,
|
|
total: numFiles,
|
|
activeProgress: {
|
|
inputNum: activeTab,
|
|
progress: 0
|
|
}
|
|
}, false);
|
|
|
|
this.inputWorker.postMessage({
|
|
action: "loadUIFiles",
|
|
data: {
|
|
files: files,
|
|
activeTab: activeTab
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Displays a message to show the app is loading files
|
|
*/
|
|
showLoadingMessage() {
|
|
$("#loading-files-modal").modal("show");
|
|
}
|
|
|
|
/**
|
|
* Hides the loading message
|
|
*/
|
|
hideLoadingMessage() {
|
|
$("#loading-files-modal").modal("hide");
|
|
}
|
|
|
|
/**
|
|
* Checks the length of the files input. If it's 0, hide loading message
|
|
*/
|
|
checkInputFiles() {
|
|
const fileInput = document.getElementById("open-file");
|
|
const folderInput = document.getElementById("open-folder");
|
|
|
|
if (fileInput.value.length === 0 && folderInput.value.length === 0) {
|
|
this.hideLoadingMessage();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler for open input button click.
|
|
* Opens the open file dialog.
|
|
*/
|
|
inputOpenClick() {
|
|
this.showLoadingMessage();
|
|
document.getElementById("open-file").click();
|
|
document.body.onfocus = this.checkInputFiles.bind(this);
|
|
}
|
|
|
|
/**
|
|
* Handler for open folder button click
|
|
* Opens the open folder dialog.
|
|
*/
|
|
folderOpenClick() {
|
|
this.showLoadingMessage();
|
|
document.getElementById("open-folder").click();
|
|
document.body.onfocus = this.checkInputFiles.bind(this);
|
|
}
|
|
|
|
// set / setFile
|
|
// get the data for the active tab from inputWorker
|
|
// display it!
|
|
|
|
// setinputInfo
|
|
/**
|
|
* Display the loaded files information in the input header
|
|
* @param {object} loadedData
|
|
* @param {number} loadedData.pending
|
|
* @param {number} loadedData.loading
|
|
* @param {number} loadedData.loaded
|
|
* @param {number} loadedData.total
|
|
* @param {object} loadedData.activeProgress
|
|
* @param {number} loadedData.activeProgress.inputNum
|
|
* @param {number} loadedData.activeProgress.progress
|
|
* @param {boolean} autoRefresh
|
|
*/
|
|
showLoadingInfo(loadedData, autoRefresh) {
|
|
const pending = loadedData.pending;
|
|
const loading = loadedData.loading;
|
|
const loaded = loadedData.loaded;
|
|
const total = loadedData.total;
|
|
|
|
let width = total.toString().length;
|
|
width = width < 2 ? 2 : width;
|
|
|
|
const totalStr = total.toString().padStart(width, " ").replace(/ /g, " ");
|
|
let msg = "Total: " + totalStr;
|
|
|
|
const loadedStr = loaded.toString().padStart(width, " ").replace(/ /g, " ");
|
|
msg += "<br>Loaded: " + loadedStr;
|
|
|
|
if (pending > 0) {
|
|
const pendingStr = pending.toString().padStart(width, " ").replace(/ /g, " ");
|
|
msg += "<br>Pending: " + pendingStr;
|
|
} else if (loading > 0) {
|
|
const loadingStr = loading.toString().padStart(width, " ").replace(/ /g, " ");
|
|
msg += "<br>Loading: " + loadingStr;
|
|
}
|
|
|
|
document.getElementById("input-files-info").innerHTML = msg;
|
|
|
|
this.updateFileProgress(loadedData.activeProgress.inputNum, loadedData.activeProgress.progress);
|
|
|
|
const inputTitle = document.getElementById("input").firstElementChild;
|
|
if (loaded < total) {
|
|
const percentComplete = loaded / total * 100;
|
|
inputTitle.style.background = `linear-gradient(to right, var(--title-background-colour) ${percentComplete}%, var(--primary-background-colour) ${percentComplete}%)`;
|
|
} else {
|
|
inputTitle.style.background = "";
|
|
}
|
|
|
|
if (loaded < total && autoRefresh) {
|
|
setTimeout(function() {
|
|
this.inputWorker.postMessage({
|
|
action: "getLoadProgress",
|
|
data: this.getActiveTab()
|
|
});
|
|
}.bind(this), 100);
|
|
}
|
|
|
|
if (loaded === total) {
|
|
this.app.autoBake();
|
|
}
|
|
}
|
|
// displayTabInfo
|
|
// simple getInput for each tab
|
|
|
|
// displayFilePreview
|
|
|
|
/**
|
|
* Create a tab element for the input tab bar
|
|
*
|
|
* @param {number} inputNum
|
|
* @param {boolean} [active=false]
|
|
* @returns {Element}
|
|
*/
|
|
createTabElement(inputNum, active) {
|
|
const newTab = document.createElement("li");
|
|
newTab.setAttribute("inputNum", inputNum.toString());
|
|
|
|
if (active) newTab.classList.add("active-input-tab");
|
|
|
|
const newTabContent = document.createElement("div");
|
|
newTabContent.classList.add("input-tab-content");
|
|
newTabContent.innerText = `${inputNum.toString()}: New Tab`;
|
|
|
|
const newTabButton = document.createElement("button");
|
|
newTabButton.type = "button";
|
|
newTabButton.className = "btn btn-primary bmd-btn-icon btn-close-tab";
|
|
|
|
const newTabButtonIcon = document.createElement("i");
|
|
newTabButtonIcon.classList.add("material-icons");
|
|
newTabButtonIcon.innerText = "clear";
|
|
|
|
newTabButton.appendChild(newTabButtonIcon);
|
|
|
|
newTab.appendChild(newTabContent);
|
|
newTab.appendChild(newTabButton);
|
|
|
|
return newTab;
|
|
}
|
|
|
|
/**
|
|
* Redraw the tab bar with an updated list of tabs
|
|
*
|
|
* @param nums
|
|
* @param {number} activeTab
|
|
*/
|
|
refreshTabs(nums, activeTab) {
|
|
const tabsList = document.getElementById("input-tabs");
|
|
|
|
for (let i = tabsList.children.length - 1; i >= 0; i--) {
|
|
tabsList.children.item(i).remove();
|
|
}
|
|
|
|
for (let i = 0; i < nums.length; i++) {
|
|
let active = false;
|
|
if (nums[i] === activeTab) active = true;
|
|
tabsList.appendChild(this.createTabElement(nums[i], active));
|
|
}
|
|
|
|
if (nums.length > 1) {
|
|
tabsList.parentElement.style.display = "block";
|
|
|
|
document.getElementById("input-wrapper").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
|
|
document.getElementById("input-highlighter").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
|
|
document.getElementById("input-file").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
|
|
} else {
|
|
tabsList.parentElement.style.display = "none";
|
|
|
|
document.getElementById("input-wrapper").style.height = "calc(100% - var(--title-height))";
|
|
document.getElementById("input-highlighter").style.height = "calc(100% - var(--title-height))";
|
|
document.getElementById("input-file").style.height = "calc(100% - var(--title-height))";
|
|
}
|
|
|
|
this.changeTab(activeTab, this.app.options.syncTabs);
|
|
}
|
|
|
|
/**
|
|
* Change the active tab
|
|
*
|
|
* @param {number} inputNum
|
|
* @param {boolean} [changeOutput=false]
|
|
*/
|
|
changeTab(inputNum, changeOutput) {
|
|
const tabsList = document.getElementById("input-tabs");
|
|
|
|
this.manager.highlighter.removeHighlights();
|
|
getSelection().removeAllRanges();
|
|
|
|
let found = false;
|
|
let minNum = Number.MAX_SAFE_INTEGER;
|
|
for (let i = 0; i < tabsList.children.length; i++) {
|
|
const tabNum = parseInt(tabsList.children.item(i).getAttribute("inputNum"), 10);
|
|
if (tabNum === inputNum) {
|
|
tabsList.children.item(i).classList.add("active-input-tab");
|
|
found = true;
|
|
} else {
|
|
tabsList.children.item(i).classList.remove("active-input-tab");
|
|
}
|
|
if (tabNum < minNum) {
|
|
minNum = tabNum;
|
|
}
|
|
}
|
|
if (!found) {
|
|
let direction = "right";
|
|
if (inputNum < minNum) {
|
|
direction = "left";
|
|
}
|
|
this.inputWorker.postMessage({
|
|
action: "refreshTabs",
|
|
data: {
|
|
inputNum: inputNum,
|
|
direction: direction
|
|
}
|
|
});
|
|
} else {
|
|
this.inputWorker.postMessage({
|
|
action: "setInput",
|
|
data: {
|
|
inputNum: inputNum,
|
|
silent: true
|
|
}
|
|
});
|
|
}
|
|
|
|
if (changeOutput) {
|
|
this.manager.output.changeTab(inputNum, false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler for clicking on a tab
|
|
*
|
|
* @param {event} mouseEvent
|
|
*/
|
|
changeTabClick(mouseEvent) {
|
|
if (!mouseEvent.target) return;
|
|
|
|
const tabNum = mouseEvent.target.parentElement.getAttribute("inputNum");
|
|
if (tabNum >= 0) {
|
|
this.changeTab(parseInt(tabNum, 10), this.app.options.syncTabs);
|
|
}
|
|
}
|
|
|
|
|
|
/**
|
|
* Updates the tab header to display the new input content
|
|
*/
|
|
updateTabHeader(headerData) {
|
|
const tabsList = document.getElementById("input-tabs");
|
|
const inputNum = headerData.inputNum;
|
|
let inputData = "New Tab";
|
|
if (headerData.input.length > 0) {
|
|
inputData = headerData.input.slice(0, 100);
|
|
}
|
|
for (let i = 0; i < tabsList.children.length; i++) {
|
|
if (tabsList.children.item(i).getAttribute("inputNum") === inputNum.toString()) {
|
|
tabsList.children.item(i).firstElementChild.innerText = `${inputNum}: ${inputData}`;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
// removeTabClick
|
|
|
|
// move getNearbyNums / getLargest / getSmallest / getNext / getPrevious to inputWorker
|
|
|
|
/**
|
|
* Gets the number of the current active tab
|
|
*
|
|
* @returns {number}
|
|
*/
|
|
getActiveTab() {
|
|
const activeTabs = document.getElementsByClassName("active-input-tab");
|
|
if (activeTabs.length > 0) {
|
|
const activeTab = activeTabs.item(0);
|
|
const tabNum = activeTab.getAttribute("inputNum");
|
|
return parseInt(tabNum, 10);
|
|
}
|
|
return -1;
|
|
}
|
|
|
|
/**
|
|
* Gets the li element for the tab of an input number
|
|
*
|
|
* @param {number} inputNum
|
|
* @returns {Element}
|
|
*/
|
|
getTabItem(inputNum) {
|
|
const tabs = document.getElementById("input-tabs").children;
|
|
for (let i = 0; i < tabs.length; i++) {
|
|
if (parseInt(tabs.item(i).getAttribute("inputNum"), 10) === inputNum) {
|
|
return tabs.item(i);
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Gets a list of tab numbers for the currently open tabs
|
|
*/
|
|
getTabList() {
|
|
const nums = [];
|
|
const tabs = document.getElementById("input-tabs").children;
|
|
for (let i = 0; i < tabs.length; i++) {
|
|
nums.push(parseInt(tabs.item(i).getAttribute("inputNum"), 10));
|
|
}
|
|
return nums;
|
|
}
|
|
|
|
/**
|
|
* Handler for clear all IO events.
|
|
* Resets the input, output and info areas
|
|
*/
|
|
clearAllIoClick() {
|
|
this.manager.worker.cancelBake();
|
|
this.manager.output.removeAllOutputs();
|
|
|
|
this.manager.highlighter.removeHighlights();
|
|
getSelection().removeAllRanges();
|
|
|
|
const tabsList = document.getElementById("input-tabs").children;
|
|
for (let i = tabsList.length - 1; i >= 0; i--) {
|
|
tabsList.item(i).remove();
|
|
}
|
|
|
|
this.setupInputWorker();
|
|
this.addInput(true);
|
|
}
|
|
|
|
/**
|
|
* Handler for clear IO click event.
|
|
* Resets the input for the current tab
|
|
*/
|
|
clearIoClick() {
|
|
const inputNum = this.getActiveTab();
|
|
if (inputNum === -1) return;
|
|
|
|
this.manager.highlighter.removeHighlights();
|
|
getSelection().removeAllRanges();
|
|
|
|
this.updateInputValue(inputNum, "");
|
|
|
|
this.set({
|
|
inputNum: inputNum,
|
|
input: ""
|
|
});
|
|
|
|
this.updateTabHeader({inputNum: inputNum, input: ""});
|
|
}
|
|
|
|
// filter stuff should be sent to the inputWorker
|
|
// returns a filterResult message that is handled and used to update the UI
|
|
|
|
/**
|
|
* Sets the console log level in the worker.
|
|
*
|
|
* @param {string} level
|
|
*/
|
|
setLogLevel(level) {
|
|
if (!this.inputWorker) return;
|
|
this.inputWorker.postMessage({
|
|
action: "setLogLevel",
|
|
data: log.getLevel()
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Sends a message to the inputWorker to add a new input.
|
|
* @param {boolean} [changeTab=false]
|
|
*/
|
|
addInput(changeTab=false) {
|
|
if (!this.inputWorker) return;
|
|
this.inputWorker.postMessage({
|
|
action: "addInput",
|
|
data: changeTab
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handler for add input button clicked
|
|
*/
|
|
addInputClick() {
|
|
this.addInput(true);
|
|
}
|
|
|
|
/**
|
|
* Handler for when the inputWorker adds a new input
|
|
*
|
|
* @param {boolean} changeTab
|
|
* @param {number} inputNum
|
|
*/
|
|
inputAdded(changeTab, inputNum) {
|
|
this.addTab(inputNum, changeTab);
|
|
this.manager.output.addOutput(inputNum, changeTab);
|
|
}
|
|
|
|
/**
|
|
* Adds a new input tab.
|
|
*
|
|
* @param {number} inputNum
|
|
* @param {boolean} [changeTab=true]
|
|
*/
|
|
addTab(inputNum, changeTab = true) {
|
|
const tabsWrapper = document.getElementById("input-tabs");
|
|
const numTabs = tabsWrapper.children.length;
|
|
|
|
if (!this.getTabItem(inputNum) && numTabs < this.maxTabs) {
|
|
const newTab = this.createTabElement(inputNum, false);
|
|
|
|
|
|
tabsWrapper.appendChild(newTab);
|
|
|
|
if (numTabs > 0) {
|
|
tabsWrapper.parentElement.style.display = "block";
|
|
|
|
document.getElementById("input-wrapper").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
|
|
document.getElementById("input-highlighter").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
|
|
document.getElementById("input-file").style.height = "calc(100% - var(--tab-height) - var(--title-height))";
|
|
} else {
|
|
tabsWrapper.parentElement.style.display = "none";
|
|
|
|
document.getElementById("input-wrapper").style.height = "calc(100% - var(--title-height))";
|
|
document.getElementById("input-highlighter").style.height = "calc(100% - var(--title-height))";
|
|
document.getElementById("input-file").style.height = "calc(100% - var(--title-height))";
|
|
}
|
|
|
|
this.inputWorker.postMessage({
|
|
action: "updateTabHeader",
|
|
data: inputNum
|
|
});
|
|
}
|
|
|
|
if (changeTab) {
|
|
this.changeTab(inputNum, true);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler for when the inputWorker adds multiple inputs
|
|
*/
|
|
addInputs() {
|
|
this.inputWorker.postMessage({
|
|
action: "refreshTabs",
|
|
data: {
|
|
inputNum: this.getActiveTab(),
|
|
direction: "left"
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Sends a message to the inputWorker to remove an input.
|
|
* If the input tab is on the screen, refreshes the tabs
|
|
*
|
|
* @param {number} inputNum
|
|
*/
|
|
removeInput(inputNum) {
|
|
let refresh = false;
|
|
if (this.getTabItem(inputNum) !== null) {
|
|
refresh = true;
|
|
}
|
|
this.inputWorker.postMessage({
|
|
action: "removeInput",
|
|
data: {
|
|
inputNum: inputNum,
|
|
refreshTabs: refresh
|
|
}
|
|
});
|
|
|
|
this.manager.output.removeTab(inputNum);
|
|
}
|
|
|
|
/**
|
|
* Handler for clicking on a remove tab button
|
|
* @param {event} mouseEvent
|
|
*/
|
|
removeTabClick(mouseEvent) {
|
|
if (!mouseEvent.target) {
|
|
return;
|
|
}
|
|
const tabNum = mouseEvent.target.parentElement.parentElement.getAttribute("inputNum");
|
|
if (tabNum) {
|
|
this.removeInput(parseInt(tabNum, 10));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler for clicking on next tab button
|
|
*/
|
|
changeTabRight() {
|
|
const activeTab = this.getActiveTab();
|
|
this.inputWorker.postMessage({
|
|
action: "changeTabRight",
|
|
data: {
|
|
activeTab: activeTab,
|
|
nums: this.getTabList()
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handler for clicking on next tab button
|
|
*/
|
|
changeTabLeft() {
|
|
const activeTab = this.getActiveTab();
|
|
this.inputWorker.postMessage({
|
|
action: "changeTabLeft",
|
|
data: {
|
|
activeTab: activeTab,
|
|
nums: this.getTabList()
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Handler for go to tab button clicked
|
|
*/
|
|
goToTab() {
|
|
const tabNum = parseInt(window.prompt("Enter tab number:", this.getActiveTab().toString()), 10);
|
|
this.changeTab(tabNum, this.app.options.syncTabs);
|
|
}
|
|
|
|
/**
|
|
* Handler for find tab button clicked
|
|
*/
|
|
findTab() {
|
|
this.filterTabSearch();
|
|
$("#input-tab-modal").modal();
|
|
}
|
|
|
|
/**
|
|
* Sends a message to the inputWorker to search the inputs
|
|
*/
|
|
filterTabSearch() {
|
|
const showPending = document.getElementById("input-show-pending").checked;
|
|
const showLoading = document.getElementById("input-show-loading").checked;
|
|
const showLoaded = document.getElementById("input-show-loaded").checked;
|
|
|
|
const fileNameFilter = document.getElementById("input-filename-filter").value;
|
|
const contentFilter = document.getElementById("input-content-filter").value;
|
|
const numResults = parseInt(document.getElementById("input-num-results").value, 10);
|
|
|
|
this.inputWorker.postMessage({
|
|
action: "filterTabs",
|
|
data: {
|
|
showPending: showPending,
|
|
showLoading: showLoading,
|
|
showLoaded: showLoaded,
|
|
fileNameFilter: fileNameFilter,
|
|
contentFilter: contentFilter,
|
|
numResults: numResults
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Displays the results of a tab search in the find tab box
|
|
*
|
|
* @param {object[]} results
|
|
*
|
|
*/
|
|
displayTabSearchResults(results) {
|
|
const resultsList = document.getElementById("input-search-results");
|
|
|
|
for (let i = resultsList.children.length - 1; i >= 0; i--) {
|
|
resultsList.children.item(i).remove();
|
|
}
|
|
|
|
for (let i = 0; i < results.length; i++) {
|
|
const newListItem = document.createElement("li");
|
|
newListItem.classList.add("input-filter-result");
|
|
newListItem.setAttribute("inputNum", results[i].inputNum);
|
|
newListItem.innerText = `${results[i].inputNum}: ${results[i].textDisplay}`;
|
|
|
|
resultsList.appendChild(newListItem);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Handler for clicking on a filter result
|
|
*
|
|
* @param {event} e
|
|
*/
|
|
filterItemClick(e) {
|
|
if (!e.target) return;
|
|
const inputNum = parseInt(e.target.getAttribute("inputNum"), 10);
|
|
if (inputNum <= 0) return;
|
|
|
|
$("#input-tab-modal").modal("hide");
|
|
this.changeTab(inputNum, this.app.options.syncTabs);
|
|
}
|
|
|
|
/**
|
|
* Update the input URL to the new value
|
|
*
|
|
* @param {object} urlData
|
|
* @param {boolean} urlData.includeInput
|
|
* @param {string} urlData.input
|
|
*/
|
|
setUrl(urlData) {
|
|
this.app.updateTitle(urlData.includeInput, urlData.input, true);
|
|
}
|
|
|
|
|
|
}
|
|
|
|
export default InputWaiter;
|