introducing and refactoring to using custom components for building category / cat lists / operation lists. This allows us to group relevant functionality more efficiently, easier maintenance in the future. We could, one by one, refactor and encapsulate components across CC in this manner. A very convenient and much needed effect of this implementation is that these components can hold functionality and references exclusively pertaining to themselves. Separating said functionality increases code compartmentalisation while references and event listeners requiring these references are guaranteed to be up to date with the component at all times.

This commit is contained in:
Robin Scholtes 2023-06-16 12:09:04 +12:00
parent 1e5190cd7d
commit b1b0be254b
17 changed files with 489 additions and 313 deletions

View file

@ -90,7 +90,7 @@ CyberChef is built to support
## Node.js support
CyberChef is built to fully support Node.js `v16`. For more information, see the Node API page in the project [wiki pages](https://github.com/gchq/CyberChef/wiki)
CyberChef is built to fully support Node.js `v18`. For more information, see the Node API page in the project [wiki pages](https://github.com/gchq/CyberChef/wiki)
## Contributing

View file

@ -7,11 +7,11 @@
import Utils, { debounce } from "../core/Utils.mjs";
import {fromBase64} from "../core/lib/Base64.mjs";
import Manager from "./Manager.mjs";
import HTMLCategory from "./HTMLCategory.mjs";
import HTMLOperation from "./HTMLOperation.mjs";
import Split from "split.js";
import moment from "moment-timezone";
import cptable from "codepage";
import {CCategoryLi} from "./components/c-category-li.mjs";
import {CCategoryList} from "./components/c-category-list.mjs";
/**
@ -65,9 +65,9 @@ class App {
setup() {
document.dispatchEvent(this.manager.appstart);
this.initialiseUI();
this.loadLocalStorage();
this.buildOperationsList();
this.buildUI();
this.buildCategoryList();
this.manager.setup();
this.manager.output.saveBombe();
this.uriParams = this.getURIParams();
@ -249,7 +249,7 @@ class App {
*
* @param {boolean} [minimise=false] - Set this flag if attempting to minimise frames to 0 width
*/
initialiseUI() {
buildUI() {
if (this.isMobileView()) {
this.setMobileUI();
} else {
@ -416,54 +416,41 @@ class App {
}
favourites.push(name);
this.updateFavourites(favourites, false);
this.updateFavourites(favourites);
}
/**
* Update favourites in localstorage, load the updated
* favourites and re-render the favourites category.
*
* Apply 'favourite' classes and set icons appropriately
* favourites and re-render c-category-li [favourites]
*
* @param {string[]} favourites
* @param {boolean} updateAllIcons
*/
updateFavourites(favourites, updateAllIcons = true) {
updateFavourites(favourites) {
this.saveFavourites(favourites);
this.loadFavourites();
this.buildFavouritesCategory();
if (updateAllIcons) {
this.manager.ops.updateOpsFavouriteIcons();
}
/* Rebuild only the favourites category */
if (!this.isMobileView()) {
this.manager.recipe.initialiseOperationDragNDrop();
// double-check if the first category is indeed "catFavourites",
if (document.querySelector("c-category-list > ul > c-category-li > li > a[data-target='#catFavourites']")) {
// then destroy
document.querySelectorAll("c-category-list > ul > c-category-li")[0].remove();
// and rebuild it
const favCatConfig = this.categories.find( catConfig => catConfig.name === "Favourites");
const favouriteCategory = new CCategoryLi(
this,
favCatConfig,
this.operations,
false
);
// finally prepend it to c-category-list
document.querySelector("c-category-list > ul").prepend(favouriteCategory);
}
}
/**
* Build the Favourites category and insert it into #categories
*/
buildFavouritesCategory() {
// Move the edit button away before we re-render the favourite category
// A note: this is hacky and should be solved in a proper way once the entire
// codebase and architecture gets a thorough refactoring.
document.body.appendChild(document.getElementById("edit-favourites"));
// rerender the favourites category
const catConf = this.categories.find((cat) => cat.name === "Favourites");
this.removeCategoryFromDOM("catFavourites");
this.addCategoryToElement(catConf, document.getElementById("categories"));
// Restore "edit favourites" after the favourites category has been re-rendered, and reinitialize the listener
this.setEditFavourites();
document.getElementById("edit-favourites").addEventListener("click", this.manager.ops.editFavouritesClick.bind(this.manager.ops));
}
/**
* Gets the URI params from the window and parses them to extract the actual values.
*
@ -862,19 +849,19 @@ class App {
* @param {boolean} minimise
*/
setDesktopUI(minimise) {
$("[data-toggle=tooltip]").tooltip("enable");
this.setCompileMessage();
this.setDesktopSplitter(minimise);
this.adjustComponentSizes();
$("[data-toggle=tooltip]").tooltip("enable");
}
/**
* Set mobile UI ( on init and on window resize events )
*/
setMobileUI() {
$("[data-toggle=tooltip]").tooltip("disable");
this.setMobileSplitter();
this.assignAvailableHeight();
$("[data-toggle=tooltip]").tooltip("disable");
}
/**
@ -902,98 +889,18 @@ class App {
document.getElementById("operations-dropdown").style.maxHeight = `${window.innerHeight - (bannerHeight+operationsHeight)}px`;
}
/**
* Create the template for a single category
*
* @param {object} catConf
* @param {boolean} selected
* @fires Manager#oplistcreate in nested c-category-li build() function
*/
createCategory(catConf, selected) {
const cat = new HTMLCategory(catConf.name, selected);
buildCategoryList() {
const categoryList = new CCategoryList(
this,
this.categories,
this.operations
);
categoryList.build();
catConf.ops.forEach(opName => {
if (!(opName in this.operations)) {
log.warn(`${opName} could not be found.`);
return;
}
const op = new HTMLOperation(opName, this.operations[opName], this, this.manager);
cat.addOperation(op);
});
return cat;
}
/**
* Add a category to an element
*
* @param {object} catConf
* @param {HTMLElement} targetElement
* @param {boolean} selected ( false by default )
*/
addCategoryToElement(catConf, targetElement, selected = false) {
const cat = this.createCategory(catConf, selected);
const catName = "cat" + cat.name.replace(/[\s/\-:_]/g, "");
if (catConf.name === "Favourites") {
targetElement.innerHTML = cat.toHtml() + targetElement.innerHTML;
} else {
targetElement.innerHTML += cat.toHtml();
}
targetElement.querySelector(`#${catName} > .op-list`).dispatchEvent(this.manager.oplistcreate);
}
/**
* Remove a category from the DOM
*
* @param {string} catName
*/
removeCategoryFromDOM(catName) {
document.querySelector(`#${catName}`).parentNode.remove();
}
/**
* Build the #operations accordion list with the categories and operations specified in the
* view constructor.
*
* @fires Manager#oplistcreate
*/
buildOperationsList() {
const targetElement = document.getElementById("categories");
// Move the edit button away before we overwrite the categories.
// A note: this is hacky and should be solved in a proper way once the entire
// codebase and architecture gets a thorough refactoring.
document.body.appendChild(document.getElementById("edit-favourites"));
this.categories.forEach((catConf, index) => {
this.addCategoryToElement(catConf, targetElement, index === 0);
});
this.setEditFavourites();
this.manager.ops.updateListItemsClasses("#catFavourites > .op-list", "favourite");
this.manager.ops.updateListItemsClasses("#rec-list", "selected");
}
/**
* Set appropriate attributes and values and append "edit-favourites"
* to the favourites category
*/
setEditFavourites() {
// Add edit button to first category (Favourites)
const favCat = document.querySelector("#categories a[data-target='#catFavourites']");
favCat.appendChild(document.getElementById("edit-favourites"));
favCat.setAttribute("data-help-title", "Favourite operations");
favCat.setAttribute("data-help", `<p>This category displays your favourite operations.</p>
<ul>
<li><b>To add:</b> drag an operation over the Favourites category</li>
<li><b>To reorder:</b> Click on the 'Edit favourites' button and drag operations up and down in the list provided</li>
<li><b>To remove:</b> Click on the 'Edit favourites' button and hit the delete button next to the operation you want to remove</li>
</ul>`);
document.querySelector("#categories").appendChild( categoryList );
}
}

View file

@ -35,14 +35,11 @@ class HTMLCategory {
/**
* Renders the category and all operations within it in HTML.
*
* @returns {string}
*
* @TODO: it will be better if this, and the other HTMLX.js, toHTML() functions
* created HTML elements rather than insert and change stringified html, but that
* would be part of a bigger refactoring adventure
* @returns {HTMLElement}
*/
toHtml() {
const catName = "cat" + this.name.replace(/[\s/\-:_]/g, "");
let html = `<div class="panel category">
<a class="category-title" data-toggle="collapse" data-target="#${catName}">
${this.name}
@ -55,9 +52,9 @@ class HTMLCategory {
}
html += "</ul></div></div>";
return html;
}
}
export default HTMLCategory;

View file

@ -7,6 +7,7 @@
import HTMLIngredient from "./HTMLIngredient.mjs";
import Utils from "../core/Utils.mjs";
import url from "url";
import {COperationLi} from "./components/c-operation-li.mjs";
/**
@ -40,47 +41,6 @@ class HTMLOperation {
}
/**
* Renders the operation in HTML as a stub operation with no ingredients.
*
* @returns {string}
*/
toStubHtml(removeIcon) {
// this.name is polluted with HTML if it originates from search-results, so before
// returning the HTML we purge this.name from any HTML for the data-name attribute
const name = this.name.replace(/(<([^>]+)>)/ig, "");
// check if local storage is available *and* has favourites at all ( otherwise default favs are used )
const isFavourite = this.app.isLocalStorageAvailable() && localStorage.favourites?.includes(name);
let html = `<li data-name="${name}" class="operation ${isFavourite && "favourite"}"`;
if (this.description) {
const infoLink = this.infoURL ? `<hr>${titleFromWikiLink(this.infoURL)}` : "";
html += ` data-container='body' data-toggle='popover' data-placement='left'
data-content="${this.description}${infoLink}" data-html='true' data-trigger='hover'
data-boundary='viewport'`;
}
html += ">" + this.name;
if (removeIcon) {
html += "<i class='material-icons remove-icon op-icon'>delete</i>";
} else if (!removeIcon && this.app.isMobileView()) {
html += "<i class='material-icons check-icon op-icon'>check</i>";
}
if (this.app.isMobileView()) {
html += `<i title="${this.name}" class="material-icons icon-add-favourite star-icon op-icon">${isFavourite ? "star" : "star_outline"}</i>`;
}
html += "</li>";
return html;
}
/**
* Renders the operation in HTML as a full operation with ingredients.
*
@ -154,36 +114,6 @@ class HTMLOperation {
this.description = desc;
}
}
}
/**
* Given a URL for a Wikipedia (or other wiki) page, this function returns a link to that page.
*
* @param {string} urlStr
* @returns {string}
*/
function titleFromWikiLink(urlStr) {
const urlObj = url.parse(urlStr);
let wikiName = "",
pageTitle = "";
switch (urlObj.host) {
case "forensicswiki.xyz":
wikiName = "Forensics Wiki";
pageTitle = urlObj.query.substr(6).replace(/_/g, " "); // Chop off 'title='
break;
case "wikipedia.org":
wikiName = "Wikipedia";
pageTitle = urlObj.pathname.substr(6).replace(/_/g, " "); // Chop off '/wiki/'
break;
default:
// Not a wiki link, return full URL
return `<a href='${urlStr}' target='_blank'>More Information<i class='material-icons inline-icon'>open_in_new</i></a>`;
}
return `<a href='${urlObj.href}' target='_blank'>${pageTitle}<i class='material-icons inline-icon'>open_in_new</i></a> on ${wikiName}`;
}
export default HTMLOperation;

View file

@ -156,7 +156,7 @@ class Manager {
document.getElementById("reset-favourites").addEventListener("click", this.ops.resetFavouritesClick.bind(this.ops));
this.addDynamicListener(".op-list", "oplistcreate", this.ops.opListCreate, this.ops);
this.addDynamicListener("li.operation", "operationadd", this.recipe.opAdd, this.recipe);
this.addDynamicListener(".icon-add-favourite", "click", this.ops.onIconFavouriteClick, this.ops);
// this.addDynamicListener(".op-icon.star-icon", "click", this.ops.onIconFavouriteClick, this.ops);
// Recipe
this.addDynamicListener(".arg:not(select)", "input", this.recipe.ingChange, this.recipe);

0
src/web/TODO.md Normal file
View file

View file

@ -0,0 +1,184 @@
import {COperationLi} from "./c-operation-li.mjs";
/**
* c(ustom element)-category-li ( list item )
*
* @param {App} app - The main view object for CyberChef
* @param {CatConf} category - The category and operations to be populated.
* @param {Object.<string, OpConf>} operations - The list of operation configuration objects.
* @param {Boolean} isExpanded - expand the category on init or not
* */
export class CCategoryLi extends HTMLElement {
constructor(
app,
category,
operations,
isExpanded
) {
super();
this.app = app;
this.category = category;
this.operations = operations;
this.label = category.name;
this.isExpanded = isExpanded;
this.build();
this.addEventListener("click", this.handleClick.bind(this));
}
// /**
// * Handle click
// *
// * @param {Event} e
// */
// handleClick(e) {
// if (e.target === this.querySelector("button")) {
// // todo back to this "hitbox" issue w the icon inside the button
// this.app.manager.ops.editFavouritesClick(e);
// }
// }
/**
* Build the li element
*/
buildListItem() {
const li = document.createElement("li");
li.classList.add("panel");
li.classList.add("category");
return li;
};
/**
* Build the anchor element
*/
buildAnchor() {
const a = document.createElement("a");
a.classList.add("category-title");
a.setAttribute("data-toggle", "collapse");
a.setAttribute("data-target", `#${"cat" + this.label.replace(/[\s/\-:_]/g, "")}`);
a.innerText = this.label;
if (this.label === "Favourites"){
const editFavouritesButton = this.buildEditFavourites(a);
a.setAttribute("data-help-title", "Favourite operations");
a.setAttribute("data-help", `<p>This category displays your favourite operations.</p>
<ul>
<li><b>To add:</b> Click on the star icon of an operation or drag an operation over the Favourites category on desktop devices</li>
<li><b>To reorder:</b> Click on the 'Edit favourites' button and drag operations up and down in the list provided</li>
<li><b>To remove:</b> Click on the 'Edit favourites' button and hit the delete button next to the operation you want to remove</li>
</ul>`);
a.appendChild(editFavouritesButton);
}
return a;
};
/**
* Build the collapsable panel that contains the op-list for this category
*/
buildCollapsablePanel(){
const div = document.createElement("div");
div.setAttribute("id", `${"cat" + this.label.replace(/[\s/\-:_]/g, "")}`);
div.setAttribute("data-parent", "#categories");
div.classList.add("panel-collapse");
div.classList.add("collapse");
if (this.isExpanded) {
div.classList.add("show");
}
return div;
};
/**
* Build the op-list for this category
*
* @param {string[]} opNames
*/
buildOperationList(opNames) {
return opNames.map(opName => {
if (!(opName in this.operations)) {
log.warn(`${opName} could not be found.`);
return;
}
return new COperationLi(
this.app,
opName,
{
class: "check-icon",
innerText: "check"
},
this.operations[opName]
);
});
}
/**
* Build c-category-li and dispatch event oplistcreate
*/
build() {
const ul = document.createElement("ul");
ul.classList.add("op-list");
const li = this.buildListItem();
const a = this.buildAnchor();
const div = this.buildCollapsablePanel();
li.appendChild(a);
li.appendChild(div);
div.appendChild(ul);
this.appendChild(li);
this.buildOperationList(this.category.ops).forEach(operationListItem =>
ul.appendChild(operationListItem)
);
ul.dispatchEvent(this.app.manager.oplistcreate);
}
/**
* Append a c-operation-li to this op-list
*
* @param {HTMLElement} cOperationLiElement
*/
appendOperation(cOperationLiElement) {
this.querySelector('li > div > ul').appendChild(cOperationLiElement);
}
/**
* If this category is Favourites, build and return the star icon to the category
*/
buildEditFavourites() {
const button = document.createElement("button");
const icon = document.createElement("i");
button.setAttribute("id", "edit-favourites");
button.setAttribute("type", "button");
button.setAttribute("data-toggle", "tooltip");
button.setAttribute("title", "Edit favourites");
button.classList.add("btn");
button.classList.add("btn-warning");
button.classList.add("bmd-btn-icon");
icon.classList.add("material-icons");
icon.innerText = "star";
button.appendChild(icon);
return button;
}
}
customElements.define("c-category-li", CCategoryLi);

View file

@ -0,0 +1,40 @@
import {CCategoryLi} from "./c-category-li.mjs";
/**
* c(ustom element)-category-list
*
* @param {App} app - The main view object for CyberChef
* @param {CatConf[]} categories - The list of categories and operations to be populated.
* @param {Object.<string, OpConf>} operations - The list of operation configuration objects.
**/
export class CCategoryList extends HTMLElement {
constructor( app, categories, operations ) {
super();
this.app = app;
this.categories = categories;
this.operations = operations;
}
/**
* Build c-category-list
*/
build() {
const ul = document.createElement("ul");
this.categories.forEach((category, index) => {
const cat = new CCategoryLi(
this.app,
category,
this.operations,
index === 0
);
ul.appendChild(cat);
});
this.append(ul);
}
}
customElements.define("c-category-list", CCategoryList);

View file

@ -0,0 +1,176 @@
import url from "url";
/**
* c(ustom element)-operation-li ( list item )
*
* @param {App} app - The main view object for CyberChef
* @param {string} name - The name of the operation
* @param {Object} icon - { class: string, innerText: string } - The optional and customizable icon displayed on the right side of the operation
* @param {Object} config - The configuration object for this operation.
*/
export class COperationLi extends HTMLElement {
constructor(
app,
name,
icon,
config
) {
super();
this.app = app;
this.name = name;
this.isFavourite = this.app.isLocalStorageAvailable() && localStorage.favourites?.includes(name);
this.icon = icon;
this.config = config;
this.build();
this.addEventListener('click', this.handleClick.bind(this));
this.addEventListener('dblclick', this.handleDoubleClick.bind(this));
}
/**
* @fires OperationsWaiter#operationDblclick on double click
* @param {Event} e
*/
handleDoubleClick(e) {
if (e.target === this.querySelector("li")) {
this.querySelector("li.operation").classList.add("selected");
}
}
/**
* Handle click
* @param {Event} e
*/
handleClick(e) {
if (e.target === this.querySelector("i.star-icon")) {
this.app.addFavourite(this.name);
this.updateFavourite(true);
}
}
/**
* Given a URL for a Wikipedia (or other wiki) page, this function returns a link to that page.
*
* @param {string} urlStr
* @returns {string}
*/
titleFromWikiLink(urlStr) {
const urlObj = url.parse(urlStr);
let wikiName = "",
pageTitle = "";
switch (urlObj.host) {
case "forensicswiki.xyz":
wikiName = "Forensics Wiki";
pageTitle = urlObj.query.substr(6).replace(/_/g, " "); // Chop off 'title='
break;
case "wikipedia.org":
wikiName = "Wikipedia";
pageTitle = urlObj.pathname.substr(6).replace(/_/g, " "); // Chop off '/wiki/'
break;
default:
// Not a wiki link, return full URL
return `<a href='${urlStr}' target='_blank'>More Information<i class='material-icons inline-icon'>open_in_new</i></a>`;
}
return `<a href='${urlObj.href}' target='_blank'>${pageTitle}<i class='material-icons inline-icon'>open_in_new</i></a> on ${wikiName}`;
}
/**
* Build the li element
*/
buildListItem() {
const li = document.createElement("li");
li.setAttribute("data-name", this.name);
li.classList.add("operation");
if (this.isFavourite) {
li.classList.add("favourite");
}
li.textContent = this.name;
if (this.config.description){
let dataContent = this.config.description;
if (this.config.infoURL) {
dataContent += `<hr>${this.titleFromWikiLink(this.config.infoURL)}`;
}
li.setAttribute("data-container", "body");
li.setAttribute("data-toggle", "popover");
li.setAttribute("data-placement", "left");
li.setAttribute("data-html", "true");
li.setAttribute("data-trigger", "hover");
li.setAttribute("data-boundary", "viewport");
li.setAttribute("data-content", dataContent);
}
return li;
}
/**
* Build the operation list item right side icon
*/
buildIcon() {
const icon = document.createElement("i");
icon.classList.add("material-icons");
icon.classList.add("op-icon");
icon.classList.add(this.icon.class);
icon.innerText = this.icon.innerText;
return icon;
}
/**
* Build the star icon
*/
buildStarIcon() {
const icon = document.createElement("i");
icon.setAttribute("title", this.name);
icon.classList.add("material-icons");
icon.classList.add("op-icon");
icon.classList.add("star-icon");
if (this.isFavourite){
icon.innerText = "star";
} else {
icon.innerText = "star_outline";
}
return icon;
}
/**
* Build c-operation-li
*/
build() {
const li = this.buildListItem();
const icon = this.buildIcon();
const starIcon = this.buildStarIcon();
li.appendChild(icon);
li.appendChild(starIcon);
this.appendChild(li);
}
updateFavourite(isFavourite) {
if (isFavourite) {
this.querySelector("li").classList.add("favourite");
this.querySelector("i.star-icon").innerText = "star";
} else {
this.querySelector("li").classList.remove("favourite");
this.querySelector("i.star-icon").innerText = "star_outline";
}
}
}
customElements.define("c-operation-li", COperationLi);

View file

@ -142,13 +142,13 @@
<div id="preloader-error" class="loading-error"></div>
</div>
<!-- End preloader overlay -->
<button type="button"
class="btn btn-warning bmd-btn-icon"
id="edit-favourites"
data-toggle="tooltip"
title="Edit favourites">
<i class="material-icons">star</i>
</button>
<!-- <button type="button"-->
<!-- class="btn btn-warning bmd-btn-icon"-->
<!-- id="edit-favourites"-->
<!-- data-toggle="tooltip"-->
<!-- title="Edit favourites">-->
<!-- <i class="material-icons">star</i>-->
<!-- </button>-->
<div id="content-wrapper">
<div id="banner">
@ -942,3 +942,4 @@
<!-- End modals -->
</body>
</html>

View file

@ -15,17 +15,13 @@
border-top: none;
border-left: none;
border-right: none;
cursor: pointer;
}
@media only screen and (min-width: 768px){
.operation {
cursor: grab;
}
/* never display check icons on desktop views */
.op-icon.check-icon {
display: none;
}
}
.op-icon {
@ -34,8 +30,8 @@
cursor: pointer;
}
/*mobile only favourite icon in operation list*/
.icon-add-favourite {
.op-icon.star-icon {
float: left;
margin: 0 10px 0 0;
padding: 0;
@ -44,7 +40,7 @@
cursor: pointer;
}
li.operation.favourite > .icon-add-favourite {
li.operation.favourite > .op-icon.star-icon {
opacity: 1;
pointer-events: none;
}
@ -62,14 +58,7 @@ li.operation.favourite > .icon-add-favourite {
background-color: var(--selected-operation-bg-colour);
}
@media only screen and (min-width: 768px){
.operation.selected {
background-color: var(--op-list-operation-bg-colour);
}
}
.operation.selected > .op-icon.check-icon {
.operation.selected > i.op-icon.check-icon {
display: inline-block;
}

View file

@ -20,6 +20,7 @@
background-color: var(--rec-list-operation-bg-colour);
border-color: var(--rec-list-operation-border-colour);
padding: 14px;
cursor: grab;
}
#rec-list li.sortable-chosen{

View file

@ -2,8 +2,11 @@
* Operations - Categories list
*/
#categories {
c-category-list > ul {
border-top: 1px solid var(--primary-border-colour);
list-style-type: none;
padding-left: 0;
margin-bottom: 0;
}
.category-title {

View file

@ -23,6 +23,7 @@
background-color: var(--secondary-background-colour);
}
/* 'hidden' is used to handle the visibility of a variety of mobile only elements */
#close-ops-dropdown-icon.hidden,
#search-results.hidden,
#categories.hidden {
@ -35,8 +36,8 @@
border-bottom: none;
}
/* On desktop UI, the dropdown is always open */
#search-results.hidden,
/* On desktop UI, the categories are always visible */
/*#search-results.hidden,*/
#categories.hidden {
z-index: initial;
display: block;

View file

@ -106,6 +106,7 @@ class OperationsWaiter {
searchResultsEl.innerHTML = matchedOpsHtml;
searchResultsEl.dispatchEvent(this.manager.oplistcreate);
}
this.manager.ops.updateListItemsClasses("#rec-list", "selected");
}
}
@ -183,34 +184,14 @@ class OperationsWaiter {
*/
opListCreate(e) {
if (this.app.isMobileView()) {
this.createMobileOpList(e);
$(document.querySelectorAll(".op-list .operation")).popover("disable");
} else {
this.createDesktopOpList(e);
$(document.querySelectorAll(".op-list .operation")).popover("enable");
this.enableOpPopover(e.target);
this.manager.recipe.createSortableSeedList(e.target);
}
}
/**
* Create the desktop op-list which allows popovers
* and dragging
*
* @param {event} e
*/
createDesktopOpList(e) {
this.manager.recipe.createSortableSeedList(e.target);
this.enableOpPopover(e.target);
}
/**
* Create the mobile op-list which does not allow
* popovers and dragging
*
* @param {event} e
*/
createMobileOpList(e) {
this.manager.recipe.createSortableSeedList(e.target, false);
this.disableOpsListPopovers();
}
/**
* Enable the target operation popover itself to gain focus which
@ -219,7 +200,9 @@ class OperationsWaiter {
* @param {Element} el - The element to start selecting from
*/
enableOpPopover(el) {
$(el).find("[data-toggle=popover]").addBack("[data-toggle=popover]")
$(el)
.find("[data-toggle=popover]")
.addBack("[data-toggle=popover]")
.popover({trigger: "manual"})
.on("mouseenter", function(e) {
if (e.buttons > 0) return; // Mouse button held down - likely dragging an operation
@ -241,14 +224,6 @@ class OperationsWaiter {
}
/**
* Disable popovers on all op-list list items
*/
disableOpsListPopovers() {
$(document.querySelectorAll(".op-list .operation")).popover("disable");
}
/**
* Handler for operation doubleclick events.
* Adds the operation to the recipe and auto bakes.
@ -370,21 +345,6 @@ class OperationsWaiter {
}
/**
* Add op to Favourites and add the 'favourite' class to the list item,
* set the star icon to a filled star
*
* @param {Event} e
*/
onIconFavouriteClick(e) {
this.app.addFavourite(e.target.getAttribute("title"));
document.querySelectorAll(`li[data-name="${e.target.getAttribute("title")}"]`).forEach(listItem => {
listItem.querySelector("i.star-icon").innerText = "star";
listItem.classList.add("favourite");
});
}
/**
* Update classes in the #dropdown-operations op-lists based on the
* list items of a srcListSelector.
@ -400,7 +360,7 @@ class OperationsWaiter {
*/
updateListItemsClasses(srcListSelector, className) {
const listItems = document.querySelectorAll(`${srcListSelector} > li`);
const ops = document.querySelectorAll(".op-list > li.operation");
const ops = document.querySelectorAll("c-operation-li > li.operation");
this.removeClassFromOps(className);
@ -417,29 +377,13 @@ class OperationsWaiter {
}
}
/**
* Set 'favourite' classes to all ops currently listed in the Favourites
* category, and update the ops-list operation favourite icons
*/
updateOpsFavouriteIcons() {
this.updateListItemsClasses("#catFavourites > .op-list", "favourite");
document.querySelectorAll("li.operation.favourite > i.star-icon").forEach((icon) => {
icon.innerText = "star";
});
document.querySelectorAll("li.operation:not(.favourite) > i.star-icon").forEach((icon) => {
icon.innerText = "star_outline";
});
}
/**
* Generic function to remove a class from > ALL < operation list items
*
* @param {string} className - the class to remove
*/
removeClassFromOps(className) {
const ops = document.querySelectorAll(".op-list > li.operation");
const ops = document.querySelectorAll("c-operation-li > li.operation");
ops.forEach((op => {
this.removeClassFromOp(op.getAttribute("data-name"), className);
@ -450,11 +394,11 @@ class OperationsWaiter {
/**
* Generic function to remove a class from target operation list item
*
* @param {string} opDataName - data-name attribute of the target operation
* @param {string} opName - operation name through data-name attribute of the target operation
* @param {string} className - the class to remove
*/
removeClassFromOp(opDataName, className) {
const ops = document.querySelectorAll(`.op-list > li.operation[data-name="${opDataName}"].${className}`);
removeClassFromOp(opName, className) {
const ops = document.querySelectorAll(`c-operation-li > li.operation[data-name="${opName}"].${className}`);
// the same operation may occur twice if it is also in #catFavourites
ops.forEach((op) => {
@ -466,11 +410,11 @@ class OperationsWaiter {
/**
* Generic function to add a class to an operation list item
*
* @param {string} opDataName - data-name attribute of the target operation
* @param {string} opName - operation name through data-name attribute of the target operation
* @param {string} className - the class to add to the operation list item
*/
addClassToOp(opDataName, className) {
const ops = document.querySelectorAll(`.op-list > li.operation[data-name="${opDataName}"]`);
addClassToOp(opName, className) {
const ops = document.querySelectorAll(`c-operation-li > li.operation[data-name="${opName}"]`);
// the same operation may occur twice if it is also in #catFavourites
ops.forEach((op => {

View file

@ -87,16 +87,15 @@ class RecipeWaiter {
* Creates a drag-n-droppable seed list of operations.
*
* @param {element} listEl - The list to initialise
* @param {boolean} draggable - Are list items draggable
*/
createSortableSeedList(listEl, draggable = true) {
createSortableSeedList(listEl) {
Sortable.create(listEl, {
group: {
name: "recipe",
pull: "clone",
put: false,
},
draggable: draggable ? ".operation" : null,
draggable: ".operation",
sort: false,
setData: function(dataTransfer, dragEl) {
dataTransfer.setData("Text", dragEl.getAttribute("data-name"));
@ -408,6 +407,10 @@ class RecipeWaiter {
$(item).find("[data-toggle='tooltip']").tooltip();
item.dispatchEvent(this.manager.operationadd);
this.manager.ops.updateListItemsClasses("#rec-list", "selected");
console.log("operation add");
return item;
}
@ -471,7 +474,7 @@ class RecipeWaiter {
*/
opAdd(e) {
log.debug(`'${e.target.getAttribute("data-name")}' added to recipe`);
this.manager.ops.addClassToOp(e.target.getAttribute("data-name"), "selected");
this.manager.ops.updateListItemsClasses("#rec-list", "selected");
this.triggerArgEvents(e.target);
window.dispatchEvent(this.manager.statechange);
}
@ -487,8 +490,9 @@ class RecipeWaiter {
*/
opRemove(e) {
log.debug("Operation removed from recipe");
this.manager.ops.updateListItemsClasses("#rec-list", "selected");
window.dispatchEvent(this.manager.statechange);
this.manager.ops.updateListItemsClasses("#rec-list", "selected");
console.log("operation remove");
}

View file

@ -53,8 +53,7 @@ class WindowWaiter {
onResizeToDesktop() {
this.app.setDesktopUI(false);
// if a window is resized past breakpoint while #recipe or #input is maximised,
// the maximised pane is set to its default ( non-maximised ) state
// if a window is resized past breakpoint while #recipe or #input is maximised, close these maxed panes
["recipe", "input"].forEach(paneId => this.manager.controls.setPaneMaximised(paneId, false));
// to prevent #recipe from keeping the height set in divideAvailableSpace