/* globals Sortable */ /** * Waiter to handle events related to the recipe. * * @author n1474335 [n1474335@gmail.com] * @copyright Crown Copyright 2016 * @license Apache-2.0 * * @constructor * @param {HTMLApp} app - The main view object for CyberChef. * @param {Manager} manager - The CyberChef event manager. */ var RecipeWaiter = function(app, manager) { this.app = app; this.manager = manager; this.remove_intent = false; }; /** * Sets up the drag and drop capability for operations in the operations and recipe areas. */ RecipeWaiter.prototype.initialise_operation_drag_n_drop = function() { var rec_list = document.getElementById("rec_list"), op_lists = document.querySelectorAll(".category .op_list"); // Recipe list Sortable.create(rec_list, { group: "recipe", sort: true, animation: 0, delay: 0, filter: ".arg-input,.arg", // Relies on commenting out a line in Sortable.js which calls evt.preventDefault() setData: function(dataTransfer, drag_el) { dataTransfer.setData("Text", drag_el.querySelector(".arg-title").textContent); }, onEnd: function(evt) { if (this.remove_intent) { evt.item.remove(); evt.target.dispatchEvent(this.manager.operationremove); } }.bind(this) }); Sortable.utils.on(rec_list, "dragover", function() { this.remove_intent = false; }.bind(this)); Sortable.utils.on(rec_list, "dragleave", function() { this.remove_intent = true; this.app.progress = 0; }.bind(this)); // Favourites category document.querySelector("#categories a").addEventListener("dragover", this.fav_dragover.bind(this)); document.querySelector("#categories a").addEventListener("dragleave", this.fav_dragleave.bind(this)); document.querySelector("#categories a").addEventListener("drop", this.fav_drop.bind(this)); }; /** * Creates a drag-n-droppable seed list of operations. * * @param {element} list_el - The list the initialise */ RecipeWaiter.prototype.create_sortable_seed_list = function(list_el) { Sortable.create(list_el, { group: { name: "recipe", pull: "clone", put: false }, sort: false, setData: function(dataTransfer, drag_el) { dataTransfer.setData("Text", drag_el.textContent); }, onStart: function(evt) { $(evt.item).popover("destroy"); evt.item.setAttribute("data-toggle", "popover-disabled"); }, onEnd: this.op_sort_end.bind(this) }); }; /** * Handler for operation sort end events. * Removes the operation from the list if it has been dropped outside. If not, adds it to the list * at the appropriate place and initialises it. * * @fires Manager#operationadd * @param {event} evt */ RecipeWaiter.prototype.op_sort_end = function(evt) { if (this.remove_intent) { if (evt.item.parentNode.id == "rec_list") { evt.item.remove(); } return; } // Reinitialise the popover on the original element in the ops list because for some reason it // gets destroyed and recreated. $(evt.clone).popover(); $(evt.clone).children("[data-toggle=popover]").popover(); if (evt.item.parentNode.id !== "rec_list") { return; } this.build_recipe_operation(evt.item); evt.item.dispatchEvent(this.manager.operationadd); }; /** * Handler for favourite dragover events. * If the element being dragged is an operation, displays a visual cue so that the user knows it can * be dropped here. * * @param {event} e */ RecipeWaiter.prototype.fav_dragover = function(e) { if (e.dataTransfer.effectAllowed !== "move") return false; e.stopPropagation(); e.preventDefault(); if (e.target.className && e.target.className.indexOf("category-title") > -1) { // Hovering over the a e.target.classList.add("favourites-hover"); } else if (e.target.parentNode.className && e.target.parentNode.className.indexOf("category-title") > -1) { // Hovering over the Edit button e.target.parentNode.classList.add("favourites-hover"); } else if (e.target.parentNode.parentNode.className && e.target.parentNode.parentNode.className.indexOf("category-title") > -1) { // Hovering over the image on the Edit button e.target.parentNode.parentNode.classList.add("favourites-hover"); } }; /** * Handler for favourite dragleave events. * Removes the visual cue. * * @param {event} e */ RecipeWaiter.prototype.fav_dragleave = function(e) { e.stopPropagation(); e.preventDefault(); document.querySelector("#categories a").classList.remove("favourites-hover"); }; /** * Handler for favourite drop events. * Adds the dragged operation to the favourites list. * * @param {event} e */ RecipeWaiter.prototype.fav_drop = function(e) { e.stopPropagation(); e.preventDefault(); e.target.classList.remove("favourites-hover"); var op_name = e.dataTransfer.getData("Text"); this.app.add_favourite(op_name); }; /** * Handler for ingredient change events. * * @fires Manager#statechange */ RecipeWaiter.prototype.ing_change = function() { window.dispatchEvent(this.manager.statechange); }; /** * Handler for disable click events. * Updates the icon status. * * @fires Manager#statechange * @param {event} e */ RecipeWaiter.prototype.disable_click = function(e) { var icon = e.target; if (icon.getAttribute("disabled") == "false") { icon.setAttribute("disabled", "true"); icon.classList.add("disable-icon-selected"); icon.parentNode.parentNode.classList.add("disabled"); } else { icon.setAttribute("disabled", "false"); icon.classList.remove("disable-icon-selected"); icon.parentNode.parentNode.classList.remove("disabled"); } this.app.progress = 0; window.dispatchEvent(this.manager.statechange); }; /** * Handler for breakpoint click events. * Updates the icon status. * * @fires Manager#statechange * @param {event} e */ RecipeWaiter.prototype.breakpoint_click = function(e) { var bp = e.target; if (bp.getAttribute("break") == "false") { bp.setAttribute("break", "true"); bp.classList.add("breakpoint-selected"); } else { bp.setAttribute("break", "false"); bp.classList.remove("breakpoint-selected"); } window.dispatchEvent(this.manager.statechange); }; /** * Handler for operation doubleclick events. * Removes the operation from the recipe and auto bakes. * * @fires Manager#statechange * @param {event} e */ RecipeWaiter.prototype.operation_dblclick = function(e) { e.target.remove(); window.dispatchEvent(this.manager.statechange); }; /** * Handler for operation child doubleclick events. * Removes the operation from the recipe. * * @fires Manager#statechange * @param {event} e */ RecipeWaiter.prototype.operation_child_dblclick = function(e) { e.target.parentNode.remove(); window.dispatchEvent(this.manager.statechange); }; /** * Generates a configuration object to represent the current recipe. * * @returns {recipe_config} */ RecipeWaiter.prototype.get_config = function() { var config = [], ingredients, ing_list, disabled, bp, item, operations = document.querySelectorAll("#rec_list li.operation"); for (var i = 0; i < operations.length; i++) { ingredients = []; disabled = operations[i].querySelector(".disable-icon"); bp = operations[i].querySelector(".breakpoint"); ing_list = operations[i].querySelectorAll(".arg"); for (var j = 0; j < ing_list.length; j++) { if (ing_list[j].getAttribute("type") == "checkbox") { // checkbox ingredients[j] = ing_list[j].checked; } else if (ing_list[j].classList.contains("toggle-string")) { // toggle_string ingredients[j] = { option: ing_list[j].previousSibling.children[0].textContent.slice(0, -1), string: ing_list[j].value }; } else { // all others ingredients[j] = ing_list[j].value; } } item = { op: operations[i].querySelector(".arg-title").textContent, args: ingredients }; if (disabled && disabled.getAttribute("disabled") == "true") { item.disabled = true; } if (bp && bp.getAttribute("break") == "true") { item.breakpoint = true; } config.push(item); } return config; }; /** * Moves or removes the breakpoint indicator in the recipe based on the position. * * @param {number} position */ RecipeWaiter.prototype.update_breakpoint_indicator = function(position) { var operations = document.querySelectorAll("#rec_list li.operation"); for (var i = 0; i < operations.length; i++) { if (i == position) { operations[i].classList.add("break"); } else { operations[i].classList.remove("break"); } } }; /** * Given an operation stub element, this function converts it into a full recipe element with * arguments. * * @param {element} el - The operation stub element from the operations pane */ RecipeWaiter.prototype.build_recipe_operation = function(el) { var op_name = el.textContent; var op = new HTMLOperation(op_name, this.app.operations[op_name], this.app, this.manager); el.innerHTML = op.to_full_html(); if (this.app.operations[op_name].flow_control) { el.classList.add("flow-control-op"); } // Disable auto-bake if this is a manual op - this should be moved to the 'operationadd' // handler after event restructuring if (op.manual_bake && this.app.auto_bake_) { this.manager.controls.set_auto_bake(false); this.app.alert("Auto-Bake is disabled by default when using this operation.", "info", 5000); } }; /** * Adds the specified operation to the recipe. * * @fires Manager#operationadd * @param {string} name - The name of the operation to add * @returns {element} */ RecipeWaiter.prototype.add_operation = function(name) { var item = document.createElement("li"); item.classList.add("operation"); item.innerHTML = name; this.build_recipe_operation(item); document.getElementById("rec_list").appendChild(item); item.dispatchEvent(this.manager.operationadd); return item; }; /** * Removes all operations from the recipe. * * @fires Manager#operationremove */ RecipeWaiter.prototype.clear_recipe = function() { var rec_list = document.getElementById("rec_list"); while (rec_list.firstChild) { rec_list.removeChild(rec_list.firstChild); } rec_list.dispatchEvent(this.manager.operationremove); }; /** * Handler for operation dropdown events from toggle_string arguments. * Sets the selected option as the name of the button. * * @param {event} e */ RecipeWaiter.prototype.dropdown_toggle_click = function(e) { var el = e.target, button = el.parentNode.parentNode.previousSibling; button.innerHTML = el.textContent + " "; this.ing_change(); }; /** * Handler for operationadd events. * * @listens Manager#operationadd * @fires Manager#statechange * @param {event} e */ RecipeWaiter.prototype.op_add = function(e) { window.dispatchEvent(this.manager.statechange); }; /** * Handler for operationremove events. * * @listens Manager#operationremove * @fires Manager#statechange * @param {event} e */ RecipeWaiter.prototype.op_remove = function(e) { window.dispatchEvent(this.manager.statechange); };