From 4e00ac930028390d19297225c689cfbac617fd5a Mon Sep 17 00:00:00 2001 From: n1474335 Date: Mon, 18 Dec 2017 20:39:55 +0000 Subject: [PATCH] Files are now uploaded in a worker and not displayed in the input by default. Added ArrayBuffer Dish type. --- src/core/Chef.js | 7 +- src/core/Dish.js | 46 ++++++--- src/web/InputWaiter.js | 122 +++++++++++++++-------- src/web/LoaderWorker.js | 50 ++++++++++ src/web/Manager.js | 7 +- src/web/html/index.html | 14 +++ src/web/stylesheets/components/_pane.css | 41 ++++++++ src/web/stylesheets/layout/_io.css | 12 ++- 8 files changed, 232 insertions(+), 67 deletions(-) create mode 100644 src/web/LoaderWorker.js diff --git a/src/core/Chef.js b/src/core/Chef.js index 4e7c042a..d176a36d 100755 --- a/src/core/Chef.js +++ b/src/core/Chef.js @@ -19,7 +19,7 @@ const Chef = function() { /** * Runs the recipe over the input. * - * @param {string} inputText - The input data as a string + * @param {string|ArrayBuffer} input - The input data as a string or ArrayBuffer * @param {Object[]} recipeConfig - The recipe configuration object * @param {Object} options - The options object storing various user choices * @param {boolean} options.attempHighlight - Whether or not to attempt highlighting @@ -33,7 +33,7 @@ const Chef = function() { * @returns {number} response.duration - The number of ms it took to execute the recipe * @returns {number} response.error - The error object thrown by a failed operation (false if no error) */ -Chef.prototype.bake = async function(inputText, recipeConfig, options, progress, step) { +Chef.prototype.bake = async function(input, recipeConfig, options, progress, step) { let startTime = new Date().getTime(), recipe = new Recipe(recipeConfig), containsFc = recipe.containsFlowControl(), @@ -62,7 +62,8 @@ Chef.prototype.bake = async function(inputText, recipeConfig, options, progress, // If starting from scratch, load data if (progress === 0) { - this.dish.set(inputText, Dish.STRING); + const type = input instanceof ArrayBuffer ? Dish.ARRAY_BUFFER : Dish.STRING; + this.dish.set(input, type); } try { diff --git a/src/core/Dish.js b/src/core/Dish.js index 914188c1..3cd1c6f3 100755 --- a/src/core/Dish.js +++ b/src/core/Dish.js @@ -8,11 +8,11 @@ import Utils from "./Utils.js"; * @license Apache-2.0 * * @class - * @param {byteArray|string|number} value - The value of the input data. + * @param {byteArray|string|number|ArrayBuffer} value - The value of the input data. * @param {number} type - The data type of value, see Dish enums. */ const Dish = function(value, type) { - this.value = value || typeof value == "string" ? value : null; + this.value = value || typeof value === "string" ? value : null; this.type = type || Dish.BYTE_ARRAY; }; @@ -41,6 +41,12 @@ Dish.NUMBER = 2; * @enum */ Dish.HTML = 3; +/** + * Dish data type enum for ArrayBuffers. + * @readonly + * @enum + */ +Dish.ARRAY_BUFFER = 4; /** @@ -64,6 +70,9 @@ Dish.typeEnum = function(typeStr) { case "html": case "HTML": return Dish.HTML; + case "arrayBuffer": + case "ArrayBuffer": + return Dish.ARRAY_BUFFER; default: throw "Invalid data type string. No matching enum."; } @@ -87,6 +96,8 @@ Dish.enumLookup = function(typeEnum) { return "number"; case Dish.HTML: return "html"; + case Dish.ARRAY_BUFFER: + return "ArrayBuffer"; default: throw "Invalid data type enum. No matching type."; } @@ -96,7 +107,7 @@ Dish.enumLookup = function(typeEnum) { /** * Sets the data value and type and then validates them. * - * @param {byteArray|string|number} value - The value of the input data. + * @param {byteArray|string|number|ArrayBuffer} value - The value of the input data. * @param {number} type - The data type of value, see Dish enums. */ Dish.prototype.set = function(value, type) { @@ -114,7 +125,7 @@ Dish.prototype.set = function(value, type) { * Returns the value of the data in the type format specified. * * @param {number} type - The data type of value, see Dish enums. - * @returns {byteArray|string|number} The value of the output data. + * @returns {byteArray|string|number|ArrayBuffer} The value of the output data. */ Dish.prototype.get = function(type) { if (this.type !== type) { @@ -134,20 +145,23 @@ Dish.prototype.translate = function(toType) { switch (this.type) { case Dish.STRING: this.value = this.value ? Utils.strToByteArray(this.value) : []; - this.type = Dish.BYTE_ARRAY; break; case Dish.NUMBER: this.value = typeof this.value == "number" ? Utils.strToByteArray(this.value.toString()) : []; - this.type = Dish.BYTE_ARRAY; break; case Dish.HTML: this.value = this.value ? Utils.strToByteArray(Utils.unescapeHtml(Utils.stripHtmlTags(this.value, true))) : []; - this.type = Dish.BYTE_ARRAY; + break; + case Dish.ARRAY_BUFFER: + // Array.from() would be nicer here, but it's slightly slower + this.value = Array.prototype.slice.call(new Uint8Array(this.value)); break; default: break; } + this.type = Dish.BYTE_ARRAY; + // Convert from byteArray to toType switch (toType) { case Dish.STRING: @@ -159,6 +173,10 @@ Dish.prototype.translate = function(toType) { this.value = this.value ? parseFloat(Utils.byteArrayToUtf8(this.value)) : 0; this.type = Dish.NUMBER; break; + case Dish.ARRAY_BUFFER: + this.value = new Uint8Array(this.value).buffer; + this.type = Dish.ARRAY_BUFFER; + break; default: break; } @@ -180,7 +198,7 @@ Dish.prototype.valid = function() { // Check that every value is a number between 0 - 255 for (let i = 0; i < this.value.length; i++) { - if (typeof this.value[i] != "number" || + if (typeof this.value[i] !== "number" || this.value[i] < 0 || this.value[i] > 255) { return false; @@ -189,15 +207,11 @@ Dish.prototype.valid = function() { return true; case Dish.STRING: case Dish.HTML: - if (typeof this.value == "string") { - return true; - } - return false; + return typeof this.value === "string"; case Dish.NUMBER: - if (typeof this.value == "number") { - return true; - } - return false; + return typeof this.value === "number"; + case Dish.ARRAY_BUFFER: + return this.value instanceof ArrayBuffer; default: return false; } diff --git a/src/web/InputWaiter.js b/src/web/InputWaiter.js index aba57334..b5748e3f 100755 --- a/src/web/InputWaiter.js +++ b/src/web/InputWaiter.js @@ -1,4 +1,5 @@ import Utils from "../core/Utils.js"; +import LoaderWorker from "worker-loader?inline&fallback=false!./LoaderWorker.js"; /** @@ -33,6 +34,9 @@ const InputWaiter = function(app, manager) { 144, //Num 145, //Scroll ]; + + this.loaderWorker = null; + this.fileBuffer = null; }; @@ -42,23 +46,49 @@ const InputWaiter = function(app, manager) { * @returns {string} */ InputWaiter.prototype.get = function() { - return document.getElementById("input-text").value; + return this.fileBuffer || document.getElementById("input-text").value; }; /** - * Sets the input in the input textarea. + * Sets the input in the input area. * - * @param {string} input + * @param {string|File} input * * @fires Manager#statechange */ InputWaiter.prototype.set = function(input) { + if (input instanceof File) { + this.setFile(input); + input = ""; + } + document.getElementById("input-text").value = input; window.dispatchEvent(this.manager.statechange); }; +/** + * Shows file details. + * + * @param {File} file + */ +InputWaiter.prototype.setFile = function(file) { + // Display file overlay in input area with details + const fileOverlay = document.getElementById("input-file"), + fileName = document.getElementById("input-file-name"), + fileSize = document.getElementById("input-file-size"), + fileType = document.getElementById("input-file-type"), + fileUploaded = document.getElementById("input-file-uploaded"); + + fileOverlay.style.display = "block"; + fileName.textContent = file.name; + fileSize.textContent = file.size.toLocaleString() + " bytes"; + fileType.textContent = file.type; + fileUploaded.textContent = "0%"; +}; + + /** * Displays information about the input. * @@ -118,7 +148,7 @@ InputWaiter.prototype.inputDragover = function(e) { e.stopPropagation(); e.preventDefault(); - e.target.classList.add("dropping-file"); + e.target.closest("#input-text,#input-file").classList.add("dropping-file"); }; @@ -131,7 +161,8 @@ InputWaiter.prototype.inputDragover = function(e) { InputWaiter.prototype.inputDragleave = function(e) { e.stopPropagation(); e.preventDefault(); - e.target.classList.remove("dropping-file"); + document.getElementById("input-text").classList.remove("dropping-file"); + document.getElementById("input-file").classList.remove("dropping-file"); }; @@ -149,55 +180,57 @@ InputWaiter.prototype.inputDrop = function(e) { e.stopPropagation(); e.preventDefault(); - const el = e.target; const file = e.dataTransfer.files[0]; const text = e.dataTransfer.getData("Text"); - const reader = new FileReader(); - let inputCharcode = ""; - let offset = 0; - const CHUNK_SIZE = 20480; // 20KB - const setInput = function() { - const recipeConfig = this.app.getRecipeConfig(); - if (!recipeConfig[0] || recipeConfig[0].op !== "From Hex") { - recipeConfig.unshift({op: "From Hex", args: ["Space"]}); - this.app.setRecipeConfig(recipeConfig); - } + document.getElementById("input-text").classList.remove("dropping-file"); + document.getElementById("input-file").classList.remove("dropping-file"); - this.set(inputCharcode); - - el.classList.remove("loadingFile"); - }.bind(this); - - const seek = function() { - if (offset >= file.size) { - setInput(); - return; - } - el.value = "Processing... " + Math.round(offset / file.size * 100) + "%"; - const slice = file.slice(offset, offset + CHUNK_SIZE); - reader.readAsArrayBuffer(slice); - }; - - reader.onload = function(e) { - const data = new Uint8Array(reader.result); - inputCharcode += Utils.toHexFast(data); - offset += CHUNK_SIZE; - seek(); - }; - - - el.classList.remove("dropping-file"); + if (text) { + this.closeFile(); + this.set(text); + return; + } if (file) { - el.classList.add("loadingFile"); - seek(); - } else if (text) { - this.set(text); + this.closeFile(); + this.loaderWorker = new LoaderWorker(); + this.loaderWorker.addEventListener("message", this.handleLoaderMessage.bind(this)); + this.loaderWorker.postMessage({"file": file}); + this.set(file); } }; +/** + * Handler for messages sent back by the LoaderWorker. + * + * @param {MessageEvent} e + */ +InputWaiter.prototype.handleLoaderMessage = function(e) { + const r = e.data; + if (r.hasOwnProperty("progress")) { + const fileUploaded = document.getElementById("input-file-uploaded"); + fileUploaded.textContent = r.progress + "%"; + } + + if (r.hasOwnProperty("fileBuffer")) { + this.fileBuffer = r.fileBuffer; + window.dispatchEvent(this.manager.statechange); + } +}; + + +/** + * Handler for file close events. + */ +InputWaiter.prototype.closeFile = function() { + if (this.loaderWorker) this.loaderWorker.terminate(); + this.fileBuffer = null; + document.getElementById("input-file").style.display = "none"; +}; + + /** * Handler for clear IO events. * Resets the input, output and info areas. @@ -205,6 +238,7 @@ InputWaiter.prototype.inputDrop = function(e) { * @fires Manager#statechange */ InputWaiter.prototype.clearIoClick = function() { + this.closeFile(); this.manager.highlighter.removeHighlights(); document.getElementById("input-text").value = ""; document.getElementById("output-text").value = ""; diff --git a/src/web/LoaderWorker.js b/src/web/LoaderWorker.js new file mode 100644 index 00000000..b841c096 --- /dev/null +++ b/src/web/LoaderWorker.js @@ -0,0 +1,50 @@ +/** + * Web Worker to load large amounts of data without locking up the UI. + * + * @author n1474335 [n1474335@gmail.com] + * @copyright Crown Copyright 2017 + * @license Apache-2.0 + */ + + +/** + * Respond to message from parent thread. + */ +self.addEventListener("message", function(e) { + const r = e.data; + if (r.hasOwnProperty("file")) { + self.loadFile(r.file); + } +}); + + +/** + * Loads a file object into an ArrayBuffer, then transfers it back to the parent thread. + * + * @param {File} file + */ +self.loadFile = function(file) { + const reader = new FileReader(); + let data = new Uint8Array(file.size); + let offset = 0; + const CHUNK_SIZE = 10485760; // 10MiB + + const seek = function() { + if (offset >= file.size) { + self.postMessage({"progress": 100}); + self.postMessage({"fileBuffer": data.buffer}, [data.buffer]); + return; + } + self.postMessage({"progress": Math.round(offset / file.size * 100)}); + const slice = file.slice(offset, offset + CHUNK_SIZE); + reader.readAsArrayBuffer(slice); + }; + + reader.onload = function(e) { + data.set(new Uint8Array(reader.result), offset); + offset += CHUNK_SIZE; + seek(); + }; + + seek(); +}; diff --git a/src/web/Manager.js b/src/web/Manager.js index 2b44e6d2..b3ef0158 100755 --- a/src/web/Manager.js +++ b/src/web/Manager.js @@ -135,13 +135,14 @@ Manager.prototype.initialiseEventListeners = function() { this.addMultiEventListener("#input-text", "keyup paste", this.input.inputChange, this.input); document.getElementById("reset-layout").addEventListener("click", this.app.resetLayout.bind(this.app)); document.getElementById("clr-io").addEventListener("click", this.input.clearIoClick.bind(this.input)); - document.getElementById("input-text").addEventListener("dragover", this.input.inputDragover.bind(this.input)); - document.getElementById("input-text").addEventListener("dragleave", this.input.inputDragleave.bind(this.input)); - document.getElementById("input-text").addEventListener("drop", this.input.inputDrop.bind(this.input)); + this.addListeners("#input-text,#input-file", "dragover", this.input.inputDragover, this.input); + this.addListeners("#input-text,#input-file", "dragleave", this.input.inputDragleave, this.input); + this.addListeners("#input-text,#input-file", "drop", this.input.inputDrop, this.input); document.getElementById("input-text").addEventListener("scroll", this.highlighter.inputScroll.bind(this.highlighter)); document.getElementById("input-text").addEventListener("mouseup", this.highlighter.inputMouseup.bind(this.highlighter)); document.getElementById("input-text").addEventListener("mousemove", this.highlighter.inputMousemove.bind(this.highlighter)); this.addMultiEventListener("#input-text", "mousedown dblclick select", this.highlighter.inputMousedown, this.highlighter); + document.querySelector("#input-file .close").addEventListener("click", this.input.closeFile.bind(this.input)); // Output document.getElementById("save-to-file").addEventListener("click", this.output.saveClick.bind(this.output)); diff --git a/src/web/html/index.html b/src/web/html/index.html index 66dfcc80..3bfefa8b 100755 --- a/src/web/html/index.html +++ b/src/web/html/index.html @@ -181,6 +181,20 @@
+
+
+
+ +
+ + Name:
+ Size:
+ Type:
+ Uploaded: +
+
+
+
diff --git a/src/web/stylesheets/components/_pane.css b/src/web/stylesheets/components/_pane.css index 7246a24a..f69984b4 100644 --- a/src/web/stylesheets/components/_pane.css +++ b/src/web/stylesheets/components/_pane.css @@ -28,3 +28,44 @@ margin: 0; padding: 0; } + +.card { + box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2); + transition: 0.3s; + width: 400px; + height: 150px; + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + font-family: var(--primary-font-family); + color: var(--primary-font-colour); + line-height: 30px; + background-color: var(--primary-background-colour); +} + +.card:hover { + box-shadow: 0 8px 16px 0 rgba(0,0,0,0.2); +} + +.card>img { + float: left; + width: 150px; + height: 150px; +} + +.card-body .close { + position: absolute; + right: 10px; + top: 10px; +} + +.card-body { + float: left; + padding: 16px; + width: 250px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + user-select: text; +} \ No newline at end of file diff --git a/src/web/stylesheets/layout/_io.css b/src/web/stylesheets/layout/_io.css index 997af92e..855d4262 100644 --- a/src/web/stylesheets/layout/_io.css +++ b/src/web/stylesheets/layout/_io.css @@ -39,7 +39,7 @@ } .textarea-wrapper textarea, -.textarea-wrapper div { +.textarea-wrapper>div { font-family: var(--fixed-width-font-family); font-size: var(--fixed-width-font-size); color: var(--fixed-width-font-colour); @@ -77,6 +77,16 @@ transition: all 0.5s ease; } +#input-file { + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: var(--title-background-colour); + display: none; +} + .io-btn-group { float: right; margin-top: -4px;