/** * @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 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.loaderWorkers = []; this.maxWorkers = navigator.hardwareConcurrency || 4; this.inputs = []; this.pendingFiles = []; this.maxTabs = 4; // Calculate this } /** * Calculates the maximum number of tabs to display */ calcMaxTabs() { const numTabs = Math.floor((document.getElementById("IO").offsetWidth - 75) / 120); this.maxTabs = numTabs; } /** * Terminates any existing loader workers and sets up a new worker */ setupLoaderWorker() { for (let i = 0; i < this.loaderWorkers.length; i++) { const worker = this.loaderWorkers.pop(); worker.terminate(); } this.addLoaderWorker(); } /** * Adds a new loaderWorker * * @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 * * @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)); 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"), fileThumb = document.getElementById("input-file-thumbnail"); inputText.style.overflow = "auto"; inputText.classList.remove("blur"); fileThumb.src = require("./static/images/file-128x128.png"); } } /** * 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); } } } /** * 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} e */ 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[0]; this.pendingFiles.splice(0, 1); 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 (typeof this.inputs[index].data === "string") { return this.inputs[index].data; } else { return this.inputs[index].data.fileBuffer; } } /** * 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(); const input = this.getInput(inputNum); if (input === null || typeof input === "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 = []; for (let i = 0; i < this.inputs.length; i++) { if (this.inputs[i].status === "loaded") { inputs.push({ inputNum: this.inputs[i].inputNum, input: this.getInput(this.inputs[i].inputNum) || "" }); } } if (inputs.length === 0) { inputs.push({ inputNum: 1, input: "" }); } return inputs; } /** * Get the progress of the loaderWorkers */ 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; } } return { total: totalInputs, pending: pendingInputs, loading: loadingInputs, loaded: (totalInputs - pendingInputs - loadingInputs) }; } /** * Handler for input change events * * @param {event} e * * @fires Manager#statechange */ inputChange(e) { // 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(); // Reset recipe progress as any previous processing will be redundant now this.app.progress = 0; // Update the input metadata info const lines = input.length < (this.app.options.ioDisplayThreshold * 1024) ? input.count("\n") + 1 : null; 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 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)) { this.inputChange(e); } else { e.preventDefault(); e.stopPropagation(); const file = new File([pastedData], "PastedData", { type: "text/plain", lastModified: Date.now() }); 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. * * @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 into the input textarea * * @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) { this.closeFile(this.getActiveTab()); this.set(text); 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 input, creating tabs if needed * * @param files */ loadUIFiles(files) { let inputNum; if (files.length > 20) { this.manager.controls.setAutoBake(false); this.app.alert("Auto-Bake is disabled by default when inputting more than 20 files.", 5000); } 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, this.app.options.syncTabs); } /** * 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 (!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 index = this.getInputIndex(inputNum); if (index === -1) return; const input = this.inputs[index]; 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(), input = this.getInput(inputNum), inputText = document.getElementById("input-text"), fileSlice = input.slice(0, 4096), fileThumb = document.getElementById("input-file-thumbnail"), arrBuffer = new Uint8Array(input), type = isImage(arrBuffer); if (type && type !== "image/tiff" && this.app.options.imagePreview && input.byteLength < 1024000) { // Don't show TIFFs as not much supports them fileThumb.src = `data:${type};base64,${toBase64(arrBuffer)}`; } else { fileThumb.src = require("./static/images/file-128x128.png"); } inputText.style.overflow = "hidden"; inputText.classList.add("blur"); inputText.value = Utils.printable(Utils.arrayBufferToStr(fileSlice)); if (this.getInput(inputNum).byteLength > 4096) { inputText.value += "[truncated]..."; } } /** * Create a tab element for the input tab bar * * @param {number} inputNum * @returns {Element} */ createTabElement(inputNum) { const newTab = document.createElement("li"); newTab.setAttribute("inputNum", inputNum.toString()); 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; } /** * 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 }); this.manager.output.addOutput(inputNum, changeTab); const tabsWrapper = document.getElementById("input-tabs"); const numTabs = tabsWrapper.children.length; if (numTabs < this.maxTabs) { // Create a tab element const newTab = this.createTabElement(inputNum); 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))"; } } if (changeTab) { 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); } this.manager.output.removeTab(inputNum); } /** * Redraw the entire tab bar to remove any outdated tabs * @param {number} activeTab */ refreshTabs(activeTab) { const tabsList = document.getElementById("input-tabs"); let newInputs = this.getNearbyNums(activeTab, "right"); if (newInputs.length < this.maxTabs) { newInputs = this.getNearbyNums(activeTab, "left"); } for (let i = tabsList.children.length - 1; i >= 0; i--) { tabsList.children.item(i).remove(); } for (let i = 0; i < newInputs.length; i++) { tabsList.appendChild(this.createTabElement(newInputs[i])); this.displayTabInfo(newInputs[i]); } if (newInputs.length > 1) { tabsList.parentElement.style.display = "block"; document.getElementById("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))"; } if (newInputs.length === 0) { activeTab = this.addTab(); this.displayTabInfo(activeTab); } this.changeTab(activeTab); this.manager.output.refreshTabs(activeTab); // MAKE THE OUTPUT REFRESH TOO } /** * Handler for remove tab button click * @param {event} mouseEvent */ removeTabClick(mouseEvent) { if (!mouseEvent.target) { return; } const tabNum = mouseEvent.target.parentElement.parentElement.getAttribute("inputNum"); if (tabNum) { this.removeTab(parseInt(tabNum, 10)); } } /** * Generates a list of the nearby inputNums * @param inputNum * @param direction */ getNearbyNums(inputNum, direction) { const nums = []; for (let i = 0; i < this.maxTabs; i++) { let newNum; if (i === 0) { newNum = inputNum; } else { switch (direction) { case "left": newNum = this.getNextInputNum(nums[i - 1]); if (newNum === nums[i - 1]) { direction = "right"; newNum = this.getPreviousInputNum(nums[i - 1]); } break; case "right": newNum = this.getPreviousInputNum(nums[i - 1]); if (newNum === nums[i - 1]) { direction = "left"; newNum = this.getNextInputNum(nums[i - 1]); } } } if (!nums.includes(newNum) && (newNum > 0)) { nums.push(newNum); } } nums.sort(function(a, b) { return a - b; }); return nums; } /** * Changes the active tab * * @param {number} inputNum * @param {boolean} [changeOutput=false] */ changeTab(inputNum, changeOutput = false) { const currentNum = this.getActiveTab(); if (this.getInputIndex(inputNum) === -1) return; const tabsWrapper = document.getElementById("input-tabs"); const tabs = tabsWrapper.children; let found = false; for (let i = 0; i < tabs.length; i++) { if (tabs.item(i).getAttribute("inputNum") === inputNum.toString()) { tabs.item(i).classList.add("active-input-tab"); found = true; } else { tabs.item(i).classList.remove("active-input-tab"); } } if (!found) { // Shift the tabs here let direction = "right"; if (currentNum > inputNum) { 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), true); } else { this.setFile(inputNum); } if (changeOutput) { this.manager.output.changeTab(inputNum, false); } } /** * Handler for changing tabs event * * @param {event} mouseEvent */ changeTabClick(mouseEvent) { if (!mouseEvent.target) { return; } const tabNum = mouseEvent.target.parentElement.getAttribute("inputNum"); if (tabNum) { this.changeTab(parseInt(tabNum, 10), this.app.options.syncTabs); } } /** * Handler for changing to the left tab */ changeTabLeft() { const currentTab = this.getActiveTab(); const currentInput = this.getInputIndex(currentTab); if (currentInput > 0) { this.changeTab(this.getPreviousInputNum(currentTab), this.app.options.syncTabs); } else { this.changeTab(this.inputs[0].inputNum, this.app.options.syncTabs); } } /** * Handler for changing to the right tab */ changeTabRight() { const currentTab = this.getActiveTab(); this.changeTab(this.getNextInputNum(currentTab), this.app.options.syncTabs); } /** * Handler for go to tab button clicked */ goToTab() { const tabNum = parseInt(window.prompt("Enter tab number:", this.getActiveTab().toString()), 10); if (this.getInputIndex(tabNum) >= 0) { this.changeTab(tabNum, this.app.options.syncTabs); } } /** * 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 smallest inputNum * * @returns {number} */ getSmallestInputNum() { let smallest = this.getLargestInputNum(); for (let i = 0; i < this.inputs.length; i++) { if (this.inputs[i].inputNum < smallest) { smallest = this.inputs[i].inputNum; } } return smallest; } /** * 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 {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; } /** * Handler for clear all IO events. * Resets the input, output and info areas */ clearAllIoClick() { this.manager.worker.cancelBake(); for (let i = this.inputs.length - 1; i >= 0; i--) { this.manager.output.removeOutput(this.inputs[i].inputNum); 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;