/** * @author n1474335 [n1474335@gmail.com] * @author j433866 [j433866@gmail.com] * @copyright Crown Copyright 2016 * @license Apache-2.0 */ import LoaderWorker from "worker-loader?inline&fallback=false!../workers/LoaderWorker.js"; import InputWorker from "worker-loader?inline&fallback=false!../workers/InputWorker.mjs"; import Utils, { debounce } from "../../core/Utils.mjs"; import { toBase64 } from "../../core/lib/Base64.mjs"; import { isImage } from "../../core/lib/FileType.mjs"; /** * Waiter to handle events related to the input. */ class InputWaiter { /** * InputWaiter constructor. * * @param {App} app - The main view object for CyberChef. * @param {Manager} manager - The CyberChef event manager. */ constructor(app, manager) { this.app = app; this.manager = manager; // Define keys that don't change the input so we don't have to autobake when they are pressed this.badKeys = [ 16, //Shift 17, //Ctrl 18, //Alt 19, //Pause 20, //Caps 27, //Esc 33, 34, 35, 36, //PgUp, PgDn, End, Home 37, 38, 39, 40, //Directional 44, //PrntScrn 91, 92, //Win 93, //Context 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, //F1-12 144, //Num 145, //Scroll ]; this.inputWorker = null; this.loaderWorkers = []; this.workerId = 0; this.maxTabs = this.manager.tabs.calcMaxTabs(); this.callbacks = {}; this.callbackID = 0; this.maxWorkers = 1; if (navigator.hardwareConcurrency !== undefined && navigator.hardwareConcurrency > 1) { // Subtract 1 from hardwareConcurrency value to avoid using // the entire available resources this.maxWorkers = navigator.hardwareConcurrency - 1; } } /** * Calculates the maximum number of tabs to display */ calcMaxTabs() { const numTabs = this.manager.tabs.calcMaxTabs(); if (this.inputWorker && this.maxTabs !== numTabs) { this.maxTabs = numTabs; this.inputWorker.postMessage({ action: "updateMaxTabs", data: { maxTabs: numTabs, activeTab: this.manager.tabs.getActiveInputTab() } }); } } /** * Terminates any existing workers and sets up a new InputWorker and LoaderWorker */ setupInputWorker() { if (this.inputWorker !== null) { this.inputWorker.terminate(); this.inputWorker = null; } for (let i = this.loaderWorkers.length - 1; i >= 0; i--) { this.removeLoaderWorker(this.loaderWorkers[i]); } log.debug("Adding new InputWorker"); this.inputWorker = new InputWorker(); this.inputWorker.postMessage({ action: "updateMaxWorkers", data: this.maxWorkers }); this.inputWorker.postMessage({ action: "updateMaxTabs", data: { maxTabs: this.maxTabs, activeTab: this.manager.tabs.getActiveInputTab() } }); this.inputWorker.postMessage({ action: "setLogLevel", data: log.getLevel() }); this.inputWorker.addEventListener("message", this.handleInputWorkerMessage.bind(this)); } /** * Activates a loaderWorker and sends it to the InputWorker */ activateLoaderWorker() { const workerIdx = this.addLoaderWorker(); if (workerIdx === -1) return; const workerObj = this.loaderWorkers[workerIdx]; this.inputWorker.postMessage({ action: "loaderWorkerReady", data: { id: workerObj.id } }); } /** * Adds a new loaderWorker * * @returns {number} - The index of the created worker */ addLoaderWorker() { if (this.loaderWorkers.length === this.maxWorkers) { return -1; } log.debug("Adding new LoaderWorker."); const newWorker = new LoaderWorker(); const workerId = this.workerId++; newWorker.addEventListener("message", this.handleLoaderMessage.bind(this)); newWorker.postMessage({id: workerId}); const newWorkerObj = { worker: newWorker, id: workerId }; this.loaderWorkers.push(newWorkerObj); return this.loaderWorkers.indexOf(newWorkerObj); } /** * Removes a loaderworker * * @param {Object} workerObj - 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 * * @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 * * @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 * * @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 * Sends the message straight to the inputWorker to be handled there. * * @param {MessageEvent} e */ handleLoaderMessage(e) { const r = e.data; if (Object.prototype.hasOwnProperty.call(r, "progress") && Object.prototype.hasOwnProperty.call(r, "inputNum")) { this.manager.tabs.updateInputTabProgress(r.inputNum, r.progress, 100); } const transferable = Object.prototype.hasOwnProperty.call(r, "fileBuffer") ? [r.fileBuffer] : undefined; this.inputWorker.postMessage({ action: "loaderWorkerMessage", data: r }, transferable); } /** * Handler for messages sent back by the inputWorker * * @param {MessageEvent} e */ handleInputWorkerMessage(e) { const r = e.data; if (!("action" in r)) { log.error("A message was received from the InputWorker with no action property. Ignoring message."); 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, r.data.tabsLeft, r.data.tabsRight); break; case "changeTab": this.changeTab(r.data, this.app.options.syncTabs); break; case "updateTabHeader": this.manager.tabs.updateInputTabHeader(r.data.inputNum, r.data.input); break; case "loadingInfo": this.showLoadingInfo(r.data, true); break; case "setInput": debounce(this.set, 50, "setInput", this, [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 "queueInputError": this.manager.worker.queueInputError(r.data); break; case "bakeAllInputs": this.manager.worker.bakeAllInputs(r.data); break; case "displayTabSearchResults": this.displayTabSearchResults(r.data); break; case "filterTabError": this.app.handleError(r.data); break; case "setUrl": this.setUrl(r.data); break; case "inputSwitch": this.manager.output.inputSwitch(r.data); break; case "getInput": case "getInputNums": this.callbacks[r.data.id](r.data); break; case "removeChefWorker": this.removeChefWorker(); break; case "fileLoaded": this.fileLoaded(r.data.inputNum); break; default: log.error(`Unknown action ${r.action}.`); } } /** * Sends a message to the inputWorker to bake all inputs */ bakeAll() { this.app.progress = 0; debounce(this.manager.controls.toggleBakeButtonFunction, 20, "toggleBakeButton", this, ["loading"]); this.inputWorker.postMessage({ action: "bakeAll" }); } /** * Sets the input in the input area * * @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 false, fires the manager statechange event */ async set(inputData, silent=false) { return new Promise(function(resolve, reject) { const activeTab = this.manager.tabs.getActiveInputTab(); 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"); inputText.scroll(0, 0); const lines = inputData.input.length < (this.app.options.ioDisplayThreshold * 1024) ? inputData.input.count("\n") + 1 : null; this.setInputInfo(inputData.input.length, lines); // Set URL to current input const inputStr = toBase64(inputData.input, "A-Za-z0-9+/"); if (inputStr.length > 0 && inputStr.length <= 68267) { this.setUrl({ includeInput: true, input: inputStr }); } if (!silent) window.dispatchEvent(this.manager.statechange); } else { this.setFile(inputData, silent); } }.bind(this)); } /** * Displays file details * * @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=true] - If false, fires the manager statechange event */ setFile(inputData, silent=true) { const activeTab = this.manager.tabs.getActiveInputTab(); if (inputData.inputNum !== activeTab) return; const fileOverlay = document.getElementById("input-file"), fileName = document.getElementById("input-file-name"), fileSize = document.getElementById("input-file-size"), fileType = document.getElementById("input-file-type"), fileLoaded = document.getElementById("input-file-loaded"); fileOverlay.style.display = "block"; fileName.textContent = inputData.name; fileSize.textContent = inputData.size + " bytes"; fileType.textContent = inputData.type; if (inputData.status === "error") { fileLoaded.textContent = "Error"; fileLoaded.style.color = "#FF0000"; } else { fileLoaded.style.color = ""; fileLoaded.textContent = inputData.progress + "%"; } this.setInputInfo(inputData.size, null); this.displayFilePreview(inputData); if (!silent) window.dispatchEvent(this.manager.statechange); } /** * Update file details when a file completes loading * * @param {number} inputNum - The inputNum of the input which has finished loading */ fileLoaded(inputNum) { this.manager.tabs.updateInputTabProgress(inputNum, 100, 100); const activeTab = this.manager.tabs.getActiveInputTab(); if (activeTab !== inputNum) return; this.inputWorker.postMessage({ action: "setInput", data: { inputNum: inputNum, silent: false } }); this.updateFileProgress(inputNum, 100); } /** * Render the input thumbnail */ async renderFileThumb() { const activeTab = this.manager.tabs.getActiveInputTab(), input = await this.getInputValue(activeTab), fileThumb = document.getElementById("input-file-thumbnail"); if (typeof input === "string" || !this.app.options.imagePreview) { this.resetFileThumb(); return; } const inputArr = new Uint8Array(input), type = isImage(inputArr); if (type && type !== "image/tiff" && inputArr.byteLength <= 512000) { // Most browsers don't support displaying TIFFs, so ignore them // Don't render images over 512000 bytes const blob = new Blob([inputArr], {type: type}), url = URL.createObjectURL(blob); fileThumb.src = url; } else { this.resetFileThumb(); } } /** * Reset the input thumbnail to the default icon */ resetFileThumb() { const fileThumb = document.getElementById("input-file-thumbnail"); fileThumb.src = require("../static/images/file-128x128.png"); } /** * Shows a chunk of the file in the input behind the file overlay * * @param {Object} inputData - 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 */ displayFilePreview(inputData) { const activeTab = this.manager.tabs.getActiveInputTab(), input = inputData.input, inputText = document.getElementById("input-text"); if (inputData.inputNum !== activeTab) return; inputText.style.overflow = "hidden"; inputText.classList.add("blur"); inputText.value = Utils.printable(Utils.arrayBufferToStr(input.slice(0, 4096))); this.renderFileThumb(); } /** * Updates the displayed load progress for a file * * @param {number} inputNum * @param {number | string} progress - Either a number or "error" */ updateFileProgress(inputNum, progress) { const activeTab = this.manager.tabs.getActiveInputTab(); 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 = ""; } } /** * Updates the stored value for the specified inputNum * * @param {number} inputNum * @param {string | ArrayBuffer} value * @param {boolean} [force=false] - If true, forces the value to be updated even if the type is different to the currently stored type */ updateInputValue(inputNum, value, force=false) { 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 }); // Value is either a string set by the input or an ArrayBuffer from a LoaderWorker, // so is safe to use typeof === "string" const transferable = (typeof value !== "string") ? [value] : undefined; this.inputWorker.postMessage({ action: "updateInputValue", data: { inputNum: inputNum, value: value, force: force } }, transferable); } /** * Updates the .data property for the input of the specified inputNum. * Used for switching the output into the input * * @param {number} inputNum - The inputNum of the input we're changing * @param {object} inputData - The new data object */ updateInputObj(inputNum, inputData) { const transferable = (typeof inputData !== "string") ? [inputData.fileBuffer] : undefined; this.inputWorker.postMessage({ action: "updateInputObj", data: { inputNum: inputNum, data: inputData } }, transferable); } /** * 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 } }); } /** * Gets the number of inputs from the inputWorker * * @returns {object} */ async getInputNums() { return await new Promise(resolve => { this.getNums(r => { resolve(r); }); }); } /** * Gets a list of inputNums from the inputWorker, and sends * them back to the specified callback */ getNums(callback) { const id = this.callbackID++; this.callbacks[id] = callback; this.inputWorker.postMessage({ action: "getInputNums", data: 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, " "); 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; } /** * Handler for input change events. * Debounces the input so we don't call autobake too often. * * @param {event} e */ debounceInputChange(e) { debounce(this.inputChange, 50, "inputChange", this, [e])(); } /** * Handler for input change events. * Updates the value stored in the inputWorker * * @param {event} e * * @fires Manager#statechange */ inputChange(e) { // Ignore this function if the input is a file const fileOverlay = document.getElementById("input-file"); if (fileOverlay.style.display === "block") return; // Remove highlighting from input and output panes as the offsets might be different now this.manager.highlighter.removeHighlights(); const textArea = document.getElementById("input-text"); const value = (textArea.value !== undefined) ? textArea.value : ""; const activeTab = this.manager.tabs.getActiveInputTab(); 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.manager.tabs.updateInputTabHeader(activeTab, value.replace(/[\n\r]/g, "").slice(0, 100)); 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 */ async inputPaste(e) { e.preventDefault(); e.stopPropagation(); const self = this; /** * Triggers the input file/binary data overlay * * @param {string} pastedData */ function triggerOverlay(pastedData) { const file = new File([pastedData], "PastedData", { type: "text/plain", lastModified: Date.now() }); self.loadUIFiles([file]); } const pastedData = e.clipboardData.getData("Text"); const inputText = document.getElementById("input-text"); const selStart = inputText.selectionStart; const selEnd = inputText.selectionEnd; const startVal = inputText.value.slice(0, selStart); const endVal = inputText.value.slice(selEnd); const val = startVal + pastedData + endVal; if (val.length >= (this.app.options.ioDisplayThreshold * 1024)) { // Data too large to display, use overlay triggerOverlay(val); return false; } else if (await this.preserveCarriageReturns(val)) { // Data contains a carriage return and the user doesn't wish to edit it, use overlay // We check this in a separate condition to make sure it is not run unless absolutely // necessary. triggerOverlay(val); return false; } else { // Pasting normally fires the inputChange() event before // changing the value, so instead change it here ourselves // and manually fire inputChange() inputText.value = val; inputText.setSelectionRange(selStart + pastedData.length, selStart + pastedData.length); this.debounceInputChange(e); } } /** * Handler for input dragover events. * Gives the user a visual cue to show that items can be dropped here. * * @param {event} e */ inputDragover(e) { // This will be set if we're dragging an operation if (e.dataTransfer.effectAllowed === "move") return false; e.stopPropagation(); e.preventDefault(); e.target.closest("#input-text,#input-file").classList.add("dropping-file"); } /** * Handler for input dragleave events. * Removes the visual cue. * * @param {event} e */ inputDragleave(e) { e.stopPropagation(); e.preventDefault(); e.target.closest("#input-text,#input-file").classList.remove("dropping-file"); } /** * Handler for input drop events. * Loads the dragged data. * * @param {event} e */ inputDrop(e) { // This will be set if we're dragging an operation if (e.dataTransfer.effectAllowed === "move") return false; e.stopPropagation(); e.preventDefault(); const text = e.dataTransfer.getData("Text"); e.target.closest("#input-text,#input-file").classList.remove("dropping-file"); if (text) { // Append the text to the current input and fire inputChange() document.getElementById("input-text").value += text; this.inputChange(e); 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 = ""; } } /** * Checks if an input contains carriage returns. * If a CR is detected, checks if the preserve CR option has been set, * and if not, asks the user for their preference. * * @param {string} input - The input to be checked * @returns {boolean} - If true, the input contains a CR which should be * preserved, so display an overlay so it can't be edited */ async preserveCarriageReturns(input) { if (input.indexOf("\r") < 0) return false; const optionsStr = "This behaviour can be changed in the Options pane"; if (!this.app.options.userSetCR) { // User has not set a CR preference yet let preserve = await new Promise(function(resolve, reject) { this.app.confirm( "Carriage Return Detected", "A carriage return (\\r, 0x0d) was detected in your input. As HTML textareas can't display carriage returns, editing must be turned off to preserve them.
Alternatively, you can enable editing but your carriage returns will not be preserved.

This preference will be saved but can be toggled in the options pane.", "Preserve Carriage Returns", "Enable Editing", resolve, this); }.bind(this)); if (preserve === undefined) { // The confirm pane was closed without picking a specific choice this.app.alert(`Not preserving carriage returns.\n${optionsStr}`, 5000); preserve = false; } this.manager.options.updateOption("preserveCR", preserve); this.manager.options.updateOption("userSetCR", true); } else { if (this.app.options.preserveCR) { this.app.alert(`A carriage return (\\r, 0x0d) was detected in your input, so editing has been disabled to preserve it.
${optionsStr}`, 10000); } else { this.app.alert(`A carriage return (\\r, 0x0d) was detected in your input. Editing is remaining enabled, but carriage returns will not be preserved.
${optionsStr}`, 10000); } } return this.app.options.preserveCR; } /** * Load files from the UI into the inputWorker * * @param {FileList} files - The list of files to be loaded */ loadUIFiles(files) { const numFiles = files.length; const activeTab = this.manager.tabs.getActiveInputTab(); log.debug(`Loading ${numFiles} files.`); // Display the number of files as pending so the user // knows that we've received the files. this.showLoadingInfo({ pending: numFiles, loading: 0, loaded: 0, total: numFiles, activeProgress: { inputNum: activeTab, progress: 0 } }, false); this.inputWorker.postMessage({ action: "loadUIFiles", data: { files: files, activeTab: activeTab } }); } /** * Handler for open input button click. * Opens the open file dialog. */ inputOpenClick() { document.getElementById("open-file").click(); } /** * Handler for open folder button click * Opens the open folder dialog. */ folderOpenClick() { document.getElementById("open-folder").click(); } /** * 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 */ showLoadingInfo(loadedData, autoRefresh) { const pending = loadedData.pending; const loading = loadedData.loading; const loaded = loadedData.loaded; const total = loadedData.total; let width = total.toLocaleString().length; width = width < 2 ? 2 : width; const totalStr = total.toLocaleString().padStart(width, " ").replace(/ /g, " "); let msg = "total: " + totalStr; const loadedStr = loaded.toLocaleString().padStart(width, " ").replace(/ /g, " "); msg += "
loaded: " + loadedStr; if (pending > 0) { const pendingStr = pending.toLocaleString().padStart(width, " ").replace(/ /g, " "); msg += "
pending: " + pendingStr; } else if (loading > 0) { const loadingStr = loading.toLocaleString().padStart(width, " ").replace(/ /g, " "); msg += "
loading: " + loadingStr; } const inFiles = document.getElementById("input-files-info"); if (total > 1) { inFiles.innerHTML = msg; inFiles.style.display = ""; } else { inFiles.style.display = "none"; } this.updateFileProgress(loadedData.activeProgress.inputNum, loadedData.activeProgress.progress); const inputTitle = document.getElementById("input").firstElementChild; if (loaded < total) { const percentComplete = loaded / total * 100; inputTitle.style.background = `linear-gradient(to right, var(--title-background-colour) ${percentComplete}%, var(--primary-background-colour) ${percentComplete}%)`; } else { inputTitle.style.background = ""; } if (loaded < total && autoRefresh) { setTimeout(function() { this.inputWorker.postMessage({ action: "getLoadProgress", data: this.manager.tabs.getActiveInputTab() }); }.bind(this), 100); } } /** * Change to a different tab. * * @param {number} inputNum - The inputNum of the tab to change to * @param {boolean} [changeOutput=false] - If true, also changes the output */ changeTab(inputNum, changeOutput) { if (this.manager.tabs.getInputTabItem(inputNum) !== null) { this.manager.tabs.changeInputTab(inputNum); this.inputWorker.postMessage({ action: "setInput", data: { inputNum: inputNum, silent: true } }); } else { const minNum = Math.min(...this.manager.tabs.getInputTabList()); let direction = "right"; if (inputNum < minNum) { direction = "left"; } this.inputWorker.postMessage({ action: "refreshTabs", data: { inputNum: inputNum, direction: direction } }); } 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); } } /** * Handler for clear all IO events. * Resets the input, output and info areas, and creates a new inputWorker */ clearAllIoClick() { this.manager.worker.cancelBake(true, true); this.manager.worker.loaded = false; this.manager.output.removeAllOutputs(); this.manager.output.terminateZipWorker(); this.manager.highlighter.removeHighlights(); getSelection().removeAllRanges(); const tabsList = document.getElementById("input-tabs"); const tabsListChildren = tabsList.children; tabsList.classList.remove("tabs-left"); tabsList.classList.remove("tabs-right"); for (let i = tabsListChildren.length - 1; i >= 0; i--) { tabsListChildren.item(i).remove(); } this.showLoadingInfo({ pending: 0, loading: 0, loaded: 1, total: 1, activeProgress: { inputNum: 1, progress: 100 } }); this.setupInputWorker(); this.manager.worker.setupChefWorker(); this.addInput(true); this.bakeAll(); } /** * Handler for clear IO click event. * Resets the input for the current tab */ clearIoClick() { const inputNum = this.manager.tabs.getActiveInputTab(); if (inputNum === -1) return; this.manager.highlighter.removeHighlights(); getSelection().removeAllRanges(); this.updateInputValue(inputNum, "", true); this.set({ inputNum: inputNum, input: "" }); this.manager.tabs.updateInputTabHeader(inputNum, ""); } /** * Sets the console log level in the worker. * * @param {string} level */ setLogLevel(level) { if (!this.inputWorker) return; this.inputWorker.postMessage({ action: "setLogLevel", data: log.getLevel() }); } /** * Sends a message to the inputWorker to add a new input. * @param {boolean} [changeTab=false] - If true, changes the tab to the new input */ addInput(changeTab=false) { if (!this.inputWorker) return; this.inputWorker.postMessage({ action: "addInput", data: changeTab }); } /** * Handler for add input button clicked. */ addInputClick() { this.addInput(true); } /** * Handler for when the inputWorker adds a new input * * @param {boolean} changeTab - 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. * * @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"), numTabs = tabsWrapper.children.length; if (!this.manager.tabs.getInputTabItem(inputNum) && numTabs < this.maxTabs) { const newTab = this.manager.tabs.createInputTabElement(inputNum, changeTab); tabsWrapper.appendChild(newTab); if (numTabs > 0) { this.manager.tabs.showTabBar(); } else { this.manager.tabs.hideTabBar(); } this.inputWorker.postMessage({ action: "updateTabHeader", data: inputNum }); } else if (numTabs === this.maxTabs) { // Can't create a new tab document.getElementById("input-tabs").lastElementChild.classList.add("tabs-right"); } if (changeTab) this.changeTab(inputNum, false); } /** * Refreshes the input tabs, and changes to activeTab * * @param {number[]} nums - The inputNums to be displayed as tabs * @param {number} activeTab - The tab to change to * @param {boolean} tabsLeft - True if there are input tabs to the left of the displayed tabs * @param {boolean} tabsRight - True if there are input tabs to the right of the displayed tabs */ refreshTabs(nums, activeTab, tabsLeft, tabsRight) { this.manager.tabs.refreshInputTabs(nums, activeTab, tabsLeft, tabsRight); this.inputWorker.postMessage({ action: "setInput", data: { inputNum: activeTab, silent: true } }); } /** * Sends a message to the inputWorker to remove an input. * If the input tab is on the screen, refreshes the tabs * * @param {number} inputNum - The inputNum of the tab to be removed */ removeInput(inputNum) { let refresh = false; if (this.manager.tabs.getInputTabItem(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 * * @param {event} mouseEvent */ removeTabClick(mouseEvent) { if (!mouseEvent.target) { return; } const tabNum = mouseEvent.target.closest("button").parentElement.getAttribute("inputNum"); if (tabNum) { this.removeInput(parseInt(tabNum, 10)); } } /** * Handler for scrolling on the input tabs area * * @param {event} wheelEvent */ scrollTab(wheelEvent) { wheelEvent.preventDefault(); if (wheelEvent.deltaY > 0) { this.changeTabLeft(); } else if (wheelEvent.deltaY < 0) { this.changeTabRight(); } } /** * Handler for mouse down on the next tab button */ nextTabClick() { this.mousedown = true; this.changeTabRight(); const time = 200; const func = function(time) { if (this.mousedown) { this.changeTabRight(); const newTime = (time > 50) ? time = time - 10 : 50; setTimeout(func.bind(this, [newTime]), newTime); } }; this.tabTimeout = setTimeout(func.bind(this, [time]), time); } /** * Handler for mouse down on the previous tab button */ previousTabClick() { this.mousedown = true; this.changeTabLeft(); const time = 200; const func = function(time) { if (this.mousedown) { this.changeTabLeft(); const newTime = (time > 50) ? time = time - 10 : 50; setTimeout(func.bind(this, [newTime]), newTime); } }; this.tabTimeout = setTimeout(func.bind(this, [time]), time); } /** * Handler for mouse up event on the tab buttons */ tabMouseUp() { this.mousedown = false; clearTimeout(this.tabTimeout); this.tabTimeout = null; } /** * Changes to the next (right) tab */ changeTabRight() { const activeTab = this.manager.tabs.getActiveInputTab(); if (activeTab === -1) return; this.inputWorker.postMessage({ action: "changeTabRight", data: { activeTab: activeTab } }); } /** * Changes to the previous (left) tab */ changeTabLeft() { const activeTab = this.manager.tabs.getActiveInputTab(); if (activeTab === -1) return; this.inputWorker.postMessage({ action: "changeTabLeft", data: { activeTab: activeTab } }); } /** * Handler for go to tab button clicked */ async goToTab() { const inputNums = await this.getInputNums(); let tabNum = window.prompt(`Enter tab number (${inputNums.min} - ${inputNums.max}):`, this.manager.tabs.getActiveInputTab().toString()); if (tabNum === null) return; tabNum = parseInt(tabNum, 10); this.changeTab(tabNum, this.app.options.syncTabs); } /** * Handler for find tab button clicked */ findTab() { this.filterTabSearch(); $("#input-tab-modal").modal(); } /** * Sends a message to the inputWorker to search the inputs */ filterTabSearch() { const showPending = document.getElementById("input-show-pending").checked; const showLoading = document.getElementById("input-show-loading").checked; const showLoaded = document.getElementById("input-show-loaded").checked; const filter = document.getElementById("input-filter").value; const filterType = document.getElementById("input-filter-button").innerText; const numResults = parseInt(document.getElementById("input-num-results").value, 10); this.inputWorker.postMessage({ action: "filterTabs", data: { showPending: showPending, showLoading: showLoading, showLoaded: showLoaded, filter: filter, filterType: filterType, numResults: numResults } }); } /** * Handle when an option in the filter drop down box is clicked * * @param {event} mouseEvent */ filterOptionClick(mouseEvent) { document.getElementById("input-filter-button").innerText = mouseEvent.target.innerText; this.filterTabSearch(); } /** * Displays the results of a tab search in the find tab box * * @param {object[]} results - List of results objects * */ displayTabSearchResults(results) { const resultsList = document.getElementById("input-search-results"); for (let i = resultsList.children.length - 1; i >= 0; i--) { resultsList.children.item(i).remove(); } for (let i = 0; i < results.length; i++) { const newListItem = document.createElement("li"); newListItem.classList.add("input-filter-result"); newListItem.setAttribute("inputNum", results[i].inputNum); newListItem.innerText = `${results[i].inputNum}: ${results[i].textDisplay}`; resultsList.appendChild(newListItem); } } /** * Handler for clicking on a filter result * * @param {event} e */ filterItemClick(e) { if (!e.target) return; const inputNum = parseInt(e.target.getAttribute("inputNum"), 10); if (inputNum <= 0) return; $("#input-tab-modal").modal("hide"); this.changeTab(inputNum, this.app.options.syncTabs); } /** * Update the input URL to the new value * * @param {object} urlData - 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); } } export default InputWaiter;