CyberChef/src/web/InputWaiter.mjs

1420 lines
46 KiB
JavaScript
Raw Normal View History

2018-05-15 19:36:45 +02:00
/**
* @author n1474335 [n1474335@gmail.com]
* @author j433866 [j433866@gmail.com]
* @copyright Crown Copyright 2019
2018-05-15 19:36:45 +02:00
* @license Apache-2.0
*/
import LoaderWorker from "worker-loader?inline&fallback=false!./LoaderWorker";
import InputWorker from "worker-loader?inline&fallback=false!./InputWorker";
2018-05-15 19:36:45 +02:00
import Utils from "../core/Utils";
import { toBase64 } from "../core/lib/Base64";
import { isImage } from "../core/lib/FileType";
2018-05-15 19:36:45 +02:00
/**
* 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.
2018-05-15 19:36:45 +02:00
*/
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;
this.inputTimeout = null;
this.callbacks = {};
this.callbackID = 0;
2018-05-15 19:36:45 +02:00
}
/**
* 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() {
2019-04-30 14:18:22 +02:00
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
}
});
}
2018-05-15 19:36:45 +02:00
/**
* Adds a new loaderWorker
2018-05-15 19:36:45 +02:00
*
2019-05-14 12:45:13 +02:00
* @returns {number} - The index of the created worker
2018-05-15 19:36:45 +02:00
*/
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);
2018-05-15 19:36:45 +02:00
}
/**
* Removes a loaderworker
*
2019-05-14 12:45:13 +02:00
* @param {Object} workerObj - Object containing the loaderWorker and its id
* @param {LoaderWorker} workerObj.worker - The actual loaderWorker
* @param {number} workerObj.id - The ID of the loaderWorker
*/
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
*
2019-05-14 12:45:13 +02:00
* @param {number} id - The ID of the loaderWorker to find
* @returns {object}
*/
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
*
2019-05-14 12:45:13 +02:00
* @param {number} id - The ID of hte loaderWorker to find
* @returns {number} The current index of the loaderWorker in the array
*/
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
*
2019-05-14 12:45:13 +02:00
* @param {object} inputData - Object containing the input to be loaded
* @param {File} inputData.file - The actual file object to load
* @param {number} inputData.inputNum - The inputNum for the file object
* @param {number} inputData.workerId - The ID of the loaderWorker that will load it
*/
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
2019-05-14 12:45:13 +02:00
* Sends the message straight to the inputWorker to be handled there.
*
* @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 "loadingInfo":
2019-04-30 14:18:22 +02:00
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 "queueInput":
this.manager.worker.queueInput(r.data);
break;
case "bakeAllInputs":
this.manager.worker.bakeAllInputs(r.data);
break;
2019-04-30 15:15:05 +02:00
case "displayTabSearchResults":
this.displayTabSearchResults(r.data);
break;
case "setUrl":
this.setUrl(r.data);
break;
case "inputSwitch":
this.manager.output.inputSwitch(r.data);
break;
case "getInput":
this.callbacks[r.data.id](r.data);
break;
case "removeChefWorker":
this.removeChefWorker();
break;
default:
log.error(`Unknown action ${r.action}.`);
}
}
/**
* Sends a message to the inputWorker to bake all inputs
*/
bakeAll() {
this.app.progress = 0;
this.manager.controls.toggleBakeButtonFunction(false, true);
this.inputWorker.postMessage({
action: "bakeAll"
});
}
/**
* Sets the input in the input area
*
2019-05-14 12:45:13 +02:00
* @param {object} inputData - Object containing the input and its metadata
* @param {number} inputData.inputNum - The unique inputNum for the selected input
* @param {string | object} inputData.input - The actual input data
* @param {string} inputData.name - The name of the input file
* @param {number} inputData.size - The size in bytes of the input file
* @param {string} inputData.type - The MIME type of the input file
* @param {number} inputData.progress - The load progress of the input file
* @param {boolean} [silent=false] - If true, fires the manager statechange event
*/
2019-05-08 14:46:29 +02:00
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;
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");
2019-05-08 14:46:29 +02:00
const lines = inputData.input.length < (this.app.options.ioDisplayThreshold * 1024) ?
inputData.input.count("\n") + 1 : null;
this.setInputInfo(inputData.input.length, lines);
if (!silent) window.dispatchEvent(this.manager.statechange);
2019-05-08 14:46:29 +02:00
} else {
this.setFile(inputData);
}
2019-05-08 14:46:29 +02:00
}.bind(this));
2018-05-15 19:36:45 +02:00
}
/**
2019-05-14 12:45:13 +02:00
* Displays file details
*
2019-05-14 12:45:13 +02:00
* @param {object} inputData - Object containing the input and its metadata
* @param {number} inputData.inputNum - The unique inputNum for the selected input
* @param {string | object} inputData.input - The actual input data
* @param {string} inputData.name - The name of the input file
* @param {number} inputData.size - The size in bytes of the input file
* @param {string} inputData.type - The MIME type of the input file
* @param {number} inputData.progress - The load progress of the input file
*/
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 + "%";
}
2019-05-29 12:46:21 +02:00
this.setInputInfo(inputData.size, null);
this.displayFilePreview(inputData);
}
2018-05-15 19:36:45 +02:00
/**
* Reset the input thumbnail to the default icon
*/
resetFileThumb() {
const fileThumb = document.getElementById("input-file-thumbnail");
fileThumb.src = require("./static/images/file-128x128.png");
}
2018-05-15 19:36:45 +02:00
/**
* Shows a chunk of the file in the input behind the file overlay
2018-05-15 19:36:45 +02:00
*
2019-05-14 12:45:13 +02:00
* @param {Object} inputData - Object containing the input data
* @param {number} inputData.inputNum - The inputNum of the file being displayed
* @param {ArrayBuffer} inputData.input - The actual input to display
2018-05-15 19:36:45 +02:00
*/
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)));
if (this.app.options.imagePreview) {
const inputArr = new Uint8Array(input),
type = isImage(inputArr);
if (type && type !== "image/tiff" && inputArr.byteLength <= 512000) {
2019-05-14 12:45:13 +02:00
// Most browsers don't support displaying TIFFs, so ignore them
// Assume anything over 512000 bytes is truncated
const blob = new Blob([inputArr], {type: type}),
url = URL.createObjectURL(blob);
fileThumb.src = url;
} else {
this.resetFileThumb();
}
} else {
this.resetFileThumb();
}
2018-05-15 19:36:45 +02:00
}
/**
2019-05-14 12:45:13 +02:00
* Updates the displayed load progress for a file
*
* @param {number} inputNum
2019-05-14 12:45:13 +02:00
* @param {number | string} progress - Either a number or "error"
*/
updateFileProgress(inputNum, progress) {
const activeTab = this.getActiveTab();
if (inputNum !== activeTab) return;
const fileLoaded = document.getElementById("input-file-loaded");
let oldProgress = fileLoaded.textContent;
if (oldProgress !== "Error") {
oldProgress = parseInt(oldProgress.replace("%", ""), 10);
}
if (progress === "error") {
fileLoaded.textContent = "Error";
fileLoaded.style.color = "#FF0000";
} else {
fileLoaded.textContent = progress + "%";
fileLoaded.style.color = "";
}
if (progress === 100 && progress !== oldProgress) {
2019-05-14 12:45:13 +02:00
// Don't set the input if the progress hasn't changed
this.inputWorker.postMessage({
action: "setInput",
data: {
inputNum: inputNum,
silent: false
}
});
window.dispatchEvent(this.manager.statechange);
}
}
2018-05-15 19:36:45 +02:00
/**
2019-05-14 12:45:13 +02:00
* Updates the stored value for the specified inputNum
2018-05-15 19:36:45 +02:00
*
* @param {number} inputNum
* @param {string | ArrayBuffer} value
2018-05-15 19:36:45 +02:00
*/
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 {
2019-05-14 12:45:13 +02:00
// ArrayBuffer is transferable
this.inputWorker.postMessage({
action: "updateInputValue",
data: {
inputNum: inputNum,
value: value
}
}, [value]);
}
}
/**
2019-05-14 12:45:13 +02:00
* Updates the .data property for the input of the specified inputNum.
* Used for switching the output into the input
*
2019-05-14 12:45:13 +02:00
* @param {number} inputNum - The inputNum of the input we're changing
* @param {object} inputData - The new data object
*/
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]);
}
}
2018-05-15 19:36:45 +02:00
/**
* Get the input value for the specified input
*
* @param {number} inputNum - The inputNum of the input to retrieve from the inputWorker
* @returns {ArrayBuffer | string}
*/
async getInputValue(inputNum) {
return await new Promise(resolve => {
this.getInput(inputNum, false, r => {
resolve(r.data);
});
});
}
/**
* Get the input object for the specified input
*
* @param {number} inputNum - The inputNum of the input to retrieve from the inputWorker
* @returns {object}
*/
async getInputObj(inputNum) {
return await new Promise(resolve => {
this.getInput(inputNum, true, r => {
resolve(r.data);
});
});
}
/**
* Gets the specified input from the inputWorker
*
* @param {number} inputNum - The inputNum of the data to get
* @param {boolean} getObj - If true, get the actual data object of the input instead of just the value
* @param {Function} callback - The callback to execute when the input is returned
* @returns {ArrayBuffer | string | object}
*/
getInput(inputNum, getObj, callback) {
const id = this.callbackID++;
this.callbacks[id] = callback;
this.inputWorker.postMessage({
action: "getInput",
data: {
inputNum: inputNum,
getObj: getObj,
id: id
}
});
}
/**
* 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, "&nbsp;");
let msg = "length: " + lengthStr;
if (typeof lines === "number") {
const linesStr = lines.toString().padStart(width, " ").replace(/ /g, "&nbsp;");
msg += "<br>lines: " + linesStr;
}
document.getElementById("input-info").innerHTML = msg;
2018-05-15 19:36:45 +02:00
}
2018-05-15 19:36:45 +02:00
/**
* Debouncer to stop functions from being executed multiple times in a
* short space of time
* https://davidwalsh.name/javascript-debounce-function
*
* @param {function} func - The function to be executed after the debounce time
* @param {number} wait - The time (ms) to wait before executing the function
* @param {array} args - Array of arguments to be passed to func
* @returns {function}
*/
debounce(func, wait, args) {
return function() {
const context = this,
later = function() {
this.inputTimeout = null;
func.apply(context, args);
};
clearTimeout(this.inputTimeout);
this.inputTimeout = setTimeout(later, wait);
}.bind(this);
}
/**
* Handler for input change events.
* Debounces the input so we don't call autobake too often.
*
* @param {event} e
*/
debounceInputChange(e) {
this.debounce(this.inputChange.bind(this), 50, [e])();
}
2018-05-15 19:36:45 +02:00
/**
2019-05-14 12:45:13 +02:00
* Handler for input change events.
* Updates the value stored in the inputWorker
2018-05-15 19:36:45 +02:00
*
* @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;
2018-05-15 19:36:45 +02:00
// 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();
2018-05-15 19:36:45 +02:00
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});
2018-05-15 19:36:45 +02:00
if (e && this.badKeys.indexOf(e.keyCode) < 0) {
// Fire the statechange event as the input has been modified
window.dispatchEvent(this.manager.statechange);
}
}
2019-04-30 12:48:01 +02:00
/**
* 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)) {
2019-05-14 12:45:13 +02:00
// Pasting normally fires the inputChange() event before
// changing the value, so instead change it here ourselves
// and manually fire inputChange()
2019-04-30 12:48:01 +02:00
e.preventDefault();
document.getElementById("input-text").value += pastedData;
this.debounceInputChange(e);
2019-04-30 12:48:01 +02:00
} else {
e.preventDefault();
e.stopPropagation();
const file = new File([pastedData], "PastedData", {
type: "text/plain",
lastModified: Date.now()
});
this.loadUIFiles([file]);
return false;
}
}
2018-05-15 19:36:45 +02:00
/**
* 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");
2018-05-15 19:36:45 +02:00
}
/**
* Handler for input drop events.
* Loads the dragged data.
2018-05-15 19:36:45 +02:00
*
* @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");
2018-05-15 19:36:45 +02:00
if (text) {
2019-05-14 12:45:13 +02:00
// Append the text to the current input and fire inputChange()
document.getElementById("input-text").value += text;
this.inputChange(e);
2018-05-15 19:36:45 +02:00
return;
}
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
this.loadUIFiles(e.dataTransfer.files);
2018-05-15 19:36:45 +02:00
}
}
/**
* 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 = "";
}
}
2018-05-15 19:36:45 +02:00
/**
* Load files from the UI into the inputWorker
2018-05-15 19:36:45 +02:00
*
2019-05-14 12:45:13 +02:00
* @param {FileList} files - The list of files to be loaded
2018-05-15 19:36:45 +02:00
*/
loadUIFiles(files) {
const numFiles = files.length;
const activeTab = this.getActiveTab();
log.debug(`Loading ${numFiles} files.`);
2018-05-15 19:36:45 +02:00
2019-05-14 12:45:13 +02:00
// Display the number of files as pending so the user
// knows that we've received the files.
this.hideLoadingMessage();
this.showLoadingInfo({
pending: numFiles,
loading: 0,
loaded: 0,
total: numFiles,
activeProgress: {
inputNum: activeTab,
progress: 0
}
2019-04-30 14:18:22 +02:00
}, false);
this.inputWorker.postMessage({
action: "loadUIFiles",
data: {
files: files,
activeTab: activeTab
}
});
2018-05-15 19:36:45 +02:00
}
/**
* Displays a message to show the app is loading files
2018-05-15 19:36:45 +02:00
*/
showLoadingMessage() {
$("#loading-files-modal").modal("show");
2018-05-15 19:36:45 +02:00
}
/**
* Hides the loading message
2018-05-15 19:36:45 +02:00
*/
hideLoadingMessage() {
$("#loading-files-modal").modal("hide");
2018-05-15 19:36:45 +02:00
}
/**
2019-05-14 12:45:13 +02:00
* 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();
2019-05-14 12:45:13 +02:00
// When the document body regains focus, check if there's files in the input field
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();
2019-05-14 12:45:13 +02:00
// When the document body regains focus, check if there's files in the input field
document.body.onfocus = this.checkInputFiles.bind(this);
}
2018-05-15 19:36:45 +02:00
/**
2019-05-14 12:45:13 +02:00
* Display the loaded files information in the input header.
* Also, sets the background of the Input header to be a progress bar
* @param {object} loadedData - Object containing the loading information
* @param {number} loadedData.pending - How many files are pending (not loading / loaded)
* @param {number} loadedData.loading - How many files are being loaded
* @param {number} loadedData.loaded - How many files have been loaded
* @param {number} loadedData.total - The total number of files
* @param {object} loadedData.activeProgress - Object containing data about the active tab
* @param {number} loadedData.activeProgress.inputNum - The inputNum of the input the progress is for
* @param {number} loadedData.activeProgress.progress - The loading progress of the active input
* @param {boolean} autoRefresh - If true, automatically refreshes the loading info by sending a message to the inputWorker after 100ms
2018-05-15 19:36:45 +02:00
*/
2019-04-30 14:18:22 +02:00
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, "&nbsp;");
let msg = "Total: " + totalStr;
const loadedStr = loaded.toString().padStart(width, " ").replace(/ /g, "&nbsp;");
msg += "<br>Loaded: " + loadedStr;
if (pending > 0) {
const pendingStr = pending.toString().padStart(width, " ").replace(/ /g, "&nbsp;");
msg += "<br>Pending: " + pendingStr;
} else if (loading > 0) {
const loadingStr = loading.toString().padStart(width, " ").replace(/ /g, "&nbsp;");
msg += "<br>Loading: " + loadingStr;
}
document.getElementById("input-files-info").innerHTML = msg;
2018-05-15 19:36:45 +02:00
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 = "";
}
2019-04-30 14:18:22 +02:00
if (loaded < total && autoRefresh) {
setTimeout(function() {
this.inputWorker.postMessage({
action: "getLoadProgress",
data: this.getActiveTab()
});
}.bind(this), 100);
}
}
/**
* Create a tab element for the input tab bar
*
2019-05-14 12:45:13 +02:00
* @param {number} inputNum - The inputNum of the new tab
* @param {boolean} [active=false] - If true, sets the tab to active
* @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);
newTabButton.addEventListener("click", this.removeTabClick.bind(this));
newTab.appendChild(newTabContent);
newTab.appendChild(newTabButton);
return newTab;
}
/**
2019-05-14 12:45:13 +02:00
* Redraw the tab bar with an updated list of tabs.
* Then changes to the activeTab
*
2019-05-14 12:45:13 +02:00
* @param {number[]} nums - The inputNums of the tab bar to be drawn
* @param {number} activeTab - The inputNum of the active tab
*/
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))";
2019-03-22 16:19:03 +01:00
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);
}
/**
2019-05-14 12:45:13 +02:00
* Change to a different tab.
*
2019-05-14 12:45:13 +02:00
* @param {number} inputNum - The inputNum of the tab to change to
* @param {boolean} [changeOutput=false] - If true, also changes the output
*/
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;
2019-04-30 14:18:22 +02:00
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;
}
}
}
/**
* 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
*
2019-05-14 12:45:13 +02:00
* @param {number} inputNum - The inputNum of the tab we're trying to find
* @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
2019-05-14 12:45:13 +02:00
*
* @returns {number[]}
*/
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;
}
2019-04-30 14:18:22 +02:00
/**
* Handler for clear all IO events.
2019-05-14 12:45:13 +02:00
* Resets the input, output and info areas, and creates a new inputWorker
2019-04-30 14:18:22 +02:00
*/
clearAllIoClick() {
this.manager.worker.cancelBake(true, true);
this.manager.worker.loaded = false;
2019-04-30 14:18:22 +02:00
this.manager.output.removeAllOutputs();
this.manager.output.terminateZipWorker();
2019-04-30 14:18:22 +02:00
this.manager.highlighter.removeHighlights();
getSelection().removeAllRanges();
2019-04-30 14:18:22 +02:00
const tabsList = document.getElementById("input-tabs").children;
for (let i = tabsList.length - 1; i >= 0; i--) {
tabsList.item(i).remove();
}
this.setupInputWorker();
this.manager.worker.setupChefWorker();
2019-04-30 14:18:22 +02:00
this.addInput(true);
2019-05-21 12:24:44 +02:00
this.bakeAll();
2019-04-30 14:18:22 +02:00
}
/**
* 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: ""});
}
/**
* 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.
2019-05-14 12:45:13 +02:00
* @param {boolean} [changeTab=false] - If true, changes the tab to the new input
*/
2019-04-30 14:18:22 +02:00
addInput(changeTab=false) {
if (!this.inputWorker) return;
this.inputWorker.postMessage({
2019-04-30 14:18:22 +02:00
action: "addInput",
data: changeTab
});
}
2019-04-30 14:18:22 +02:00
/**
2019-05-14 12:45:13 +02:00
* Handler for add input button clicked.
2019-04-30 14:18:22 +02:00
*/
addInputClick() {
this.addInput(true);
}
/**
* Handler for when the inputWorker adds a new input
*
2019-05-14 12:45:13 +02:00
* @param {boolean} changeTab - Whether or not to change to the new input tab
* @param {number} inputNum - The new inputNum
*/
inputAdded(changeTab, inputNum) {
this.addTab(inputNum, changeTab);
this.manager.output.addOutput(inputNum, changeTab);
this.manager.worker.addChefWorker();
}
/**
* Remove a chefWorker from the workerWaiter if we remove an input
*/
removeChefWorker() {
const workerIdx = this.manager.worker.getInactiveChefWorker(true);
const worker = this.manager.worker.chefWorkers[workerIdx];
this.manager.worker.removeChefWorker(worker);
}
/**
* Adds a new input tab.
*
2019-05-14 12:45:13 +02:00
* @param {number} inputNum - The inputNum of the new tab
* @param {boolean} [changeTab=true] - If true, changes to the new tab once it's been added
*/
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);
}
}
/**
* Sends a message to the inputWorker to remove an input.
* If the input tab is on the screen, refreshes the tabs
*
2019-05-14 12:45:13 +02:00
* @param {number} inputNum - The inputNum of the tab to be removed
*/
removeInput(inputNum) {
let refresh = false;
if (this.getTabItem(inputNum) !== null) {
refresh = true;
}
this.inputWorker.postMessage({
action: "removeInput",
data: {
inputNum: inputNum,
refreshTabs: refresh,
removeChefWorker: true
}
});
this.manager.output.removeTab(inputNum);
}
/**
* Handler for clicking on a remove tab button
2019-05-14 12:45:13 +02:00
*
* @param {event} mouseEvent
*/
removeTabClick(mouseEvent) {
if (!mouseEvent.target) {
return;
}
2019-05-14 12:45:13 +02:00
const tabNum = mouseEvent.target.closest("button").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()
}
});
}
/**
2019-05-14 12:45:13 +02:00
* Handler for clicking on previous 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);
}
2019-04-30 15:15:05 +02:00
/**
* 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
*
2019-05-14 12:45:13 +02:00
* @param {object[]} results - List of results objects
2019-04-30 15:15:05 +02:00
*
*/
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
*
2019-05-14 12:45:13 +02:00
* @param {object} urlData - Object containing the URL data
* @param {boolean} urlData.includeInput - If true, the input is included in the title
* @param {string} urlData.input - The input data to be included
*/
setUrl(urlData) {
this.app.updateTitle(urlData.includeInput, urlData.input, true);
}
2019-04-30 15:15:05 +02:00
2018-05-15 19:36:45 +02:00
}
export default InputWaiter;