From 4a07d52230c49ad4fcf5a83591ab772ed2132549 Mon Sep 17 00:00:00 2001 From: Robin Scholtes Date: Wed, 2 Aug 2023 17:38:52 +1200 Subject: [PATCH] restore recipe on refresh --- src/web/App.mjs | 9 +- src/web/HTMLCategory.mjs | 60 ---------- src/web/HTMLIngredient.mjs | 3 + src/web/HTMLOperation.mjs | 36 ------ src/web/Manager.mjs | 8 +- src/web/TODO.md | 7 ++ src/web/components/c-ingredient-li.mjs | 72 ++++++++++++ src/web/components/c-operation-li.mjs | 15 ++- src/web/components/c-operation-list.mjs | 106 ++++++++++++----- src/web/stylesheets/components/_operation.css | 6 +- src/web/waiters/RecipeWaiter.mjs | 110 ++++++------------ 11 files changed, 215 insertions(+), 217 deletions(-) delete mode 100755 src/web/HTMLCategory.mjs create mode 100644 src/web/components/c-ingredient-li.mjs diff --git a/src/web/App.mjs b/src/web/App.mjs index 074ff88b..911f51b8 100755 --- a/src/web/App.mjs +++ b/src/web/App.mjs @@ -792,13 +792,12 @@ class App { * Fires whenever the input or recipe changes in any way. * * @listens Manager#statechange - * @param {Event} e */ - stateChange(e) { - debounce(function() { + stateChange() { + debounce(() => { this.progress = 0; this.autoBake(); - this.updateURL(true, null, true); + this.updateURL(true); }, 20, "stateChange", this, [])(); } @@ -905,7 +904,7 @@ class App { } /** - * @fires Manager#oplistcreate from nested c-category-li > c-operation-list build() function + * Build a CCategoryList element and append it to #categories */ buildCategoryList() { // double-check if the c-category-list already exists, diff --git a/src/web/HTMLCategory.mjs b/src/web/HTMLCategory.mjs deleted file mode 100755 index 89a7ef36..00000000 --- a/src/web/HTMLCategory.mjs +++ /dev/null @@ -1,60 +0,0 @@ -/** - * @author n1474335 [n1474335@gmail.com] - * @copyright Crown Copyright 2016 - * @license Apache-2.0 - */ - -/** - * Object to handle the creation of operation categories. - */ -class HTMLCategory { - - /** - * HTMLCategory constructor. - * - * @param {string} name - The name of the category. - * @param {boolean} selected - Whether this category is pre-selected or not. - */ - constructor(name, selected) { - this.name = name; - this.selected = selected; - this.opList = []; - } - - - /** - * Adds an operation to this category. - * - * @param {HTMLOperation} operation - The operation to add. - */ - addOperation(operation) { - this.opList.push(operation); - } - - - /** - * Renders the category and all operations within it in HTML. - * - * @returns {HTMLElement} - */ - toHtml() { - const catName = "cat" + this.name.replace(/[\s/\-:_]/g, ""); - - let html = `
- - ${this.name} - -
-
    `; - - for (let i = 0; i < this.opList.length; i++) { - html += this.opList[i].toStubHtml(); - } - - html += "
"; - - return html; - } -} - -export default HTMLCategory; diff --git a/src/web/HTMLIngredient.mjs b/src/web/HTMLIngredient.mjs index 2678de9b..be62f222 100755 --- a/src/web/HTMLIngredient.mjs +++ b/src/web/HTMLIngredient.mjs @@ -8,6 +8,9 @@ import Utils from "../core/Utils.mjs"; /** * Object to handle the creation of operation ingredients. + * + * @TODO: would be nice to refactor this. Move everything to c-ingredient-li and + * implement there accordingly, delete this file */ class HTMLIngredient { diff --git a/src/web/HTMLOperation.mjs b/src/web/HTMLOperation.mjs index 9c19b06f..0b076c34 100755 --- a/src/web/HTMLOperation.mjs +++ b/src/web/HTMLOperation.mjs @@ -4,12 +4,6 @@ * @license Apache-2.0 */ -import HTMLIngredient from "./HTMLIngredient.mjs"; -import Utils from "../core/Utils.mjs"; -import url from "url"; -import {COperationLi} from "./components/c-operation-li.mjs"; - - /** * Object to handle the creation of operations. */ @@ -32,36 +26,6 @@ class HTMLOperation { this.infoURL = config.infoURL; this.manualBake = config.manualBake || false; this.config = config; - this.ingList = []; - - for (let i = 0; i < config.args.length; i++) { - const ing = new HTMLIngredient(config.args[i], this.app, this.manager); - this.ingList.push(ing); - } - } - - - /** - * Renders the operation in HTML as a full operation with ingredients. - * - * @returns {string} - */ - toFullHtml() { - let html = `
${Utils.escapeHtml(this.name)}
-
`; - - for (let i = 0; i < this.ingList.length; i++) { - html += this.ingList[i].toHtml(); - } - - html += `
-
- pause - not_interested -
-
 
`; - - return html; } diff --git a/src/web/Manager.mjs b/src/web/Manager.mjs index 52b09c1c..a7f07748 100755 --- a/src/web/Manager.mjs +++ b/src/web/Manager.mjs @@ -50,10 +50,6 @@ class Manager { * @event Manager#operationremove */ this.operationremove = new CustomEvent("operationremove", {bubbles: true}); - /** - * @event Manager#oplistcreate - */ - this.oplistcreate = new CustomEvent("oplistcreate", {bubbles: true}); /** * @event Manager#statechange */ @@ -89,7 +85,7 @@ class Manager { this.input.setupInputWorker(); this.input.addInput(true); this.worker.setupChefWorker(); - this.recipe.initialiseOperationDragNDrop(); + this.recipe.initDragAndDrop(); this.controls.initComponents(); this.controls.autoBakeChange(); this.bindings.updateKeybList(); @@ -152,7 +148,7 @@ class Manager { document.getElementById("close-ops-dropdown-icon").addEventListener("click", this.ops.closeOpsDropdown.bind(this.ops)); document.getElementById("save-favourites").addEventListener("click", this.ops.saveFavouritesClick.bind(this.ops)); document.getElementById("reset-favourites").addEventListener("click", this.ops.resetFavouritesClick.bind(this.ops)); - this.addDynamicListener("li.operation", "operationadd", this.recipe.opAdd, this.recipe); + this.addDynamicListener("c-operation-li", "operationadd", this.recipe.opAdd, this.recipe); // Recipe this.addDynamicListener(".arg:not(select)", "input", this.recipe.ingChange, this.recipe); diff --git a/src/web/TODO.md b/src/web/TODO.md index e69de29b..1d36915c 100644 --- a/src/web/TODO.md +++ b/src/web/TODO.md @@ -0,0 +1,7 @@ +- ignore dropped item outside of rec-list +- search-results dropdown + +- reordering recipe list +- stupid popovers on deleting favs for instance ( dont always close nicely ) + +- UI tests etc. diff --git a/src/web/components/c-ingredient-li.mjs b/src/web/components/c-ingredient-li.mjs new file mode 100644 index 00000000..64b663c5 --- /dev/null +++ b/src/web/components/c-ingredient-li.mjs @@ -0,0 +1,72 @@ +import Utils from "../../core/Utils.mjs"; +import HTMLIngredient from "../HTMLIngredient.mjs"; + +export class CIngredientLi extends HTMLElement { + constructor(app, name, args) { + super(); + + this.app = app; + this.name = name; + this.args = []; + + for (let i = 0; i < args.length; i++) { + const ing = new HTMLIngredient(args[i], this.app, this.app.manager); + this.args.push(ing); + } + + this.build(); + } + + build() { + const li = document.createElement("li"); + li.classList.add("operation"); + li.setAttribute("data-name", this.name); + + const titleDiv = document.createElement("div"); + titleDiv.classList.add("op-title"); + titleDiv.innerText = this.name; + + const ingredientDiv = document.createElement("div"); + ingredientDiv.classList.add("ingredients"); + + li.appendChild(titleDiv); + li.appendChild(ingredientDiv) + + for (let i = 0; i < this.args.length; i++) { + ingredientDiv.innerHTML += (this.args[i].toHtml()); + } + + const iconsDiv = document.createElement("div"); + iconsDiv.classList.add("recipe-icons"); + + const breakPointIcon = document.createElement("i"); + breakPointIcon.classList.add("material-icons"); + breakPointIcon.classList.add("breakpoint"); + breakPointIcon.setAttribute("title", "Set breakpoint"); + breakPointIcon.setAttribute("break", "false"); + breakPointIcon.setAttribute("data-help-title", "Setting breakpoints"); + breakPointIcon.setAttribute("data-help", "Setting a breakpoint on an operation will cause execution of the Recipe to pause when it reaches that operation."); + breakPointIcon.innerText = "pause"; + + const disableIcon = document.createElement("i"); + disableIcon.classList.add("material-icons"); + disableIcon.classList.add("disable-icon"); + disableIcon.setAttribute("title", "Disable operation"); + disableIcon.setAttribute("disabled", "false"); + disableIcon.setAttribute("data-help-title", "Disabling operations"); + disableIcon.setAttribute("data-help", "Disabling an operation will prevent it from being executed when the Recipe is baked. Execution will skip over the disabled operation and continue with subsequent operations."); + disableIcon.innerText = "not_interested"; + + iconsDiv.appendChild(breakPointIcon); + iconsDiv.appendChild(disableIcon); + + const clearfixDiv = document.createElement("div"); + + li.appendChild(iconsDiv); + li.appendChild(clearfixDiv); + + this.appendChild(li); + } +} + +customElements.define("c-ingredient-li", CIngredientLi); diff --git a/src/web/components/c-operation-li.mjs b/src/web/components/c-operation-li.mjs index c827e7d5..365a6a2e 100644 --- a/src/web/components/c-operation-li.mjs +++ b/src/web/components/c-operation-li.mjs @@ -24,7 +24,6 @@ export class COperationLi extends HTMLElement { this.includeStarIcon = includeStarIcon; this.config = this.app.operations[name]; - // this.ingList = []; this.isFavourite = this.app.isLocalStorageAvailable() && JSON.parse(localStorage.favourites).indexOf(name) >= 0; @@ -37,11 +36,6 @@ export class COperationLi extends HTMLElement { this.observer = new MutationObserver(this.updateFavourite.bind(this)); this.observer.observe(this.querySelector("li"), { attributes: true }); } - - // for (let i = 0; i < this.config.args.length; i++) { - // const ing = new HTMLIngredient(this.config.args[i], this.app, this.manager); - // this.ingList.push(ing); - // } } /** @@ -249,6 +243,15 @@ export class COperationLi extends HTMLElement { this.querySelector("i.star-icon").innerText = "star_outline"; } } + + /** + * Override native cloneNode method so we can clone c-operation-li properly + * with constructor arguments for sortable and cloneable lists + */ + cloneNode() { + const { app, name, icon, includeStarIcon } = this; + return new COperationLi( app, name, icon, includeStarIcon ); + } } diff --git a/src/web/components/c-operation-list.mjs b/src/web/components/c-operation-list.mjs index 75245ca5..ec49aed8 100644 --- a/src/web/components/c-operation-list.mjs +++ b/src/web/components/c-operation-list.mjs @@ -37,7 +37,7 @@ export class COperationList extends HTMLElement { ul.classList.add("op-list"); this.opNames.forEach((opName => { - const li = new COperationLi( + const cOpLi = new COperationLi( this.app, opName, { @@ -47,41 +47,91 @@ export class COperationList extends HTMLElement { this.includeStarIcon ); - ul.appendChild(li); + ul.appendChild(cOpLi); })) if (this.isSortable) { - const sortableList = Sortable.create(ul, { - group: "sorting", - sort: true, - draggable: "c-operation-li", - onFilter: function (e) { - const el = sortableList.closest(e.item); - if (el && el.parentNode) { - $(el).popover("dispose"); - el.parentNode.removeChild(el); - } - }, - onEnd: function(e) { - if (this.removeIntent) { - $(e.item).popover("dispose"); - e.item.remove(); - } - }.bind(this), - }); + this.createSortableList(ul); } else if (!this.app.isMobileView() && this.isCloneable) { - const cloneableList = Sortable.create(ul, { - group: { - name: "recipe", - pull: "clone", - }, - draggable: "c-operation-li", - sort: false - }) + this.createCloneableList(ul, "recipe", "rec-list"); // target name and id can be component params if needed to make it reusable } this.append(ul); } + + /** + * Create a sortable ( not cloneable ) list + * + * @param { HTMLElement } ul + * */ + createSortableList(ul) { + const sortableList = Sortable.create(ul, { + group: "sorting", + sort: true, + draggable: "c-operation-li", + onFilter: function (e) { + const el = sortableList.closest(e.item); + if (el && el.parentNode) { + $(el).popover("dispose"); + el.parentNode.removeChild(el); + } + }, + onEnd: function(e) { + if (this.app.manager.recipe.removeIntent) { + $(e.item).popover("dispose"); + e.item.remove(); + } + }.bind(this), + }); + } + + /** + * Create a cloneable ( not sortable ) list + * + * @param { HTMLElement } ul + * @param { string } targetListName + * @param { string } targetListId + * */ + createCloneableList(ul, targetListName, targetListId) { + Sortable.create(ul, { + group: { + name: targetListName, + pull: "clone", + put: false, + }, + draggable: "c-operation-li", + sort: false, + setData: function(dataTransfer, dragEl) { + dataTransfer.setData("Text", dragEl.querySelector("li").getAttribute("data-name")); + }, + onStart: function(e) { + // Removes popover element and event bindings from the dragged operation but not the + // event bindings from the one left in the operations list. Without manually removing + // these bindings, we cannot re-initialise the popover on the stub operation. + $(e.item) + .popover("dispose") + .removeData("bs.popover") + .off("mouseenter") + .off("mouseleave") + .attr("data-toggle", "popover-disabled"); + $(e.clone) + .off(".popover") + .removeData("bs.popover"); + }, + // @todo: popovers dont display anymore after dragging into recipe list and then hovering the op + onEnd: ({item}) => { + if (item.parentNode.id === targetListId) { + this.app.manager.recipe.addOperation(item.name); + item.remove(); + return; + } + + if (item.parentNode.id !== targetListId) { + return; + } + } + }); + } } customElements.define("c-operation-list", COperationList); diff --git a/src/web/stylesheets/components/_operation.css b/src/web/stylesheets/components/_operation.css index b994a1f4..56b581d3 100755 --- a/src/web/stylesheets/components/_operation.css +++ b/src/web/stylesheets/components/_operation.css @@ -15,7 +15,7 @@ border-top: none; border-left: none; border-right: none; - cursor: pointer; + cursor: default; } @media only screen and (min-width: 768px){ @@ -272,14 +272,14 @@ input.toggle-string { word-break: break-all; } -.recip-icons { +.recipe-icons { position: absolute; top: 13px; right: 10px; height: 16px; } -.recip-icons i { +.recipe-icons i { margin-right: 10px; vertical-align: baseline; float: right; diff --git a/src/web/waiters/RecipeWaiter.mjs b/src/web/waiters/RecipeWaiter.mjs index f7aeb5ae..17c4f1fa 100755 --- a/src/web/waiters/RecipeWaiter.mjs +++ b/src/web/waiters/RecipeWaiter.mjs @@ -8,6 +8,7 @@ import HTMLOperation from "../HTMLOperation.mjs"; import Sortable from "sortablejs"; import Utils from "../../core/Utils.mjs"; import {escapeControlChars} from "../utils/editorUtils.mjs"; +import {CIngredientLi} from "../components/c-ingredient-li.mjs"; /** @@ -31,30 +32,30 @@ class RecipeWaiter { /** * Sets up the drag and drop capability for operations in the operations and recipe areas. */ - initialiseOperationDragNDrop() { + initDragAndDrop() { const recList = document.getElementById("rec-list"); - const isMobileView = this.app.isMobileView(); // Recipe list Sortable.create(recList, { group: "recipe", sort: true, - swapThreshold: isMobileView ? 0.60 : 0.3, - animation: isMobileView ? 400 : 200, - delay: isMobileView ? 200 : 0, + draggable: "c-ingredient-li", + swapThreshold: this.app.isMobileView ? 0.60 : 0.3, + animation: this.app.isMobileView ? 400 : 200, + delay: this.app.isMobileView ? 200 : 0, filter: ".arg", preventOnFilter: false, setData: function(dataTransfer, dragEl) { - dataTransfer.setData("Text", dragEl.getAttribute("data-name")); + dataTransfer.setData("Text", dragEl.querySelector("li").getAttribute("data-name")); }, - onEnd: function(evt) { + onEnd: function(e) { if (this.removeIntent) { - evt.item.remove(); - evt.target.dispatchEvent(this.manager.operationremove); + e.item.remove(); + e.target.dispatchEvent(this.manager.operationremove); } }.bind(this), - onSort: function(evt) { - if (evt.from.id === "rec-list") { + onSort: function(e) { + if (e.from.id === "rec-list") { document.dispatchEvent(this.manager.statechange); } }.bind(this) @@ -83,42 +84,6 @@ class RecipeWaiter { } - /** - * Creates a drag-n-droppable seed list of operations. - * - * @param {element} listEl - The list to initialise - */ - createSortableSeedList(listEl) { - Sortable.create(listEl, { - group: { - name: "recipe", - pull: "clone", - put: false, - }, - draggable: ".operation", - sort: false, - setData: function(dataTransfer, dragEl) { - dataTransfer.setData("Text", dragEl.getAttribute("data-name")); - }, - onStart: function(evt) { - // Removes popover element and event bindings from the dragged operation but not the - // event bindings from the one left in the operations list. Without manually removing - // these bindings, we cannot re-initialise the popover on the stub operation. - $(evt.item) - .popover("dispose") - .removeData("bs.popover") - .off("mouseenter") - .off("mouseleave") - .attr("data-toggle", "popover-disabled"); - $(evt.clone) - .off(".popover") - .removeData("bs.popover"); - }, - onEnd: this.opSortEnd.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 @@ -128,6 +93,7 @@ class RecipeWaiter { * @param {Event} evt */ opSortEnd(evt) { + console.log(evt); if (this.removeIntent && evt.item.parentNode.id === "rec-list") { evt.item.remove(); return; @@ -135,21 +101,22 @@ class RecipeWaiter { // Reinitialise the popover on the original element in the ops list because for some reason it // gets destroyed and recreated. If the clone isn't in the ops list, we use the original item instead. - let enableOpsElement; - if (evt.clone?.parentNode?.classList?.contains("op-list")) { - enableOpsElement = evt.clone; - } else { - enableOpsElement = evt.item; - $(evt.item).attr("data-toggle", "popover"); - } - this.manager.ops.enableOpPopover(enableOpsElement); + // let enableOpsElement; + // if (evt.clone?.parentNode?.classList?.contains("op-list")) { + // enableOpsElement = evt.clone; + // } else { + // enableOpsElement = evt.item; + // $(evt.item).attr("data-toggle", "popover"); + // } + + // this.manager.ops.enableOpPopover(enableOpsElement); if (evt.item.parentNode.id !== "rec-list") { return; } - this.buildRecipeOperation(evt.item); - evt.item.dispatchEvent(this.manager.operationadd); + this.buildRecipeOperation(evt.item.name); + // evt.item.dispatchEvent(this.manager.operationadd); } @@ -369,15 +336,13 @@ class RecipeWaiter { * 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 + * @param {string} name - The operation stub element from the operations pane */ - buildRecipeOperation(el) { - const opName = el.textContent; - const op = new HTMLOperation(opName, this.app.operations[opName], this.app, this.manager); - el.innerHTML = op.toFullHtml(); + buildRecipeOperation(name) { + const op = new CIngredientLi(this.app, name, this.app.operations[name].args); - if (this.app.operations[opName].flowControl) { - el.classList.add("flow-control-op"); + if (this.app.operations[name].flowControl) { + op.classList.add("flow-control-op"); } // Disable auto-bake if this is a manual op @@ -385,6 +350,8 @@ class RecipeWaiter { this.manager.controls.setAutoBake(false); this.app.alert("Auto-Bake is disabled by default when using this operation.", 5000); } + + return op; } @@ -392,24 +359,19 @@ class RecipeWaiter { * Adds the specified operation to the recipe * * @fires Manager#operationadd + * @fires Manager#statechange * @param {string} name - The name of the operation to add - * @returns {element} */ addOperation(name) { - const item = document.createElement("li"); - item.setAttribute("data-name", name); - - item.classList.add("operation"); - item.innerText = name; - this.buildRecipeOperation(item); + let item = this.buildRecipeOperation(name); document.getElementById("rec-list").appendChild(item); $(item).find("[data-toggle='tooltip']").tooltip(); item.dispatchEvent(this.manager.operationadd); + document.dispatchEvent(this.app.manager.statechange); this.manager.ops.updateListItemsClasses("#rec-list", "selected"); - return item; } @@ -471,7 +433,9 @@ class RecipeWaiter { * @param {Event} e */ opAdd(e) { - log.debug(`'${e.target.getAttribute("data-name")}' added to recipe`); + console.log(e); + log.debug(`'${e.target.querySelector("li").getAttribute("data-name")}' added to recipe`); + console.log(e.target.querySelector("li").getAttribute("data-name")); this.triggerArgEvents(e.target); window.dispatchEvent(this.manager.statechange); }