diff --git a/src/web/App.mjs b/src/web/App.mjs
index 4a939403..a8e5d078 100755
--- a/src/web/App.mjs
+++ b/src/web/App.mjs
@@ -181,7 +181,7 @@ class App {
* @returns {string}
*/
getInput() {
- return this.manager.input.get();
+ return this.manager.input.getActive();
}
/**
diff --git a/src/web/InputWaiter.mjs b/src/web/InputWaiter.mjs
old mode 100755
new mode 100644
index 3b50fd41..920cda8e
--- a/src/web/InputWaiter.mjs
+++ b/src/web/InputWaiter.mjs
@@ -1,6 +1,7 @@
/**
* @author n1474335 [n1474335@gmail.com]
- * @copyright Crown Copyright 2016
+ * @author j433866 [j433866@gmail.com]
+ * @copyright Crown Copyright 2019
* @license Apache-2.0
*/
@@ -16,8 +17,8 @@ class InputWaiter {
/**
* InputWaiter constructor.
*
- * @param {App} app - The main view object for CyberChef.
- * @param {Manager} manager - The CyberChef event manager.
+ * @param {App} app - The main view object for CyberChef
+ * @param {Manager} manager- The CyberChef event manager.
*/
constructor(app, manager) {
this.app = app;
@@ -41,166 +42,383 @@ class InputWaiter {
145, //Scroll
];
- this.loaderWorkers = {};
- this.fileBuffers = {};
- this.inputs = {};
+ this.loaderWorkers = [];
+ this.maxWorkers = navigator.hardwareConcurrency || 4;
+ this.inputs = [];
+ this.pendingFiles = [];
+ this.maxTabs = 4; // Calculate this
}
-
/**
- * Gets the user's input from the active input textarea.
- *
- * @returns {string}
+ * Terminates any existing loader workers and sets up a new worker
*/
- get() {
- const textArea = document.getElementById("input-text");
- const value = textArea.value;
- const inputNum = this.getActiveTab();
- if (this.fileBuffers[inputNum] && this.fileBuffers[inputNum].fileBuffer) {
- return this.fileBuffers[inputNum].fileBuffer;
+ setupLoaderWorker() {
+ for (let i = 0; i < this.loaderWorkers.length; i++) {
+ const worker = this.loaderWorkers.pop();
+ worker.terminate();
}
- return value;
+
+ this.addLoaderWorker();
}
/**
- * Gets the inputs from all tabs
+ * Adds a new loaderWorker
*
- * @returns {Array}
+ * @returns {number} The index of the created worker
+ */
+ addLoaderWorker() {
+ for (let i = 0; i < this.loaderWorkers.length; i++) {
+ if (!this.loaderWorkers[i].active) {
+ return i;
+ }
+ }
+ if (this.loaderWorkers.length === this.maxWorkers) {
+ return -1;
+ }
+ log.debug("Adding new LoaderWorker.");
+ const newWorker = new LoaderWorker();
+ newWorker.addEventListener("message", this.handleLoaderMessage.bind(this));
+ const newWorkerObj = {
+ worker: newWorker,
+ active: false,
+ inputNum: 0
+ };
+ this.loaderWorkers.push(newWorkerObj);
+ return this.loaderWorkers.indexOf(newWorkerObj);
+ }
+
+ /**
+ * Removes a loaderworker using inputNum
+ *
+ * @param {Object} workerObj
+ */
+ removeLoaderWorker(workerObj) {
+ const idx = this.loaderWorkers.indexOf(workerObj);
+ if (idx === -1) {
+ return;
+ }
+ this.loaderWorkers[idx].worker.terminate();
+ this.loaderWorkers.splice(idx, 1);
+ if (this.loaderWorkers.length === 0) {
+ // There should always be 1 loaderworker loaded
+ this.addLoaderWorker();
+ }
+ }
+
+ /**
+ * Finds and returns the object for the loaderWorker of a given inputNum
+ *
+ * @param {number} inputNum
+ */
+ getLoaderWorker(inputNum) {
+ for (let i = 0; i < this.loaderWorkers.length; i++) {
+ if (this.loaderWorkers[i].inputNum === inputNum) {
+ return this.loaderWorkers[i];
+ }
+ }
+ }
+
+ /**
+ * Loads a file into the input
+ *
+ * @param {File} file
+ * @param {number} inputNum
+ */
+ loadFile(file, inputNum) {
+ if (file && inputNum) {
+ this.closeFile(this.getLoaderWorker(inputNum));
+ let loaded = false;
+
+ const workerId = this.addLoaderWorker();
+ if (workerId !== -1) {
+ this.loaderWorkers[workerId].active = true;
+ this.loaderWorkers[workerId].inputNum = inputNum;
+ this.loaderWorkers[workerId].worker.postMessage({
+ file: file,
+ inputNum: inputNum
+ });
+ loaded = true;
+ } else {
+ this.pendingFiles.push({
+ file: file,
+ inputNum: inputNum
+ });
+ }
+ if (this.getInput(inputNum) !== null) {
+ this.removeInput(inputNum);
+ }
+ this.inputs.push({
+ inputNum: inputNum,
+ data: {
+ fileBuffer: new ArrayBuffer(),
+ name: file.name,
+ size: file.size.toLocaleString(),
+ type: file.type || "unknown"
+ },
+ status: (loaded) ? "loading" : "pending",
+ progress: 0
+ });
+ }
+ }
+
+ /**
+ * Closes a file and removes it from inputs
+ *
+ * @param {number} inputNum
+ */
+ closeFile(inputNum) {
+ this.removeLoaderWorker(this.getLoaderWorker(inputNum));
+ this.removeInput(inputNum);
+
+ if (inputNum === this.getActiveTab()) {
+ 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 = "";
+
+ const inputText = document.getElementById("input-text");
+ inputText.style.overflow = "auto";
+ inputText.classList.remove("blur");
+ }
+ }
+
+ /**
+ * Remove an input from the input list
+ * @param {number} inputNum
+ */
+ removeInput(inputNum) {
+ for (let i = 0; i < this.inputs.length; i++) {
+ if (this.inputs[i].inputNum === inputNum) {
+ this.inputs.splice(i, 1);
+ }
+ }
+ // if (this.inputs.length === 0) {
+ // this.inputs.push({
+ // inputNum: inputNum,
+ // data: "",
+ // status: "loaded",
+ // progress: 100
+ // });
+ // }
+ }
+
+ /**
+ * Updates the progress value of an input
+ *
+ * @param {number} inputNum
+ * @param {number} progress
+ */
+ updateInputProgress(inputNum, progress) {
+ for (let i = 0; i < this.inputs.length; i++) {
+ if (this.inputs[i].inputNum === inputNum) {
+ // Don't let progress go over 100
+ this.inputs[i].progress = (progress <= 100) ? progress : 100;
+ }
+ }
+ }
+
+ /**
+ * Updates the stored value of an input
+ *
+ * @param {number} inputNum
+ * @param {ArrayBuffer | String} value
+ */
+ updateInputValue(inputNum, value) {
+
+ for (let i = 0; i < this.inputs.length; i++) {
+ if (this.inputs[i].inputNum === inputNum) {
+ if (typeof value === "string") {
+ this.inputs[i].data = value;
+ } else {
+ this.inputs[i].data.fileBuffer = value;
+
+ if (inputNum === this.getActiveTab()) {
+ this.displayFilePreview();
+ }
+ }
+ this.inputs[i].progress = 100;
+ this.inputs[i].status = "loaded";
+ return;
+ }
+ }
+ // If we get to here, an input for inputNum could not be found
+
+ if (typeof value === "string") {
+ this.inputs.push({
+ inputNum: inputNum,
+ data: value,
+ status: "loaded",
+ progress: 100
+ });
+ }
+ }
+
+ /**
+ * Handler for messages sent back by LoaderWorkers
+ *
+ * @param {MessageEvent} else
+ */
+ handleLoaderMessage(e) {
+ const r = e.data;
+ let inputNum = 0;
+
+ if (r.hasOwnProperty("inputNum")) {
+ inputNum = r.inputNum;
+ }
+
+ if (r.hasOwnProperty("progress")) {
+ this.updateInputProgress(inputNum, r.progress);
+ this.setFile(inputNum);
+ // UI here
+ }
+
+ if (r.hasOwnProperty("error")) {
+ this.app.alert(r.error, 10000);
+ }
+
+ if (r.hasOwnProperty("fileBuffer")) {
+ log.debug(`Input file ${inputNum} loaded.`);
+ this.updateInputValue(inputNum, r.fileBuffer);
+
+ this.setLoadingInfo();
+
+ const currentWorker = this.getLoaderWorker(inputNum);
+
+ if (this.pendingFiles.length > 0) {
+ log.debug("Loading file completed. Loading next file.");
+ const nextFile = this.pendingFiles.pop();
+ currentWorker.inputNum = nextFile.inputNum;
+ currentWorker.worker.postMessage({
+ file: nextFile.file,
+ inputNum: nextFile.inputNum
+ });
+
+ } else {
+ // LoaderWorker no longer needed
+ log.debug("Loading file completed. Closing LoaderWorker.");
+ const progress = this.getLoadProgress();
+ if (progress.total === progress.loaded) {
+ window.dispatchEvent(this.manager.statechange);
+ }
+ this.removeLoaderWorker(currentWorker);
+ }
+
+ }
+ }
+
+ /**
+ * Gets the input for the specified input number
+ *
+ * @param {number} inputNum
+ */
+ getInput(inputNum) {
+ const index = this.getInputIndex(inputNum);
+ if (index === -1) {
+ return null;
+ }
+ if (this.inputs[index].inputNum === inputNum) {
+ if (typeof this.inputs[index].data === "string") {
+ return this.inputs[index].data;
+ } else {
+ return this.inputs[index].data.fileBuffer;
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Gets the index of the input in the inputs list
+ *
+ * @param {number} inputNum
+ */
+ getInputIndex(inputNum) {
+ for (let i = 0; i < this.inputs.length; i++) {
+ if (this.inputs[i].inputNum === inputNum) {
+ return i;
+ }
+ }
+ return -1;
+ }
+
+ /**
+ * Gets the input for the active tab
+ */
+ getActive() {
+ const textArea = document.getElementById("input-text");
+ const value = (textArea.value !== undefined) ? textArea.value : "";
+ const inputNum = this.getActiveTab();
+
+ if (this.getInput(inputNum) === null || typeof this.getInput(inputNum) === "string") {
+ this.updateInputValue(inputNum, value);
+ }
+
+ return this.getInput(inputNum);
+
+ }
+
+ /**
+ * Gets the input for all tabs
*/
getAll() {
+ // Need to make sure here that the active input is actually saved in this inputs
+ this.getActive();
const inputs = [];
- const tabsContainer = document.getElementById("input-tabs");
- const tabs = tabsContainer.firstElementChild.children;
- for (let i = 0; i < tabs.length; i++) {
- const inputNum = tabs.item(i).id.replace("input-tab-", "");
- if (this.fileBuffers[inputNum] && this.fileBuffers[inputNum].fileBuffer) {
+ for (let i = 0; i < this.inputs.length; i++) {
+ if (this.inputs[i].status === "loaded") {
inputs.push({
- inputNum: inputNum,
- input: this.fileBuffers[inputNum].fileBuffer
- });
- } else {
- inputs.push({
- inputNum: inputNum,
- input: this.inputs[inputNum] || ""
+ inputNum: this.inputs[i].inputNum,
+ input: this.getInput(this.inputs[i].inputNum) || ""
});
}
}
-
if (inputs.length === 0) {
inputs.push({
- inputNum: this.getActiveTab(),
+ inputNum: 1,
input: ""
});
}
-
return inputs;
}
-
/**
- * Sets the input in the input area.
- *
- * @param {string|File} input
- * @param {boolean} [silent=false] - Suppress statechange event
- *
- * @fires Manager#statechange
+ * Get the progress of the loaderWorkers
*/
- set(input, silent=false) {
- const inputText = document.getElementById("input-text");
- const inputNum = this.getActiveTab();
- if (input instanceof File) {
- this.setFile(input);
- inputText.value = "";
- this.setInputInfo(input.size, null);
- this.displayTabInfo(input);
- } else {
- inputText.value = input;
- this.inputs[inputNum] = input;
- this.closeFile(inputNum);
- if (!silent) window.dispatchEvent(this.manager.statechange);
- const lines = input.length < (this.app.options.ioDisplayThreshold * 1024) ?
- input.count("\n") + 1 : null;
- this.setInputInfo(input.length, lines);
- this.displayTabInfo(input);
+ getLoadProgress() {
+ const totalInputs = this.inputs.length;
+ const pendingInputs = this.pendingFiles.length;
+ let loadingInputs = 0;
+ for (let i = 0; i < this.loaderWorkers.length; i++) {
+ if (this.loaderWorkers[i].active) {
+ loadingInputs += 0;
+ }
}
- }
-
-
- /**
- * Shows file details.
- *
- * @param {File} file
- */
- setFile(file) {
- // Display file overlay in input area with details
- const inputNum = this.getActiveTab();
-
- this.fileBuffers[inputNum] = {
- fileBuffer: new ArrayBuffer(),
- name: file.name,
- size: file.size.toLocaleString(),
- type: file.type || "unknown",
- loaded: "0%"
+ return {
+ total: totalInputs,
+ pending: pendingInputs,
+ loading: loadingInputs,
+ loaded: (totalInputs - pendingInputs - loadingInputs)
};
-
- this.setFileInfo(this.fileBuffers[inputNum]);
}
/**
- * 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;
- 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 += "
lines: " + linesStr;
- }
-
- document.getElementById("input-info").innerHTML = msg;
-
- }
-
- /**
- * Displays information about the input file.
- *
- * @param fileObj
- */
- setFileInfo(fileObj) {
- 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 = fileObj.name;
- fileSize.textContent = fileObj.size + " bytes";
- fileType.textContent = fileObj.type;
- fileLoaded.textContent = fileObj.loaded;
- }
-
-
- /**
- * Handler for input change events.
+ * Handler for input change events
*
* @param {event} e
*
* @fires Manager#statechange
*/
inputChange(e) {
- // Ignore this function if the input is a File
- const inputNum = this.getActiveTab();
- if (this.fileBuffers[inputNum]) return;
+ // Ignore this function if the input is a file
+ const input = this.getActive();
+ if (typeof input !== "string") return;
// Remove highlighting from input and output panes as the offsets might be different now
// this.manager.highlighter.removeHighlights();
@@ -209,13 +427,11 @@ class InputWaiter {
this.app.progress = 0;
// Update the input metadata info
- const inputText = this.get();
- this.inputs[inputNum] = inputText;
- const lines = inputText.length < (this.app.options.ioDisplayThreshold * 1024) ?
- inputText.count("\n") + 1 : null;
+ const lines = input.length < (this.app.options.ioDisplayThreshold * 1024) ?
+ input.count("\n") + 1 : null;
- this.setInputInfo(inputText.length, lines);
- this.displayTabInfo(inputText);
+ this.setInputInfo(input.length, lines);
+ this.displayTabInfo(this.getActiveTab());
if (e && this.badKeys.indexOf(e.keyCode) < 0) {
// Fire the statechange event as the input has been modified
@@ -223,7 +439,6 @@ class InputWaiter {
}
}
-
/**
* Handler for input paste events.
* Checks that the size of the input is below the display limit, otherwise treats it as a file/blob.
@@ -244,15 +459,12 @@ class InputWaiter {
lastModified: Date.now()
});
- this.loaderWorker = new LoaderWorker();
- this.loaderWorker.addEventListener("message", this.handleLoaderMessage.bind(this));
- this.loaderWorker.postMessage({"file": file, "inputNum": this.getActiveTab()});
+ this.loadFile(file, this.getActiveTab());
this.set(file);
return false;
}
}
-
/**
* Handler for input dragover events.
* Gives the user a visual cue to show that items can be dropped here.
@@ -269,7 +481,6 @@ class InputWaiter {
e.target.closest("#input-text,#input-file").classList.add("dropping-file");
}
-
/**
* Handler for input dragleave events.
* Removes the visual cue.
@@ -282,10 +493,9 @@ class InputWaiter {
e.target.closest("#input-text,#input-file").classList.remove("dropping-file");
}
-
/**
* Handler for input drop events.
- * Loads the dragged data into the input textarea.
+ * Loads the dragged data into the input textarea
*
* @param {event} e
*/
@@ -308,13 +518,7 @@ class InputWaiter {
}
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
- for (let i = 0; i < e.dataTransfer.files.length; i++) {
- const file = e.dataTransfer.files[i];
- if (i !== 0) {
- this.addTab();
- }
- this.loadFile(file, this.getActiveTab());
- }
+ this.loadUIFiles(e.dataTransfer.files);
}
}
@@ -327,274 +531,451 @@ class InputWaiter {
inputOpen(e) {
e.preventDefault();
- // TODO : CHANGE THIS TO HANDLE MULTIPLE FILES
- for (let i = 0; i < e.srcElement.files.length; i++) {
- const file = e.srcElement.files[i];
- if (i !== 0) {
- this.addTab();
- }
- this.loadFile(file, this.getActiveTab());
+ if (e.srcElement.files.length > 0) {
+ this.loadUIFiles(e.srcElement.files);
+ e.srcElement.value = "";
}
}
-
/**
- * Handler for messages sent back by the LoaderWorker.
+ * Load files from the UI into the input, creating tabs if needed
*
- * @param {MessageEvent} e
+ * @param files
*/
- handleLoaderMessage(e) {
- const r = e.data;
+ loadUIFiles(files) {
let inputNum;
- const tabNum = this.getActiveTab();
- if (r.hasOwnProperty("inputNum")) {
- inputNum = r.inputNum;
- }
- if (r.hasOwnProperty("progress")) {
- this.fileBuffers[inputNum].loaded = r.progress + "%";
- if (tabNum === inputNum) {
- const fileLoaded = document.getElementById("input-file-loaded");
- fileLoaded.textContent = r.progress + "%";
+ for (let i = 0; i < files.length; i++) {
+ inputNum = this.getActiveTab();
+ if (i > 0) {
+ inputNum = this.addTab(false);
+ }
+ this.loadFile(files[i], inputNum);
+
+ if (inputNum === this.getActiveTab()) {
+ this.setFile(inputNum);
}
}
+ this.changeTab(inputNum);
+ }
- if (r.hasOwnProperty("error")) {
- this.app.alert(r.error, 10000);
- }
+ /**
+ * Sets the input in the input area
+ *
+ * @param {string|File} input
+ * @param {boolean} [silent=false] - Suppress statechange event
+ *
+ * @fires Manager#statechange
+ *
+ */
+ set(input, silent=false) {
+ const inputText = document.getElementById("input-text");
+ const inputNum = this.getActiveTab();
+ if (input instanceof File) {
+ this.setFile(inputNum);
+ inputText.value = "";
+ this.setInputInfo(input.size, null);
+ this.displayTabInfo(inputNum);
+ } else {
+ inputText.value = input;
+ this.updateInputValue(inputNum, input);
+ this.closeFile(inputNum);
- if (r.hasOwnProperty("fileBuffer")) {
- log.debug("Input file loaded");
- this.fileBuffers[inputNum].fileBuffer = r.fileBuffer;
- this.displayFilePreview();
- window.dispatchEvent(this.manager.statechange);
+ if (!silent) window.dispatchEvent(this.manager.statechange);
+
+ const lines = input.length < (this.app.options.ioDisplayThreshold * 1024) ?
+ input.count("\n") + 1 : null;
+ this.setInputInfo(input.length, lines);
+ this.displayTabInfo(inputNum);
}
}
+ /**
+ * Shows file details
+ *
+ * @param {number} inputNum
+ */
+ setFile(inputNum) {
+ if (inputNum === this.getActiveTab()) {
+ for (let i = 0; i < this.inputs.length; i++) {
+ if (this.inputs[i].inputNum === inputNum && typeof this.inputs[i].data !== "string") {
+ 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"),
+ fileObj = this.inputs[i];
+ fileOverlay.style.display = "block";
+ fileName.textContent = fileObj.data.name;
+ fileSize.textContent = fileObj.data.size + " bytes";
+ fileType.textContent = fileObj.data.type;
+ fileLoaded.textContent = fileObj.progress + "%";
+
+ this.setInputInfo(fileObj.data.size, null);
+ this.displayFilePreview();
+ }
+ }
+ }
+ this.displayTabInfo(inputNum);
+ }
+
+ /**
+ * 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) {
+ // This should also update the tab?
+ let width = length.toString().length;
+ 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 += "
lines: " + linesStr;
+ }
+
+ document.getElementById("input-info").innerHTML = msg;
+
+ }
+
+ /**
+ * Display progress information for file loading in header
+ */
+ setLoadingInfo() {
+ const progress = this.getLoadProgress();
+ let width = progress.total.toString().length;
+ width = width < 2 ? 2 : width;
+
+ const totalStr = progress.total.toString().padStart(width, " ").replace(/ /g, " ");
+ let msg = "Total: " + totalStr;
+
+ const loadedStr = progress.loaded.toString().padStart(width, " ").replace(/ /g, " ");
+ msg += "
Loaded: " + loadedStr;
+
+ if (progress.pending > 0) {
+ const pendingStr = progress.pending.toString().padStart(width, " ").replace(/ /g, " ");
+ msg += "
Pending: " + pendingStr;
+
+ }
+
+ document.getElementById("input-files-info").innerHTML = msg;
+ }
+
+ /**
+ * Display input information in the tab header
+ *
+ * @param {number} inputNum
+ */
+ displayTabInfo(inputNum) {
+ const tabItem = this.getTabItem(inputNum);
+ const input = this.inputs[this.getInputIndex(inputNum)];
+ if (!tabItem) {
+ return;
+ }
+
+ const tabContent = tabItem.firstElementChild;
+ if (typeof input.data !== "string") {
+ tabContent.innerText = `${inputNum}: ${input.data.name}`;
+ } else {
+ if (input.data.length > 0) {
+ const inputText = input.data.slice(0, 100).split(/[\r\n]/)[0];
+ tabContent.innerText = `${inputNum}: ${inputText}`;
+ } else {
+ tabContent.innerText = `${inputNum}: New Tab`;
+ }
+ }
+ }
/**
* Shows a chunk of the file in the input behind the file overlay.
*/
displayFilePreview() {
- const inputNum = this.getActiveTab();
- const inputText = document.getElementById("input-text"),
- fileSlice = this.fileBuffers[inputNum].fileBuffer.slice(0, 4096);
+ const inputNum = this.getActiveTab(),
+ inputText = document.getElementById("input-text"),
+ fileSlice = this.getInput(inputNum).slice(0, 4096);
inputText.style.overflow = "hidden";
inputText.classList.add("blur");
inputText.value = Utils.printable(Utils.arrayBufferToStr(fileSlice));
- if (this.fileBuffers[inputNum].fileBuffer.byteLength > 4096) {
+ if (this.getInput(inputNum).byteLength > 4096) {
inputText.value += "[truncated]...";
}
}
-
/**
- * Handler for file close events.
- */
- closeFile(inputNum) {
- if (this.loaderWorkers[inputNum]) this.loaderWorkers[inputNum].terminate();
- delete this.fileBuffers[inputNum];
- document.getElementById("input-file").style.display = "none";
- const inputText = document.getElementById("input-text");
- inputText.style.overflow = "auto";
- inputText.classList.remove("blur");
- }
-
-
- /**
- * Loads a file into the input.
+ * Create a tab element for the input tab bar
*
- * @param {File} file
- * @param {string} inputNum
+ * @param {number} inputNum
+ * @returns {Element}
*/
- loadFile(file, inputNum) {
- if (file && inputNum) {
- this.closeFile(inputNum);
- this.loaderWorkers[inputNum] = new LoaderWorker();
- this.loaderWorkers[inputNum].addEventListener("message", this.handleLoaderMessage.bind(this));
- this.loaderWorkers[inputNum].postMessage({"file": file, "inputNum": inputNum});
- this.set(file);
- }
- }
-
- /**
- * Handler for clear IO events.
- * Resets the input, output and info areas.
- *
- * @fires Manager#statechange
- */
- clearIoClick() {
- this.closeFile(this.getActiveTab());
- this.manager.output.closeFile();
- this.manager.highlighter.removeHighlights();
- document.getElementById("input-text").value = "";
- document.getElementById("output-text").value = "";
- document.getElementById("input-info").innerHTML = "";
- document.getElementById("output-info").innerHTML = "";
- document.getElementById("input-selection-info").innerHTML = "";
- document.getElementById("output-selection-info").innerHTML = "";
- window.dispatchEvent(this.manager.statechange);
- }
-
- /**
- * Handler for clear all IO events.
- * Resets the input, output and info areas.
- *
- * @fires Manager#statechange
- */
- clearAllIoClick() {
- const tabs = document.getElementById("input-tabs").getElementsByTagName("li");
- for (let i = tabs.length - 1; i >= 0; i--) {
- const tabItem = tabs.item(i);
- this.closeFile(this.getActiveTab(tabItem.id.replace("input-tab-", "")));
- this.removeTab(tabItem);
- }
- this.manager.output.closeFile();
- // this.manager.highlighter.removeHighlights();
- const inputNum = this.getActiveTab();
- document.getElementById(`input-tab-${inputNum}`).firstElementChild.innerText = `${inputNum}: New Tab`;
- document.getElementById("input-text").value = "";
- document.getElementById("output-text").value = "";
- document.getElementById("input-info").innerHTML = "";
- document.getElementById("output-info").innerHTML = "";
- document.getElementById("input-selection-info").innerHTML = "";
- document.getElementById("output-selection-info").innerHTML = "";
- window.dispatchEvent(this.manager.statechange);
- }
-
- /**
- * Function to create a new tab
- *
- * @param {boolean} changeTab
- */
- addTab(changeTab = true) {
- const tabWrapper = document.getElementById("input-tabs");
- const tabsList = tabWrapper.children[0];
- const lastTabNum = tabsList.lastElementChild.id.replace("input-tab-", "");
- const newTabNum = parseInt(lastTabNum, 10) + 1;
-
- tabWrapper.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))";
-
- this.inputs[newTabNum.toString()] = "";
-
+ createTabElement(inputNum) {
const newTab = document.createElement("li");
- newTab.id = `input-tab-${newTabNum}`;
+ newTab.setAttribute("inputNum", inputNum.toString());
const newTabContent = document.createElement("div");
newTabContent.classList.add("input-tab-content");
- newTabContent.innerText = `${newTabNum}: New Tab`;
+ newTabContent.innerText = `${inputNum.toString()}: New Tab`;
- const newTabCloseBtn = document.createElement("button");
- newTabCloseBtn.className = "btn btn-primary bmd-btn-icon btn-close-tab";
- newTabCloseBtn.id = `btn-close-tab-${newTabNum}`;
+ const newTabButton = document.createElement("button");
+ newTabButton.type = "button";
+ newTabButton.className = "btn btn-primary bmd-btn-icon btn-close-tab";
- const newTabCloseBtnIcon = document.createElement("i");
- newTabCloseBtnIcon.classList.add("material-icons");
- newTabCloseBtnIcon.innerText = "clear";
+ const newTabButtonIcon = document.createElement("i");
+ newTabButtonIcon.classList.add("material-icons");
+ newTabButtonIcon.innerText = "clear";
+
+ newTabButton.appendChild(newTabButtonIcon);
- newTabCloseBtn.appendChild(newTabCloseBtnIcon);
newTab.appendChild(newTabContent);
- newTab.appendChild(newTabCloseBtn);
+ newTab.appendChild(newTabButton);
- tabsList.appendChild(newTab);
+ return newTab;
+ }
- this.manager.output.addTab(newTabNum.toString());
- this.manager.output.changeTab(document.getElementById(`output-tab-${newTabNum.toString()}`).firstElementChild);
+ /**
+ * Adds a new input to inputs.
+ * Will create a new tab if there's less than maxtabs visible.
+ *
+ * @param {boolean} [changeTab=true]
+ */
+ addTab(changeTab = true) {
+ let inputNum;
+ if (this.inputs.length === 0) {
+ inputNum = 1;
+ } else {
+ inputNum = this.getLargestInputNum() + 1;
+ }
+
+ this.inputs.push({
+ inputNum: inputNum,
+ data: "",
+ status: "loaded",
+ progress: 100
+ });
+
+ const tabsWrapper = document.getElementById("input-tabs");
+ const numTabs = tabsWrapper.firstElementChild.children.length;
+
+ if (numTabs < this.maxTabs) {
+ // Create a tab element
+ const newTab = this.createTabElement(inputNum);
+
+ tabsWrapper.firstElementChild.appendChild(newTab);
+
+ if (numTabs > 0) {
+ tabsWrapper.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))";
+ }
+
+ }
if (changeTab) {
- this.changeTab(newTabContent);
+ this.changeTab(inputNum);
+ }
+
+ return inputNum;
+
+ }
+
+ /**
+ * Removes a tab and it's corresponding input
+ *
+ * @param {number} inputNum
+ */
+ removeTab(inputNum) {
+ const inputIdx = this.getInputIndex(inputNum);
+ let activeTab = this.getActiveTab();
+ if (inputIdx === -1) {
+ return;
+ }
+
+ const tabElement = this.getTabItem(inputNum);
+
+ this.removeInput(inputNum);
+
+ if (tabElement !== null) {
+ if (inputNum === activeTab) {
+ activeTab = this.getPreviousInputNum(activeTab);
+ if (activeTab === this.getActiveTab()) {
+ activeTab = this.getNextInputNum(activeTab);
+ }
+ }
+ this.refreshTabs(activeTab);
}
}
/**
- * Function to remove a tab
- *
- * @param {Element} tabLiItem
+ * Redraw the entire tab bar to remove any outdated tabs
+ * @param {number} activeTab
*/
- removeTab(tabLiItem) {
- const tabList= tabLiItem.parentElement;
- if (tabList.children.length > 1) {
- if (tabLiItem.classList.contains("active-input-tab")) {
- if (tabLiItem.previousElementSibling) {
- this.changeTab(tabLiItem.previousElementSibling.firstElementChild);
- } else if (tabLiItem.nextElementSibling) {
- this.changeTab(tabLiItem.nextElementSibling.firstElementChild);
- }
- }
- const tabNum = tabLiItem.id.replace("input-tab-", "");
-
- this.fileBuffers[tabNum] = undefined;
- this.inputs[tabNum] = undefined;
-
- tabList.removeChild(tabLiItem);
-
- this.manager.output.removeTab(tabNum, this.getActiveTab());
- } else {
- const tabNum = tabLiItem.id.replace("input-tab-", "");
- delete this.fileBuffers[tabNum];
- this.inputs[tabNum] = "";
- document.getElementById("input-text").value = "";
- tabLiItem.firstElementChild.innerText = `${tabNum}: New Tab`;
+ refreshTabs(activeTab) {
+ const tabsWrapper = document.getElementById("input-tabs");
+ const tabsList = tabsWrapper.firstElementChild;
+ let newInputs = this.getNearbyNums(activeTab, "right");
+ if (newInputs.length < this.maxTabs) {
+ newInputs = this.getNearbyNums(activeTab, "left");
}
- if (tabList.children.length === 1) {
- document.getElementById("input-tabs").style.display = "none";
+
+ 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]));
+ }
+
+ if (newInputs.length > 1) {
+ tabsWrapper.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.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))";
-
}
+
+ if (newInputs.length === 0) {
+ activeTab = this.addTab();
+ }
+
+ this.changeTab(activeTab);
}
/**
- * Handler for removing an input tab
- *
+ * Handler for remove tab button click
* @param {event} mouseEvent
*/
removeTabClick(mouseEvent) {
if (!mouseEvent.srcElement) {
return;
}
- this.removeTab(mouseEvent.srcElement.parentElement.parentElement);
-
+ const tabNum = mouseEvent.srcElement.parentElement.parentElement.getAttribute("inputNum");
+ if (tabNum) {
+ this.removeTab(parseInt(tabNum, 10));
+ }
}
/**
- * Change the active tab to tabElement
+ * Generates a list of the nearby inputNums
*
- * @param {Element} tabElement The tab element to change to
+ * @param {number} inputNum
+ * @param {string} direction
*/
- changeTab(tabElement, changeOutput=true) {
- const liItem = tabElement.parentElement;
- const newTabNum = liItem.id.replace("input-tab-", "");
- const currentTabNum = this.getActiveTab();
- const inputText = document.getElementById("input-text");
-
- document.getElementById(`input-tab-${currentTabNum}`).classList.remove("active-input-tab");
- liItem.classList.add("active-input-tab");
-
- this.inputs[currentTabNum] = inputText.value;
-
- if (this.fileBuffers[newTabNum]) {
- const fileObj = this.fileBuffers[newTabNum];
- this.setInputInfo(fileObj.size, null);
- this.setFileInfo(fileObj);
- this.displayFilePreview();
+ getNearbyNums(inputNum, direction) {
+ const inputs = [];
+ 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(inputs[i-1]);
+ }
+ if (newNum === inputs[i-1]) {
+ reachedEnd = true;
+ inputs.sort(function(a, b) {
+ return b - a;
+ });
+ }
+ if (reachedEnd) {
+ newNum = this.getPreviousInputNum(inputs[i-1]);
+ }
+ if (newNum >= 0) {
+ inputs.push(newNum);
+ }
+ }
} else {
- inputText.value = this.inputs[newTabNum];
- inputText.style.overflow = "auto";
- inputText.classList.remove("blur");
+ let reachedEnd = false;
+ for (let i = 0; i < this.maxTabs; i++) {
+ let newNum;
+ if (i === 0) {
+ newNum = inputNum;
+ } else {
+ if (!reachedEnd) {
+ newNum = this.getPreviousInputNum(inputs[i-1]);
+ }
+ if (newNum === inputs[i-1]) {
+ reachedEnd = true;
+ inputs.sort(function(a, b) {
+ return b - a;
+ });
+ }
+ if (reachedEnd) {
+ newNum = this.getNextInputNum(inputs[i-1]);
+ }
+ }
+ if (newNum >= 0) {
+ inputs.push(newNum);
+ }
+ }
+ }
+ inputs.sort(function(a, b) {
+ return a - b;
+ });
+ return inputs;
+ }
- this.inputChange(null);
- document.getElementById("input-file").style.display = "none";
+ /**
+ * Changes the active tab
+ *
+ * @param {number} inputNum
+ */
+ changeTab(inputNum) {
+ const inputIdx = this.getInputIndex(inputNum);
+ let currentIdx = -1;
+ try {
+ currentIdx = this.getActiveTab();
+ } catch (err) {}
+ if (inputIdx === -1) {
+ return;
}
- if (changeOutput) {
- this.manager.output.changeTab(document.getElementById(`output-tab-${newTabNum.toString()}`).firstElementChild);
+ const tabsWrapper = document.getElementById("input-tabs");
+ const tabs = tabsWrapper.firstElementChild.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-input-tab");
+ found = true;
+ } else {
+ tabs.item(i).classList.remove("active-input-tab");
+ }
+ }
+ if (!found) {
+ // Shift the tabs here
+ let direction = "right";
+ if (currentIdx > inputIdx) {
+ direction = "left";
+ }
+
+ const newInputs = this.getNearbyNums(inputNum, direction);
+
+ for (let i = 0; i < newInputs.length; i++) {
+ tabs.item(i).setAttribute("inputNum", newInputs[i].toString());
+ this.displayTabInfo(newInputs[i]);
+ if (newInputs[i] === inputNum) {
+ tabs.item(i).classList.add("active-input-tab");
+ }
+ }
+ }
+
+ const input = this.getInput(inputNum);
+ if (typeof input === "string") {
+ this.set(this.getInput(inputNum));
+ } else {
+ this.setFile(inputNum);
}
}
@@ -608,48 +989,154 @@ class InputWaiter {
if (!mouseEvent.srcElement) {
return;
}
- this.changeTab(mouseEvent.srcElement, true);
-
+ const tabNum = mouseEvent.srcElement.parentElement.getAttribute("inputNum");
+ if (tabNum) {
+ this.changeTab(parseInt(tabNum, 10));
+ }
}
/**
- * Display input information in the tab header
- *
- * @param {string|File} input
+ * Handler for changing to the left tab
*/
- displayTabInfo(input) {
- const tabNum = this.getActiveTab();
- const activeTab = document.getElementById(`input-tab-${tabNum}`);
- const activeTabContent = activeTab.firstElementChild;
- if (input instanceof File) {
- activeTabContent.innerText = `${tabNum}: ${input.name}`;
+ changeTabLeft() {
+ const currentTab = this.getActiveTab();
+ const currentInput = this.getInputIndex(currentTab);
+ if (currentInput > 0) {
+ this.changeTab(this.getPreviousInputNum(currentTab));
} else {
- if (input.length > 0) {
- const inputText = input.slice(0, 100).split(/[\r\n]/)[0];
- activeTabContent.innerText = `${tabNum}: ${inputText}`;
- } else {
- activeTabContent.innerText = `${tabNum}: New Tab`;
+ this.changeTab(this.inputs[0].inputNum);
+ }
+ }
+
+ /**
+ * Handler for changing to the right tab
+ */
+ changeTabRight() {
+ const currentTab = this.getActiveTab();
+ this.changeTab(this.getNextInputNum(currentTab));
+ }
+
+ /**
+ * Handler for go to tab button clicked
+ */
+ goToTab() {
+ const tabNum = parseInt(window.prompt("Enter tab number:", this.getActiveTab().toString()), 10);
+ if (this.getInputIndex(tabNum)) {
+ this.changeTab(tabNum);
+ }
+ }
+
+ /**
+ * Gets the largest inputNum
+ *
+ * @returns {number}
+ */
+ getLargestInputNum() {
+ let largest = 0;
+ for (let i = 0; i < this.inputs.length; i++) {
+ if (this.inputs[i].inputNum > largest) {
+ largest = this.inputs[i].inputNum;
}
}
+ return largest;
+ }
+ /**
+ * Gets the previous inputNum
+ *
+ * @param {number} inputNum - The current input number
+ * @returns {number}
+ */
+ getPreviousInputNum(inputNum) {
+ let num = -1;
+ for (let i = 0; i < this.inputs.length; i++) {
+ if (this.inputs[i].inputNum < inputNum) {
+ if (this.inputs[i].inputNum > num) {
+ num = this.inputs[i].inputNum;
+ }
+ }
+ }
+ return num;
+ }
+
+ /**
+ * Gets the next inputNum
+ *
+ * @param {number} inputNum - The current input number
+ * @returns {number}
+ */
+ getNextInputNum(inputNum) {
+ let num = this.getLargestInputNum();
+ for (let i = 0; i < this.inputs.length; i++) {
+ if (this.inputs[i].inputNum > inputNum) {
+ if (this.inputs[i].inputNum < num) {
+ num = this.inputs[i].inputNum;
+ }
+ }
+ }
+ return num;
}
/**
* Gets the number of the current active tab
*
- * @returns {string}
+ * @returns {number}
*/
getActiveTab() {
const activeTabs = document.getElementsByClassName("active-input-tab");
if (activeTabs.length > 0) {
const activeTab = activeTabs.item(0);
- const activeTabNum = activeTab.id.replace("input-tab-", "");
- return activeTabNum;
+ const tabNum = activeTab.getAttribute("inputNum");
+ return parseInt(tabNum, 10);
} else {
- throw "Could not find an active tab";
+ 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").firstElementChild.children;
+ for (let i = 0; i < tabs.length; i++) {
+ if (parseInt(tabs.item(i).getAttribute("inputNum"), 10) === inputNum) {
+ return tabs.item(i);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * Handler for clear all IO events.
+ * Resets the input, output and info areas
+ */
+ clearAllIoClick() {
+ for (let i = this.inputs.length - 1; i >= 0; i--) {
+ this.removeInput(this.inputs[i].inputNum);
+ }
+ this.refreshTabs();
+ }
+
+ /**
+ * Handler for clear IO click event.
+ * Resets the input for the current tab
+ */
+ clearIoClick() {
+ const inputNum = this.getActiveTab();
+ this.removeInput(inputNum);
+
+ this.inputs.push({
+ inputNum: inputNum,
+ data: "",
+ status: "loaded",
+ progress: 0
+ });
+
+ this.set("");
+ }
}
export default InputWaiter;
diff --git a/src/web/Manager.mjs b/src/web/Manager.mjs
index 8bc7f76d..90525b88 100755
--- a/src/web/Manager.mjs
+++ b/src/web/Manager.mjs
@@ -82,6 +82,8 @@ class Manager {
* Sets up the various components and listeners.
*/
setup() {
+ this.input.addTab();
+ this.input.setupLoaderWorker();
this.worker.setupChefWorkers();
this.recipe.initialiseOperationDragNDrop();
this.controls.initComponents();
@@ -156,6 +158,9 @@ class Manager {
// this.addMultiEventListener("#input-text", "mousedown dblclick select", this.highlighter.inputMousedown, this.highlighter);
document.querySelector("#input-file .close").addEventListener("click", this.input.clearIoClick.bind(this.input));
document.getElementById("btn-new-tab").addEventListener("click", this.input.addTab.bind(this.input));
+ document.getElementById("btn-previous-tab").addEventListener("click", this.input.changeTabLeft.bind(this.input));
+ document.getElementById("btn-next-tab").addEventListener("click", this.input.changeTabRight.bind(this.input));
+ document.getElementById("btn-go-to-tab").addEventListener("click", this.input.goToTab.bind(this.input));
this.addDynamicListener("#input-tabs ul li .btn-close-tab i", "click", this.input.removeTabClick, this.input);
this.addDynamicListener("#input-tabs ul li .input-tab-content", "click", this.input.changeTabClick, this.input);
@@ -177,7 +182,7 @@ class Manager {
this.addDynamicListener("#output-file-slice i", "click", this.output.displayFileSlice, this.output);
document.getElementById("show-file-overlay").addEventListener("click", this.output.showFileOverlayClick.bind(this.output));
this.addDynamicListener(".extract-file,.extract-file i", "click", this.output.extractFileClick, this.output);
- this.addDynamicListener("#output-tabs ul li .output-tab-content", "click", this.output.changeTabClick, this.output);
+ // this.addDynamicListener("#output-tabs ul li .output-tab-content", "click", this.output.changeTabClick, this.output);
// Options
document.getElementById("options").addEventListener("click", this.options.optionsClick.bind(this.options));
diff --git a/src/web/WorkerWaiter.mjs b/src/web/WorkerWaiter.mjs
index 4f8b7e06..a219abe4 100644
--- a/src/web/WorkerWaiter.mjs
+++ b/src/web/WorkerWaiter.mjs
@@ -72,7 +72,7 @@ class WorkerWaiter {
inputNum: r.data.inputNum
});
if (this.pendingInputs.length > 0) {
- log.debug("Bake complete. Baking next input");
+ log.debug(`Bake ${r.data.inputNum} complete. Baking next input`);
this.bakeNextInput(r.data.inputNum);
} else if (this.runningWorkers <= 0) {
this.runningWorkers = 0;
diff --git a/src/web/html/index.html b/src/web/html/index.html
index 75ad9d20..20b1ec3b 100755
--- a/src/web/html/index.html
+++ b/src/web/html/index.html
@@ -229,6 +229,15 @@