2018-05-15 19:36:45 +02:00
|
|
|
/**
|
|
|
|
* @author n1474335 [n1474335@gmail.com]
|
|
|
|
* @copyright Crown Copyright 2016
|
|
|
|
* @license Apache-2.0
|
|
|
|
*/
|
|
|
|
|
|
|
|
import Utils from "../core/Utils";
|
|
|
|
import FileSaver from "file-saver";
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Waiter to handle events related to the output.
|
|
|
|
*/
|
|
|
|
class OutputWaiter {
|
|
|
|
|
|
|
|
/**
|
|
|
|
* OutputWaiter 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;
|
|
|
|
|
|
|
|
this.dishBuffer = null;
|
|
|
|
this.dishStr = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets the output string from the output textarea.
|
|
|
|
*
|
|
|
|
* @returns {string}
|
|
|
|
*/
|
|
|
|
get() {
|
|
|
|
return document.getElementById("output-text").value;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets the output in the output textarea.
|
|
|
|
*
|
|
|
|
* @param {string|ArrayBuffer} data - The output string/HTML/ArrayBuffer
|
|
|
|
* @param {string} type - The data type of the output
|
|
|
|
* @param {number} duration - The length of time (ms) it took to generate the output
|
|
|
|
* @param {boolean} [preserveBuffer=false] - Whether to preserve the dishBuffer
|
|
|
|
*/
|
|
|
|
async set(data, type, duration, preserveBuffer) {
|
|
|
|
log.debug("Output type: " + type);
|
|
|
|
const outputText = document.getElementById("output-text");
|
|
|
|
const outputHtml = document.getElementById("output-html");
|
|
|
|
const outputFile = document.getElementById("output-file");
|
|
|
|
const outputHighlighter = document.getElementById("output-highlighter");
|
|
|
|
const inputHighlighter = document.getElementById("input-highlighter");
|
|
|
|
let scriptElements, lines, length;
|
|
|
|
|
|
|
|
if (!preserveBuffer) {
|
|
|
|
this.closeFile();
|
|
|
|
this.dishStr = null;
|
|
|
|
document.getElementById("show-file-overlay").style.display = "none";
|
|
|
|
}
|
|
|
|
|
|
|
|
switch (type) {
|
|
|
|
case "html":
|
|
|
|
outputText.style.display = "none";
|
|
|
|
outputHtml.style.display = "block";
|
|
|
|
outputFile.style.display = "none";
|
|
|
|
outputHighlighter.display = "none";
|
|
|
|
inputHighlighter.display = "none";
|
|
|
|
|
|
|
|
outputText.value = "";
|
|
|
|
outputHtml.innerHTML = data;
|
|
|
|
|
|
|
|
// Execute script sections
|
|
|
|
scriptElements = outputHtml.querySelectorAll("script");
|
|
|
|
for (let i = 0; i < scriptElements.length; i++) {
|
|
|
|
try {
|
|
|
|
eval(scriptElements[i].innerHTML); // eslint-disable-line no-eval
|
|
|
|
} catch (err) {
|
|
|
|
log.error(err);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
await this.getDishStr();
|
|
|
|
length = this.dishStr.length;
|
|
|
|
lines = this.dishStr.count("\n") + 1;
|
|
|
|
break;
|
|
|
|
case "ArrayBuffer":
|
|
|
|
outputText.style.display = "block";
|
|
|
|
outputHtml.style.display = "none";
|
|
|
|
outputHighlighter.display = "none";
|
|
|
|
inputHighlighter.display = "none";
|
|
|
|
|
|
|
|
outputText.value = "";
|
|
|
|
outputHtml.innerHTML = "";
|
|
|
|
length = data.byteLength;
|
|
|
|
|
|
|
|
this.setFile(data);
|
|
|
|
break;
|
|
|
|
case "string":
|
|
|
|
default:
|
|
|
|
outputText.style.display = "block";
|
|
|
|
outputHtml.style.display = "none";
|
|
|
|
outputFile.style.display = "none";
|
|
|
|
outputHighlighter.display = "block";
|
|
|
|
inputHighlighter.display = "block";
|
|
|
|
|
|
|
|
outputText.value = Utils.printable(data, true);
|
|
|
|
outputHtml.innerHTML = "";
|
|
|
|
|
|
|
|
lines = data.count("\n") + 1;
|
|
|
|
length = data.length;
|
|
|
|
this.dishStr = data;
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.manager.highlighter.removeHighlights();
|
|
|
|
this.setOutputInfo(length, lines, duration);
|
2018-06-03 18:33:13 +02:00
|
|
|
this.backgroundMagic();
|
2018-05-15 19:36:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Shows file details.
|
|
|
|
*
|
|
|
|
* @param {ArrayBuffer} buf
|
|
|
|
*/
|
|
|
|
setFile(buf) {
|
|
|
|
this.dishBuffer = buf;
|
|
|
|
const file = new File([buf], "output.dat");
|
|
|
|
|
|
|
|
// Display file overlay in output area with details
|
|
|
|
const fileOverlay = document.getElementById("output-file"),
|
|
|
|
fileSize = document.getElementById("output-file-size");
|
|
|
|
|
|
|
|
fileOverlay.style.display = "block";
|
|
|
|
fileSize.textContent = file.size.toLocaleString() + " bytes";
|
|
|
|
|
|
|
|
// Display preview slice in the background
|
|
|
|
const outputText = document.getElementById("output-text"),
|
|
|
|
fileSlice = this.dishBuffer.slice(0, 4096);
|
|
|
|
|
|
|
|
outputText.classList.add("blur");
|
|
|
|
outputText.value = Utils.printable(Utils.arrayBufferToStr(fileSlice));
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Removes the output file and nulls its memory.
|
|
|
|
*/
|
|
|
|
closeFile() {
|
|
|
|
this.dishBuffer = null;
|
|
|
|
document.getElementById("output-file").style.display = "none";
|
|
|
|
document.getElementById("output-text").classList.remove("blur");
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handler for file download events.
|
|
|
|
*/
|
|
|
|
async downloadFile() {
|
|
|
|
this.filename = window.prompt("Please enter a filename:", this.filename || "download.dat");
|
|
|
|
await this.getDishBuffer();
|
|
|
|
const file = new File([this.dishBuffer], this.filename);
|
|
|
|
if (this.filename) FileSaver.saveAs(file, this.filename, false);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handler for file slice display events.
|
|
|
|
*/
|
|
|
|
displayFileSlice() {
|
|
|
|
const startTime = new Date().getTime(),
|
|
|
|
showFileOverlay = document.getElementById("show-file-overlay"),
|
|
|
|
sliceFromEl = document.getElementById("output-file-slice-from"),
|
|
|
|
sliceToEl = document.getElementById("output-file-slice-to"),
|
|
|
|
sliceFrom = parseInt(sliceFromEl.value, 10),
|
|
|
|
sliceTo = parseInt(sliceToEl.value, 10),
|
|
|
|
str = Utils.arrayBufferToStr(this.dishBuffer.slice(sliceFrom, sliceTo));
|
|
|
|
|
|
|
|
document.getElementById("output-text").classList.remove("blur");
|
|
|
|
showFileOverlay.style.display = "block";
|
|
|
|
this.set(str, "string", new Date().getTime() - startTime, true);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handler for show file overlay events.
|
|
|
|
*
|
|
|
|
* @param {Event} e
|
|
|
|
*/
|
|
|
|
showFileOverlayClick(e) {
|
|
|
|
const outputFile = document.getElementById("output-file"),
|
|
|
|
showFileOverlay = e.target;
|
|
|
|
|
|
|
|
document.getElementById("output-text").classList.add("blur");
|
|
|
|
outputFile.style.display = "block";
|
|
|
|
showFileOverlay.style.display = "none";
|
|
|
|
this.setOutputInfo(this.dishBuffer.byteLength, null, 0);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Displays information about the output.
|
|
|
|
*
|
|
|
|
* @param {number} length - The length of the current output string
|
|
|
|
* @param {number} lines - The number of the lines in the current output string
|
|
|
|
* @param {number} duration - The length of time (ms) it took to generate the output
|
|
|
|
*/
|
|
|
|
setOutputInfo(length, lines, duration) {
|
|
|
|
let width = length.toString().length;
|
|
|
|
width = width < 4 ? 4 : width;
|
|
|
|
|
|
|
|
const lengthStr = length.toString().padStart(width, " ").replace(/ /g, " ");
|
|
|
|
const timeStr = (duration.toString() + "ms").padStart(width, " ").replace(/ /g, " ");
|
|
|
|
|
|
|
|
let msg = "time: " + timeStr + "<br>length: " + lengthStr;
|
|
|
|
|
|
|
|
if (typeof lines === "number") {
|
|
|
|
const linesStr = lines.toString().padStart(width, " ").replace(/ /g, " ");
|
|
|
|
msg += "<br>lines: " + linesStr;
|
|
|
|
}
|
|
|
|
|
|
|
|
document.getElementById("output-info").innerHTML = msg;
|
|
|
|
document.getElementById("input-selection-info").innerHTML = "";
|
|
|
|
document.getElementById("output-selection-info").innerHTML = "";
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handler for save click events.
|
|
|
|
* Saves the current output to a file.
|
|
|
|
*/
|
|
|
|
saveClick() {
|
|
|
|
this.downloadFile();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handler for copy click events.
|
|
|
|
* Copies the output to the clipboard.
|
|
|
|
*/
|
|
|
|
async copyClick() {
|
|
|
|
await this.getDishStr();
|
|
|
|
|
|
|
|
// Create invisible textarea to populate with the raw dish string (not the printable version that
|
|
|
|
// contains dots instead of the actual bytes)
|
|
|
|
const textarea = document.createElement("textarea");
|
|
|
|
textarea.style.position = "fixed";
|
|
|
|
textarea.style.top = 0;
|
|
|
|
textarea.style.left = 0;
|
|
|
|
textarea.style.width = 0;
|
|
|
|
textarea.style.height = 0;
|
|
|
|
textarea.style.border = "none";
|
|
|
|
|
|
|
|
textarea.value = this.dishStr;
|
|
|
|
document.body.appendChild(textarea);
|
|
|
|
|
|
|
|
// Select and copy the contents of this textarea
|
|
|
|
let success = false;
|
|
|
|
try {
|
|
|
|
textarea.select();
|
|
|
|
success = this.dishStr && document.execCommand("copy");
|
|
|
|
} catch (err) {
|
|
|
|
success = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (success) {
|
2018-06-20 01:18:59 +02:00
|
|
|
this.app.alert("Copied raw output successfully.", 2000);
|
2018-05-15 19:36:45 +02:00
|
|
|
} else {
|
2018-06-20 01:18:59 +02:00
|
|
|
this.app.alert("Sorry, the output could not be copied.", 3000);
|
2018-05-15 19:36:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Clean up
|
|
|
|
document.body.removeChild(textarea);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handler for switch click events.
|
|
|
|
* Moves the current output into the input textarea.
|
|
|
|
*/
|
|
|
|
async switchClick() {
|
|
|
|
this.switchOrigData = this.manager.input.get();
|
|
|
|
document.getElementById("undo-switch").disabled = false;
|
|
|
|
if (this.dishBuffer) {
|
|
|
|
this.manager.input.setFile(new File([this.dishBuffer], "output.dat"));
|
|
|
|
this.manager.input.handleLoaderMessage({
|
|
|
|
data: {
|
|
|
|
progress: 100,
|
|
|
|
fileBuffer: this.dishBuffer
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
await this.getDishStr();
|
|
|
|
this.app.setInput(this.dishStr);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handler for undo switch click events.
|
|
|
|
* Removes the output from the input and replaces the input that was removed.
|
|
|
|
*/
|
|
|
|
undoSwitchClick() {
|
|
|
|
this.app.setInput(this.switchOrigData);
|
2018-06-09 11:43:36 +02:00
|
|
|
const undoSwitch = document.getElementById("undo-switch");
|
|
|
|
undoSwitch.disabled = true;
|
|
|
|
$(undoSwitch).tooltip("hide");
|
2018-05-15 19:36:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handler for maximise output click events.
|
|
|
|
* Resizes the output frame to be as large as possible, or restores it to its original size.
|
|
|
|
*/
|
|
|
|
maximiseOutputClick(e) {
|
|
|
|
const el = e.target.id === "maximise-output" ? e.target : e.target.parentNode;
|
|
|
|
|
2018-06-09 11:43:36 +02:00
|
|
|
if (el.getAttribute("data-original-title").indexOf("Maximise") === 0) {
|
2018-05-15 19:36:45 +02:00
|
|
|
this.app.columnSplitter.collapse(0);
|
|
|
|
this.app.columnSplitter.collapse(1);
|
|
|
|
this.app.ioSplitter.collapse(0);
|
|
|
|
|
2018-06-09 11:43:36 +02:00
|
|
|
$(el).attr("data-original-title", "Restore output pane");
|
|
|
|
el.querySelector("i").innerHTML = "fullscreen_exit";
|
2018-05-15 19:36:45 +02:00
|
|
|
} else {
|
2018-06-09 11:43:36 +02:00
|
|
|
$(el).attr("data-original-title", "Maximise output pane");
|
|
|
|
el.querySelector("i").innerHTML = "fullscreen";
|
2018-05-15 19:36:45 +02:00
|
|
|
this.app.resetLayout();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2019-01-15 20:03:17 +01:00
|
|
|
* Save bombe object before it is removed so that it can be used later
|
|
|
|
*/
|
|
|
|
saveBombe() {
|
|
|
|
this.bombeEl = document.getElementById("bombe").cloneNode();
|
|
|
|
this.bombeEl.setAttribute("width", "100%");
|
|
|
|
this.bombeEl.setAttribute("height", "100%");
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Shows or hides the output loading screen.
|
|
|
|
* The animated Bombe SVG, whilst quite aesthetically pleasing, is reasonably CPU
|
|
|
|
* intensive, so we remove it from the DOM when not in use. We only show it if the
|
|
|
|
* recipe is taking longer than 200ms. We add it to the DOM just before that so that
|
|
|
|
* it is ready to fade in without stuttering.
|
2018-05-15 19:36:45 +02:00
|
|
|
*
|
2019-01-15 20:03:17 +01:00
|
|
|
* @param {boolean} value - true == show loader
|
2018-05-15 19:36:45 +02:00
|
|
|
*/
|
|
|
|
toggleLoader(value) {
|
2019-01-15 20:03:17 +01:00
|
|
|
clearTimeout(this.appendBombeTimeout);
|
|
|
|
clearTimeout(this.outputLoaderTimeout);
|
|
|
|
|
2018-05-15 19:36:45 +02:00
|
|
|
const outputLoader = document.getElementById("output-loader"),
|
2019-01-15 20:03:17 +01:00
|
|
|
outputElement = document.getElementById("output-text"),
|
|
|
|
loader = outputLoader.querySelector(".loader");
|
2018-05-15 19:36:45 +02:00
|
|
|
|
|
|
|
if (value) {
|
|
|
|
this.manager.controls.hideStaleIndicator();
|
2019-01-15 20:03:17 +01:00
|
|
|
|
|
|
|
// Start a timer to add the Bombe to the DOM just before we make it
|
|
|
|
// visible so that there is no stuttering
|
|
|
|
this.appendBombeTimeout = setTimeout(function() {
|
|
|
|
loader.appendChild(this.bombeEl);
|
|
|
|
}.bind(this), 150);
|
|
|
|
|
|
|
|
// Show the loading screen
|
|
|
|
this.outputLoaderTimeout = setTimeout(function() {
|
2018-05-15 19:36:45 +02:00
|
|
|
outputElement.disabled = true;
|
|
|
|
outputLoader.style.visibility = "visible";
|
|
|
|
outputLoader.style.opacity = 1;
|
|
|
|
this.manager.controls.toggleBakeButtonFunction(true);
|
|
|
|
}.bind(this), 200);
|
|
|
|
} else {
|
2019-01-15 20:03:17 +01:00
|
|
|
// Remove the Bombe from the DOM to save resources
|
|
|
|
this.outputLoaderTimeout = setTimeout(function () {
|
|
|
|
try {
|
|
|
|
loader.removeChild(this.bombeEl);
|
|
|
|
} catch (err) {}
|
|
|
|
}.bind(this), 500);
|
2018-05-15 19:36:45 +02:00
|
|
|
outputElement.disabled = false;
|
|
|
|
outputLoader.style.opacity = 0;
|
|
|
|
outputLoader.style.visibility = "hidden";
|
|
|
|
this.manager.controls.toggleBakeButtonFunction(false);
|
|
|
|
this.setStatusMsg("");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sets the baking status message value.
|
|
|
|
*
|
|
|
|
* @param {string} msg
|
|
|
|
*/
|
|
|
|
setStatusMsg(msg) {
|
|
|
|
const el = document.querySelector("#output-loader .loading-msg");
|
|
|
|
|
|
|
|
el.textContent = msg;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns true if the output contains carriage returns
|
|
|
|
*
|
|
|
|
* @returns {boolean}
|
|
|
|
*/
|
|
|
|
async containsCR() {
|
|
|
|
await this.getDishStr();
|
|
|
|
return this.dishStr.indexOf("\r") >= 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Retrieves the current dish as a string, returning the cached version if possible.
|
|
|
|
*
|
|
|
|
* @returns {string}
|
|
|
|
*/
|
|
|
|
async getDishStr() {
|
|
|
|
if (this.dishStr) return this.dishStr;
|
|
|
|
|
|
|
|
this.dishStr = await new Promise(resolve => {
|
|
|
|
this.manager.worker.getDishAs(this.app.dish, "string", r => {
|
|
|
|
resolve(r.value);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
return this.dishStr;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Retrieves the current dish as an ArrayBuffer, returning the cached version if possible.
|
|
|
|
*
|
|
|
|
* @returns {ArrayBuffer}
|
|
|
|
*/
|
|
|
|
async getDishBuffer() {
|
|
|
|
if (this.dishBuffer) return this.dishBuffer;
|
|
|
|
|
|
|
|
this.dishBuffer = await new Promise(resolve => {
|
|
|
|
this.manager.worker.getDishAs(this.app.dish, "ArrayBuffer", r => {
|
|
|
|
resolve(r.value);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
return this.dishBuffer;
|
|
|
|
}
|
|
|
|
|
2018-06-03 18:33:13 +02:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Triggers the BackgroundWorker to attempt Magic on the current output.
|
|
|
|
*/
|
|
|
|
backgroundMagic() {
|
2018-07-27 15:37:38 +02:00
|
|
|
this.hideMagicButton();
|
2018-08-03 21:18:19 +02:00
|
|
|
if (!this.app.options.autoMagic) return;
|
|
|
|
|
2018-06-03 18:33:13 +02:00
|
|
|
const sample = this.dishStr ? this.dishStr.slice(0, 1000) :
|
|
|
|
this.dishBuffer ? this.dishBuffer.slice(0, 1000) : "";
|
|
|
|
|
|
|
|
if (sample.length) {
|
|
|
|
this.manager.background.magic(sample);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handles the results of a background Magic call.
|
|
|
|
*
|
|
|
|
* @param {Object[]} options
|
|
|
|
*/
|
|
|
|
backgroundMagicResult(options) {
|
|
|
|
if (!options.length ||
|
|
|
|
!options[0].recipe.length)
|
|
|
|
return;
|
|
|
|
|
|
|
|
const currentRecipeConfig = this.app.getRecipeConfig();
|
|
|
|
const newRecipeConfig = currentRecipeConfig.concat(options[0].recipe);
|
|
|
|
const opSequence = options[0].recipe.map(o => o.op).join(", ");
|
|
|
|
|
2018-07-27 15:37:38 +02:00
|
|
|
this.showMagicButton(opSequence, options[0].data, newRecipeConfig);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Handler for Magic click events.
|
|
|
|
*
|
|
|
|
* Loads the Magic recipe.
|
|
|
|
*
|
|
|
|
* @fires Manager#statechange
|
|
|
|
*/
|
|
|
|
magicClick() {
|
|
|
|
const magicButton = document.getElementById("magic");
|
|
|
|
this.app.setRecipeConfig(JSON.parse(magicButton.getAttribute("data-recipe")));
|
2018-07-27 17:54:49 +02:00
|
|
|
window.dispatchEvent(this.manager.statechange);
|
2018-07-27 15:37:38 +02:00
|
|
|
this.hideMagicButton();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Displays the Magic button with a title and adds a link to a complete recipe.
|
|
|
|
*
|
|
|
|
* @param {string} opSequence
|
|
|
|
* @param {string} result
|
|
|
|
* @param {Object[]} recipeConfig
|
|
|
|
*/
|
|
|
|
showMagicButton(opSequence, result, recipeConfig) {
|
|
|
|
const magicButton = document.getElementById("magic");
|
2018-08-03 20:58:44 +02:00
|
|
|
magicButton.setAttribute("data-original-title", `<i>${opSequence}</i> will produce <span class="data-text">"${Utils.truncate(result, 30)}"</span>`);
|
2018-07-27 15:37:38 +02:00
|
|
|
magicButton.setAttribute("data-recipe", JSON.stringify(recipeConfig), null, "");
|
2018-07-27 17:18:08 +02:00
|
|
|
magicButton.classList.remove("hidden");
|
2018-07-27 15:37:38 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Hides the Magic button and resets its values.
|
|
|
|
*/
|
|
|
|
hideMagicButton() {
|
|
|
|
const magicButton = document.getElementById("magic");
|
2018-07-27 17:18:08 +02:00
|
|
|
magicButton.classList.add("hidden");
|
2018-07-27 15:37:38 +02:00
|
|
|
magicButton.setAttribute("data-recipe", "");
|
|
|
|
magicButton.setAttribute("data-original-title", "Magic!");
|
2018-06-03 18:33:13 +02:00
|
|
|
}
|
|
|
|
|
2018-05-15 19:36:45 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
export default OutputWaiter;
|