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