/** * @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"; import InputWorker from "worker-loader?inline&fallback=false!../workers/InputWorker"; import Utils from "../../core/Utils"; import { toBase64 } from "../../core/lib/Base64"; import { isImage } from "../../core/lib/FileType"; /** * Waiter to handle events related to the input. */ class InputWaiter { /** * InputWaiter constructor. * * @param {App} app - The main view object for CyberChef. * @param {Manager} manager - The CyberChef event manager. */ constructor(app, manager) { this.app = app; this.manager = manager; // Define keys that don't change the input so we don't have to autobake when they are pressed this.badKeys = [ 16, //Shift 17, //Ctrl 18, //Alt 19, //Pause 20, //Caps 27, //Esc 33, 34, 35, 36, //PgUp, PgDn, End, Home 37, 38, 39, 40, //Directional 44, //PrntScrn 91, 92, //Win 93, //Context 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, //F1-12 144, //Num 145, //Scroll ]; this.inputWorker = null; this.loaderWorkers = []; this.workerId = 0; this.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 (r.hasOwnProperty("progress") && r.hasOwnProperty("inputNum")) { this.manager.tabs.updateInputTabProgress(r.inputNum, r.progress, 100); } else if (r.hasOwnProperty("fileBuffer")) { this.manager.tabs.updateInputTabProgress(r.inputNum, 100, 100); } const transferable = r.hasOwnProperty("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 (!r.hasOwnProperty("action")) { 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": this.app.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; default: log.error(`Unknown action ${r.action}.`); } } /** * Sends a message to the inputWorker to bake all inputs */ bakeAll() { this.app.progress = 0; this.manager.controls.toggleBakeButtonFunction("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 true, 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); } }.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 */ setFile(inputData) { 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); } /** * 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"), fileThumb = document.getElementById("input-file-thumbnail"); if (inputData.inputNum !== activeTab) return; inputText.style.overflow = "hidden"; inputText.classList.add("blur"); inputText.value = Utils.printable(Utils.arrayBufferToStr(input.slice(0, 4096))); if (this.app.options.imagePreview) { const inputArr = new Uint8Array(input), type = isImage(inputArr); if (type && type !== "image/tiff" && inputArr.byteLength <= 512000) { // Most browsers don't support displaying TIFFs, so ignore them // Assume anything over 512000 bytes is truncated const blob = new Blob([inputArr], {type: type}), url = URL.createObjectURL(blob); fileThumb.src = url; } else { this.resetFileThumb(); } } else { this.resetFileThumb(); } } /** * 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 = ""; } if (progress === 100 && progress !== oldProgress) { // Don't set the input if the progress hasn't changed this.inputWorker.postMessage({ action: "setInput", data: { inputNum: inputNum, silent: false } }); window.dispatchEvent(this.manager.statechange); } } /** * Updates the stored value for the specified inputNum * * @param {number} inputNum * @param {string | ArrayBuffer} value */ updateInputValue(inputNum, value) { let includeInput = false; const recipeStr = toBase64(value, "A-Za-z0-9+/"); // B64 alphabet with no padding if (recipeStr.length > 0 && recipeStr.length <= 68267) { includeInput = true; } this.setUrl({ includeInput: includeInput, input: recipeStr }); // 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 } }, 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) { this.app.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 */ inputPaste(e) { const pastedData = e.clipboardData.getData("Text"); if (pastedData.length < (this.app.options.ioDisplayThreshold * 1024)) { // Pasting normally fires the inputChange() event before // changing the value, so instead change it here ourselves // and manually fire inputChange() e.preventDefault(); 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); inputText.value = startVal + pastedData + endVal; inputText.setSelectionRange(selStart + pastedData.length, selStart + pastedData.length); this.debounceInputChange(e); } else { e.preventDefault(); e.stopPropagation(); const file = new File([pastedData], "PastedData", { type: "text/plain", lastModified: Date.now() }); this.loadUIFiles([file]); return false; } } /** * Handler for input dragover events. * Gives the user a visual cue to show that items can be dropped here. * * @param {event} e */ inputDragover(e) { // This will be set if we're dragging an operation if (e.dataTransfer.effectAllowed === "move") return false; e.stopPropagation(); e.preventDefault(); e.target.closest("#input-text,#input-file").classList.add("dropping-file"); } /** * Handler for input dragleave events. * Removes the visual cue. * * @param {event} e */ inputDragleave(e) { e.stopPropagation(); e.preventDefault(); e.target.closest("#input-text,#input-file").classList.remove("dropping-file"); } /** * Handler for input drop events. * Loads the dragged data. * * @param {event} e */ inputDrop(e) { // This will be set if we're dragging an operation if (e.dataTransfer.effectAllowed === "move") return false; e.stopPropagation(); e.preventDefault(); const text = e.dataTransfer.getData("Text"); e.target.closest("#input-text,#input-file").classList.remove("dropping-file"); if (text) { // 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 = ""; } } /** * 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").children; for (let i = tabsList.length - 1; i >= 0; i--) { tabsList.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, ""); 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.style.boxShadow = "-15px 0px 15px -15px var(--primary-border-colour) inset"; } 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;