From 783156eeacb302842869adff8deb93a6397824f2 Mon Sep 17 00:00:00 2001 From: GGodPL <46885632+GGodPL@users.noreply.github.com> Date: Mon, 31 Jul 2023 00:46:46 +0200 Subject: [PATCH 1/2] Add elementsManager mod --- mods/elementsManager.js | 1109 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 1109 insertions(+) create mode 100644 mods/elementsManager.js diff --git a/mods/elementsManager.js b/mods/elementsManager.js new file mode 100644 index 00000000..194e1285 --- /dev/null +++ b/mods/elementsManager.js @@ -0,0 +1,1109 @@ +const mod = "mods/betterMenuScreens.js"; +if (enabledMods.includes(mod)) { + const properties = { + meta: [ + {name: "name", type: "string", viewOnly: true, required: true}, + {name: "category", type: "string", required: true}, + {name: "desc", type: "string"}, + {name: "behavior", type: "string"}, + {name: "alias", type: "string"}, + {name: "seed", type: "string"}, + {name: "color", type: ["color", "array"], viewOnly: true}, + {name: "breakInto", type: ["string", "array"], viewOnly: true}, + {name: "darkText", type: "boolean"}, + {name: "baby", type: "string"}, + {name: "id", type: "number", viewOnly: true, creatorIgnore: true} + ], + default_properties: [ + {name: "viscosity", type: "number"}, + {name: "density", type: "number"}, + {name: "hardness", type: "number"}, + {name: "maxSize", type: "number"}, + {name: "conduct", type: "number"}, + {name: "foodNeed", type: "number"}, + {name: "stain", type: "number"}, + ], + data: [ + {name: "hidden", type: "boolean"}, + {name: "insulate", type: "boolean"}, + {name: "noMix", type: "boolean"}, + {name: "isFood", type: "boolean"}, + {name: "forceAutoGen", type: "boolean"}, + {name: "customColor", type: "boolean"}, + {name: "ignoreAir", type: "boolean"}, + {name: "excludeRandom", type: "boolean"}, + ], + burn: [ + {name: "burn", type: "number"}, + {name: "burnTime", type: "number"}, + {name: "burnInto", type: ["string", "array"], viewOnly: true}, + {name: "burning", type: "boolean"}, + {name: "fireElement", type: ["string", "array"], viewOnly: true}, + {name: "fireColor", type: ["color", "array"], viewOnly: true}, + ], + flip: [ + {name: "flipX", type: "boolean"}, + {name: "flipY", type: "boolean"}, + {name: "flippableX", type: "boolean"}, + {name: "flippableY", type: "boolean"}, + ], + states: [ + {name: "state", type: "string"}, + {name: "stateHigh", type: "string"}, + {name: "stateHighName", type: "string"}, + {name: "stateHighColor", type: "color"}, + {name: "stateHighColorMultiplier", type: "number"}, + {name: "stateLow", type: "string"}, + {name: "stateLowName", type: "string"}, + {name: "stateLowColor", type: "color"}, + {name: "stateLowColorMultiplier", type: "number"}, + ], + temperature: [ + {name: "temp", type: "number"}, + {name: "tempHigh", type: "number"}, + {name: "extraTempHigh", type: "number"}, + {name: "tempLow", type: "number"}, + {name: "extraTempLow", type: "number"}, + ] + } + + const br = () => document.createElement("br"); + const createDiv = () => document.createElement("div"); + const span = (innerText) => { + const element = document.createElement("span"); + element.innerText = innerText; + return element; + } + const createInput = (type, disabled, id, className="") => { + const element = document.createElement("input"); + element.id = id; + element.className = className; + element.type = type; + element.disabled = disabled; + return element; + } + + const defaultSettings = {clearElements: false, allowFreeRemoval: false}; + + const toggleSetting = (setting, target) => { + target.value = target.value == "ON" ? "OFF" : "ON"; + target.setAttribute("state", target.getAttribute("state") == "1" ? "0" : "1"); + const settings_ = Storage.get("settings", defaultSettings); + settings_[setting] = !settings_[setting]; + Storage.set("settings", settings_); + } + + class Storage { + static get(id, fallback = null, setOnNull = false) { + const res = JSON.parse(localStorage.getItem(`elementsManager/${id}`)); + if (!res && fallback && setOnNull) { + Storage.set(id, fallback); + } + return res ?? fallback; + } + static set(id, value) { + localStorage.setItem(`elementsManager/${id}`, JSON.stringify(value)); + } + static remove(id) { + localStorage.removeItem(`elementsManager/${id}`); + } + static append(id, value) { + const current = Storage.get(id, []); + current.push(value); + Storage.set(id, current); + } + static filter(id, condition) { + Storage.set(id, Storage.get(id, []).filter(condition)); + } + } + + const importElements = (elements_) => { + const settings = Storage.get("settings", {clearElements: false, allowFreeRemoval: false}, true); + if (settings.clearElements) { + Storage.set("elements", elements_); + } else { + const beforeElements = Storage.get("elements", []); + Storage.set("elements", beforeElements.concat(elements_)); + } + alert(`Successfully imported ${elements_.length} element${elements_.length != 1 ? "s" : ""}`) + } + + const exportElements = () => { + let element = document.createElement('a'); + const blob = new Blob([JSON.stringify(Storage.get("elements", []))], {type: "application/json"}); + const url = URL.createObjectURL(blob); + element.setAttribute('href', url); + element.setAttribute('download', "elements.json"); + + document.body.appendChild(element); + + element.click(); + + document.body.removeChild(element); + } + + const cssInject = () => { + const style = document.createElement("style"); + style.innerHTML = `.categoryTitle { + font-size: 1.25em; + } + #elementManagerParent input[type="number"], #elementManagerParent input[type="text"], #elementCreatorParent input[type="number"], #elementCreatorParent input[type="text"] { + background-color: black; + vertical-align: middle; + margin-left: 5px; + margin-right: 5px; + border: rgb(150, 150, 150) 1px solid; + border-radius: 20px; + padding: 0.5em; + color: white; + font-size: 1em; + font-family: Arial, Helvetica, sans-serif; + } + #elementsManagerFilter { + background-color: rgb(66, 66, 66); + vertical-align: middle; + margin-left: 5px; + margin-right: 5px; + width: 75%; + color: white; + font-size: 1em; + font-family: 'Press Start 2P', Arial; + padding: 0.5em; + border: none; + display: inline-block; + } + #elementsManagerFilter:focus { + outline: none; + background-color: rgb(60, 60, 60); + } + #elementsList li span:hover, #customElementsList li span:hover, #deletedElementsList li span:hover, .hoverableText:hover { + cursor: pointer; + color: rgb(200, 200, 200); + } + #elementAdd { + background-color: rgb(66, 66, 66); + color: white; + font-size: 1em; + font-family: 'Press Start 2P', Arial; + width: auto; + padding: 0.5em; + border: none; + outline: none; + display: inline-block; + vertical-align: middle; + margin-left: 5px; + margin-right: 5px; + } + #elementAdd:hover { + cursor: pointer; + background-color: rgb(60, 60, 60); + } + #elementsManagerSettingsButton { + background-color: rgb(66, 66, 66); + color: white; + font-size: 1em; + font-family: 'Press Start 2P', Arial; + width: auto; + padding: 0.5em; + border: none; + outline: none; + display: inline-block; + vertical-align: middle; + margin-left: 5px; + margin-right: 5px; + } + #elementsManagerSettingsButton:hover { + cursor: pointer; + background-color: rgb(60, 60, 60); + } + .plusButton { + position: absolute; + right: 0px; + top: 0px; + font-size: 2em; + padding: 5px; + text-align: center; + border: 1px solid #fff; + background-color: #1da100; + z-index: 12; + } + .plusButton:hover { + background-color: #29d902; + } + .elementRemoveButton { + color: red + } + #elementsList li .elementRemoveButton:hover, #customElementsList li .elementRemoveButton:hover { + color: darkred; + cursor: pointer; + } + .createButton { + width: auto; + padding: 5px; + vertical-align: middle; + font-size: 1.25em; + display: block; + margin-left: 5px; + margin-right: 5px; + padding: 5px; + text-align: center; + border: 1px solid #1da100; + border-radius: 10px; + } + .createButton:hover { + background-color: #1da100; + }` + document.head.appendChild(style); + } + + const loadChanges = () => { + const newElements = Storage.get("elements", []); + for (const element of newElements) { + const element_ = element; + element_["behavior"] = behaviors[element_["behavior"]]; + elements[element.name] = element_; + } + const changes = Storage.get("changes", []); + for (const change of changes) { + for (const key of Object.keys(change.changes)) { + const c = change.changes[key]; + elements[change.element][key] = c; + } + } + const deleted = Storage.get("deletedElements", []); + for (const element of deleted) { + delete elements[element]; + } + } + + const applyChange = (property, value) => { + const element = Storage.get("currentElement"); + if (elements[element]) + elements[element][property] = value; + let changes = Storage.get("changes", []); + let a; + if (a = changes.find(c => c.element == element)) { + a.changes[property] = value; + } else { + let c = {}; + c[property] = value; + changes.push({ + element, + changes: c + }) + } + Storage.set("changes", changes); + } + + const elementsManagerLoader = () => { + const listDiv = createDiv(); + const list = document.createElement("ul"); + list.id = "elementsList"; + let customElements = Storage.get("elements", []); + let deletedElements = Storage.get("deletedElements", []); + let lastFreeRemoval = Storage.get("settings", {allowFreeRemoval: false, clearElements: false}, true).allowFreeRemoval; + for (const key of Object.keys(elements).concat(customElements.map(e => e.name)).sort((a, b) => a.localeCompare(b, undefined, {caseFirst: "false"}))) { + const element = document.createElement("li"); + const text = span(key); // only the text should be clickable + text.onclick = () => { + Storage.set("currentElement", key); + openMenu("elementManager", true); + } + element.appendChild(text); + if (customElements.find(e => e.name == key) || lastFreeRemoval) { + const removeButton = span("X"); + removeButton.className = "elementRemoveButton"; + removeButton.onclick = () => { + if (confirm("Are you sure you want to delete that element?")) { + if (customElements.find(e => e.name == key)) { + Storage.filter("elements", e => e.name != key); + customElements = customElements.filter(e => e.name != key); + delete elements[key]; + element.style.display = "none"; + document.getElementById("customElementsList/" + key).remove(); + if (document.getElementById("customElementsList").children.length == 0) { + document.getElementById("noCustomElementsMessage").style.display = ""; + } + } else { + Storage.append("deletedElements", key); + delete elements[key]; + element.style.display = "none"; + document.getElementById("noDeletedElementsMessage").style.display = "none"; + const li = document.createElement("li"); + li.innerText = key; + li.onclick = () => { + if (confirm("Are you sure you want to re-add '" + key + "'?")) { + Storage.filter("deletedElements", a => a != key); + li.style.display = "none"; + } + } + document.getElementById("deletedElementsList").appendChild(li); + } + } + } + const emptySpan = span(" "); + emptySpan.className = "elementRemoveButton"; + element.appendChild(emptySpan); + element.appendChild(removeButton) + } + if (deletedElements.includes(key)) { + element.style.display = "none"; + } + list.appendChild(element); + } + + const filterInput = document.createElement("input"); + filterInput.id = "elementsManagerFilter"; + filterInput.type = "text"; + filterInput.placeholder = "Search elements..."; + filterInput.onkeyup = (ev) => { + const val = ev.target.value; + const deleted = Storage.get("deletedElements"); + for (const c of document.getElementById("elementsList").children) { + const span_ = c.querySelector("span"); + if (!span_.innerText.toLowerCase().includes(val.toLowerCase()) || deleted.includes(span_.innerText)) { + c.style.display = "none"; + } else { + c.style.display = ""; + } + } + } + const addElement = document.createElement("input"); + addElement.id = "elementAdd"; + addElement.value = "+"; + addElement.type = "button"; + addElement.onclick = (_) => { + openMenu("elementCreator", true); + } + const settingsButton = document.createElement("input"); + settingsButton.id = "elementsManagerSettingsButton"; + settingsButton.type = "button"; + settingsButton.value = "*"; + settingsButton.onclick = (_) => { + openMenu("elementsSettings", true); + } + listDiv.appendChild(filterInput); + listDiv.appendChild(addElement); + listDiv.appendChild(settingsButton); + listDiv.appendChild(list); + listDiv.style.overflowY = "scroll"; + new MenuScreen() + .setTitle("Elements Manager") + .setCloseButtonText("-") + .setParentDivId("elementsManagerParent") + .setInnerDivId("elementsManager") + .addNode(listDiv) + .build(); + } + + const parseColor = (colorString) => { + if (colorString.startsWith("rgb(")) { + const color = colorString.replace("rgb(", "").replace(")", ""); + return `#${color.split(",").map(a => parseInt(a).toString(16)).join("")}`; + } else { + if (colorString.startsWith("#")) { + const color = colorString.slice(1); + if (color.length == 3) return `#${color.repeat(2)}`; + else if (color.length == 2) return `#${color.repeat(3)}`; + else if (color.length >= 6) return `#${color.slice(0, 6)}`; + else return `#${color}`; + } + } + } + + const elementManagerLoader = () => { + const nodes = []; + for (const key of Object.keys(properties)) { + const category = createDiv(); + category.innerHTML += `${key.split("_").map(k => k[0].toUpperCase() + k.slice(1)).join(" ")}
` + for (const prop of properties[key]) { + const id = "elementsManager/" + key + "/" + prop.name; + const div = createDiv(); + div.className = "elementsManager/propertyEntry"; + div.appendChild(span(prop.name)); + if (prop.viewOnly) { + if (prop.type == "boolean") { + const el = createInput("button", true, id, "toggleInput"); + div.appendChild(el); + } else if (prop.type == "longString") { + const el = document.createElement("textarea"); + el.id = id; + el.className = "elementsCode"; + el.disabled = true; + div.appendChild(el); + } else if (prop.type instanceof Array) { + const el = createInput("text", true, id); + div.appendChild(el); + } else { + const el = createInput(prop.type == "string" ? "text" : prop.type, true, id); + div.appendChild(el); + } + } else { + if (prop.type == "boolean") { + const el = createInput("button", false, id, "toggleInput"); + el.onclick = (ev) => { + ev.target.value = ev.target.value == "ON" ? "OFF" : "ON"; + ev.target.setAttribute("state", ev.target.getAttribute("state") == "1" ? "0" : "1"); + applyChange(prop.name, ev.target.value == "ON"); + } + div.appendChild(el); + } else if (prop.type == "longString") { + const el = document.createElement("textarea"); + el.id = id; + el.className = "elementsCode"; + el.onchange = (ev) => { + applyChange(prop.name, ev.target.value); + } + div.appendChild(el); + } else if (prop.type instanceof Array) { + const el = createInput("text", false, id); + el.onchange = (ev) => { + applyChange(prop.name, ev.target.value.split(";")); + } + div.appendChild(el); + } else if (prop.name == "behavior") { + const dropdown = document.createElement("select"); + dropdown.id = id; + let i = 0; + for (const bKey of Object.keys(behaviors)) { + const option = document.createElement("option"); + option.value = bKey; + option.id = id + "/option/" + i; + option.innerText = bKey; + dropdown.appendChild(option); + i++; + } + const customOption = document.createElement("option"); + customOption.value = "CUSTOM"; + customOption.id = id + "/option/custom" + customOption.innerText = "Custom Behavior"; + dropdown.appendChild(customOption); + dropdown.onchange = (ev) => { + if (ev.target.value == "CUSTOM") { + document.getElementById(id + "/textInput").style.display = ""; + } else { + document.getElementById(id + "/textInput").style.display = "none"; + applyChange(prop.name, ev.target.value); + } + } + const el = createInput("text", false, id + "/textInput"); + el.style.display = "none"; + el.onchange = (ev) => { + if (document.getElementById(id).value == "CUSTOM") { + applyChange(prop.name, ev.target.value.split(";")); + } else { + ev.target.style.display = "none"; + } + } + div.appendChild(dropdown); + div.appendChild(el); + } else { + const el = createInput(prop.type == "string" ? "text" : prop.type, false, id); + el.onchange = (ev) => { + applyChange(prop.name, prop.type == "number" ? parseFloat(ev.target.value) : ev.target.value); + } + div.appendChild(el); + } + } + category.appendChild(div); + } + category.appendChild(br()); + nodes.push(category); + } + + new MenuScreen() + .setTitle("Element Manager") + .setCloseButtonText("<") + .setParentDivId("elementManagerParent") + .setInnerDivId("elementManager") + .addNode(nodes) + .build(); + } + + const elementCreatorLoader = () => { + const nodes = []; + for (const key of Object.keys(properties)) { + const category = createDiv(); + category.innerHTML += `${key.split("_").map(k => k[0].toUpperCase() + k.slice(1)).join(" ")}
` + for (const prop of properties[key]) { + if (prop.creatorIgnore) continue; + const div = createDiv(); + div.className = "elementsManager/propertyEntry"; + div.innerHTML += `${prop.name}` + const id = "elementsManager/creator/" + key + "/" + prop.name; + if (prop.type == "boolean") { + const el = createInput("button", false, id, "toggleInput"); + el.onclick = (ev) => { + const elementData = Storage.get("newElement", {}); + ev.target.value = ev.target.value == "ON" ? "OFF" : "ON"; + elementData[prop.name] = ev.target.value == "ON"; + ev.target.setAttribute("state", ev.target.getAttribute("state") == "1" ? "0" : "1"); + Storage.set("newElement", elementData); + } + div.appendChild(el); + } else if (prop.type == "longString") { + const el = document.createElement("textarea"); + el.id = id; + el.className = "elementsCode"; + el.onchange = (ev) => { + const elementData = Storage.get("newElement", {}); + elementData[prop.name] = ev.target.value; + Storage.set("newElement", elementData); + } + div.appendChild(el); + } else if (prop.type instanceof Array && prop.type[0] != "color") { + const el = createInput("text", false, id); + el.onchange = (ev) => { + const elementData = Storage.get("newElement", {}); + elementData[prop.name] = ev.target.value.split(";"); + Storage.set("newElement", elementData); + } + div.appendChild(el); + } else if (prop.name == "behavior") { + const dropdown = document.createElement("select"); + dropdown.id = id; + let i = 0; + for (const bKey of Object.keys(behaviors)) { + const option = document.createElement("option"); + option.value = bKey; + option.selected = i == 0; + option.innerText = bKey; + dropdown.appendChild(option); + i++; + } + const customOption = document.createElement("option"); + customOption.value = "CUSTOM"; + customOption.innerText = "Custom Behavior"; + dropdown.appendChild(customOption); + dropdown.onchange = (ev) => { + if (ev.target.value == "CUSTOM") { + document.getElementById(id + "/textInput").style.display = ""; + } else { + document.getElementById(id + "/textInput").style.display = "none"; + const elementData = Storage.get("newElement", {}); + elementData[prop.name] = ev.target.value; + Storage.set("newElement", elementData); + } + } + const el = createInput("text", false, id + "/textInput"); + el.style.display = "none"; + el.onchange = (ev) => { + if (document.getElementById(id).value == "CUSTOM") { + const elementData = Storage.get("newElement", {}); + elementData[prop.name] = ev.target.value.split(";"); + Storage.set("newElement", elementData); + } else { + ev.target.style.display = "none"; + } + } + div.appendChild(dropdown); + div.appendChild(el); + } else { + const el = createInput(prop.type == "string" ? "text" : prop.type instanceof Array ? "color" : prop.type, false, id); + el.onchange = (ev) => { + const elementData = Storage.get("newElement", {}); + if (prop.type == "number") elementData[prop.name] = parseFloat(ev.target.value); + else if (prop.type == "color" || prop.type[0] == "color") elementData[prop.name] = hexToRGB(ev.target.value); + else if (prop.type == "string") elementData[prop.name] = ev.target.value; + Storage.set("newElement", elementData); + } + div.appendChild(el); + } + if (prop.required) { + const requiredMessage = span(" This field is required"); + requiredMessage.id = id + "/required"; + requiredMessage.style.display = "none"; + requiredMessage.style.color = "red"; + div.appendChild(requiredMessage); + } + category.appendChild(div); + } + category.appendChild(br()); + nodes.push(category); + } + + const createButton = span("Create Element"); + createButton.className = "createButton"; + createButton.onclick = () => { + const elementData = Storage.get("newElement"); + if (!elementData["name"]) { + document.getElementById("elementsManager/creator/meta/name/required").style.display = ""; + } + if (!elementData["category"]) { + document.getElementById("elementsManager/creator/meta/category/required").style.display = ""; + } + + if (!elementData || !elementData["name"] || !elementData["category"]) return; + Storage.append("elements", elementData); + Storage.remove("newElement"); + closeMenu(); + alert("Element successfully created"); + } + + nodes.push(br(), createButton); + + new MenuScreen() + .setTitle("Element creator") + .setParentDivId("elementCreatorParent") + .setInnerDivId("elementCreator") + .appendInnerHtml("Takes effect after reload") + .addNode(nodes) + .build(); + } + + const readFileAsync = (file) => { + return new Promise((resolve, reject) => { + const reader = new FileReader(); + reader.onload = (event) => { + resolve(JSON.parse(event.target.result)); + }; + reader.onerror = (event) => { + reject(event.target.error); + }; + reader.readAsText(file); + }); + } + + const settingsLoader = () => { + const nodes = []; + let settings = Storage.get("settings", { + clearElements: false, + allowFreeRemoval: false + }, true); + if (!settings) { + settings = { + clearElementsButton: false, + allowFreeRemoval: false + }; + Storage.set("settings", settings); + } + const elements = createDiv(); + const elementsTitle = span("Elements"); + elementsTitle.className = "categoryTitle"; + elements.appendChild(elementsTitle); + const importDiv = createDiv(); + document.addEventListener("change", async (ev) => { + if (ev.target && ev.target.id == "importElements") { + const promises = []; + for (const file of ev.target.files) { + promises.push(readFileAsync(file)); + } + const res = await Promise.all(promises); + const result = []; + for (const array of res) { + for (const element of array) { + if (result.find(r => r.name == element.name)) continue; + result.push(element); + } + } + importElements(result); + } + }) + const importInput = document.createElement("input"); + importInput.type = "file"; + importInput.accept = "application/JSON, application/json"; + importInput.id = "importElements"; + importInput.style.display = "none"; + const importLabel = document.createElement("label"); + importLabel.setAttribute("for", "importElements"); + importLabel.innerText = "Import Elements" + importLabel.className = "hoverableText"; + importDiv.appendChild(importInput); + importDiv.appendChild(importLabel); + elements.appendChild(importDiv); + const exportButton = span("Export Elements"); + exportButton.className = "hoverableText"; + exportButton.onclick = () => {exportElements()}; + elements.appendChild(exportButton); + const clearButton = span("Clear custom elements"); + clearButton.className = "hoverableText"; + clearButton.onclick = () => { + if (confirm("Are you sure you want to remove all the custom elements?")) { + Storage.remove("elements"); + const customElementsList = document.getElementById("customElementsList"); + customElementsList.replaceChildren(); + const message = document.getElementById("noCustomElementsMessage"); + message.style.display = ""; + } + } + elements.appendChild(br()); + elements.appendChild(clearButton); + elements.appendChild(br()); + const general = createDiv(); + general.innerHTML += `General
`; + const clearElementsDiv = createDiv(); + clearElementsDiv.innerHTML += "Clear custom elements on import"; + const clearElementsButton = document.createElement("input"); + clearElementsButton.className = "toggleInput"; + clearElementsButton.type = "button"; + if (settings && settings.clearElements) { + clearElementsButton.value = "ON"; + clearElementsButton.state = "1"; + } else { + clearElementsButton.value = "OFF"; + clearElementsButton.state = "0"; + } + clearElementsButton.onclick = (ev) => { + toggleSetting("clearElements", ev.target) + } + clearElementsDiv.appendChild(clearElementsButton); + general.appendChild(clearElementsDiv) + const allowRemovalDiv = createDiv(); + allowRemovalDiv.innerHTML += "Allow free element removal"; + const allowRemovalButton = document.createElement("input"); + allowRemovalButton.className = "toggleInput"; + allowRemovalButton.type = "button"; + if (settings && settings.allowFreeRemoval) { + allowRemovalButton.value = "ON"; + allowRemovalButton.state = "1"; + } else { + allowRemovalButton.value = "OFF"; + allowRemovalButton.state = "0"; + } + allowRemovalButton.onclick = (ev) => { + toggleSetting("allowFreeRemoval", ev.target) + } + allowRemovalDiv.appendChild(allowRemovalButton); + general.appendChild(allowRemovalDiv); + const customElements = createDiv(); + const customElementsTitle = span("Custom Elements"); + customElementsTitle.className = "categoryTitle"; + customElements.appendChild(customElementsTitle); + const list = document.createElement("ul"); + list.id = "customElementsList"; + const elements_ = Storage.get("elements", []); + const deleted = Storage.get("deletedElements", []); + for (const element of elements_) { + const li = document.createElement("li"); + li.id = "customElementsList/" + element; + const text = span(element.name); + text.onclick = () => { + Storage.set("currentElement", element.name); + openMenu("elementManager", true); + } + li.appendChild(text); + const removeButton = span("X"); + removeButton.className = "elementRemoveButton"; + removeButton.onclick = () => { + if (confirm("Are you sure you want to delete that element?")) { + delete elements[element.name]; + Storage.filter("elements", a => a.name != element.name); + li.style.display = "none"; + document.getElementById("customElementsList/" + key).remove(); + if (document.getElementById("customElementsList").children.length == 0) { + document.getElementById("noCustomElementsMessage").style.display = ""; + } + } + } + const emptySpan = span(" ") + emptySpan.className = "elementRemoveButton"; + li.appendChild(emptySpan); + li.appendChild(removeButton) + list.appendChild(li); + } + const message = span("There are no custom elements"); + message.id = "noCustomElementsMessage"; + if (elements_.length > 0) message.style.display = "none"; + customElements.appendChild(br()); + customElements.appendChild(message); + customElements.appendChild(list); + const deletedElements = createDiv(); + const deletedElementsTitle = span("Deleted Elements"); + deletedElementsTitle.className = "categoryTitle"; + deletedElements.appendChild(deletedElementsTitle); + deletedElements.appendChild(br()); + const deletedElementsInfo = span("Elements will reappear after reload.\nCustom elements get deleted permanently."); + deletedElements.appendChild(br()); + deletedElements.appendChild(deletedElementsInfo); + const deletedElementsMessage = span("There are no deleted elements"); + deletedElementsMessage.style.display = deleted.length > 0 ? "none" : ""; + deletedElementsMessage.id = "noDeletedElementsMessage"; + const deletedElementsList = document.createElement("ul"); + deletedElementsList.id = "deletedElementsList"; + for (const element of deleted) { + const li = document.createElement("li"); + li.innerText = element; + li.className = "hoverableText" + li.onclick = () => { + if (confirm("Are you sure you want to re-add '" + element + "'?")) { + Storage.filter("deletedElements", a => a != element); + li.style.display = "none"; + if (Storage.get("deletedElements", []).length == 0) { + document.getElementById("noDeletedElementsMessage").style.display = ""; + } + } + } + deletedElementsList.appendChild(li); + } + deletedElements.appendChild(br()); + deletedElements.appendChild(deletedElementsMessage); + deletedElements.appendChild(deletedElementsList); + nodes.push(elements, br(), general, br(), customElements, br(), deletedElements); + + new MenuScreen() + .setTitle("Elements Manager Settings") + .setParentDivId("elementsManagerSettingsParent") + .setInnerDivId("elementsManagerSettings") + .setCloseButtonText("<") + .addNode(nodes) + .build(); + } + + menuScreens.elementsManager = { + name: "Elements Manager", + parentDiv: "elementsManagerParent", + buttonDescription: "Elements manager", + show: true, + loader: elementsManagerLoader + } + + menuScreens.elementManager = { + name: "Element Manager", + parentDiv: "elementManagerParent", + show: false, + loader: elementManagerLoader, + preOpen: () => { + const currentElement = Storage.get("currentElement"); + const element = elements[currentElement]; + if (!element) closeMenu(); + for (const key of Object.keys(properties)) { + for (const prop of properties[key]) { + const id = "elementsManager/" + key + "/" + prop.name; + const el = document.getElementById(id); + if (prop.type instanceof Array) { + if (element[prop.name]) { + if (element[prop.name] instanceof Array) { + el.setAttribute("value", element[prop.name].join(";")); + } else { + const type = { + string: "text", + number: "number", + color: "color" + } + el.setAttribute("type", type[prop.type[0]]); + el.setAttribute("value", parseColor(element[prop.name])); + } + } else { + if (prop.type[0] == "color") { + el.setAttribute("type", "color"); + el.setAttribute("value", "#ff0000"); + } + else el.setAttribute("value", "none"); + } + } else { + if (element[prop.name]) { + if (prop.type == "boolean") { + el.setAttribute("value", element[prop.name] ? "ON" : "OFF"); + el.setAttribute("state", element[prop.name] ? "1" : "0"); + } else if (prop.type == "color") { + el.setAttribute("value", parseColor(element[prop.name])); + } else if (prop.name == "behavior") { + const behavior = element[prop.name]; + const index = Object.keys(behaviors).map(b => `${behaviors[b] instanceof Array ? behaviors[b].join(";") : behaviors[b]}`).indexOf(behavior instanceof Array ? behavior.join(";") : behavior); + if (index == -1) { + document.getElementById(id + "/option/custom").selected = true; + document.getElementById(id + "/textInput").style.display = ""; + document.getElementById(id + "/textInput").setAttribute("value", behavior.join(";")) + } else { + document.getElementById(id + "/textInput").style.display = "none"; + document.getElementById(id + "/option/" + index).selected = true; + } + } else { + el.setAttribute("value", element[prop.name] instanceof Function ? element[prop.name].toString() : element[prop.name]); + } + } else if (prop.name == "name") { + el.setAttribute("value", currentElement); + } else { + const default_ = { + string: "none", + number: 0, + color: "#ff0000" + } + if (prop.type == "boolean") { + el.setAttribute("value", "OFF"); + el.setAttribute("state", "0"); + } else { + el.setAttribute("value", default_[prop.type]) + } + } + } + } + } + }, + onClose: () => { + openMenu("elementsManager"); + } + } + + menuScreens.elementCreator = { + name: "Element Creator", + parentDiv: "elementCreatorParent", + show: false, + loader: elementCreatorLoader, + preOpen: () => { + const elementData = {}; + for (const key of Object.keys(properties)) { + for (const prop of properties[key]) { + if (prop.creatorIgnore) continue; + const el = document.getElementById("elementsManager/creator/" + key + "/" + prop.name) + if (prop.type instanceof Array) { + el.setAttribute("value", "none"); + } else { + const default_ = { + string: "none", + number: 0, + color: "#ff0000", + boolean: "OFF" + } + if (prop.name == "name") { + let name = `NewElement${Math.floor(Math.random() * 10000) + 1}`; + if (elements[name]) { + let i = 0; + let j = 1; + // try 100 times, then increase the pool + while (elements[name]) { + name = `NewElement${Math.floor(Math.random() * (10000 * i)) + 1}`; + i++; + if (i <= 100) { + j++; + i = 0; + } + // if already incresed the pool 5 times and there is still nothing, just break and use static name + if (j <= 5) { + name = "NewElement"; + break; + } + } + } + el.setAttribute("value", name) + elementData[prop.name] = name; + } else if (prop.name == "category") { + el.setAttribute("value", "other") + elementData[prop.name] = "other"; + } else if (prop.name == "behavior") { + elementData[prop.name] = "POWDER_OLD"; + } else { + el.setAttribute("value", default_[prop.type]); + elementData[prop.name] = default_[prop.type]; + } + if (prop.type == "boolean") { + el.setAttribute("state", "0") + elementData[prop.name] = false; + } + } + } + } + Storage.set("newElement", elementData); + }, + close: () => { + if (!Storage.get("newElement") || confirm("Are you sure you want to close the menu without creating the element?")) { + const menuParent = document.getElementById("elementCreatorParent"); + menuParent.style.display = "none"; + Storage.remove("newElement"); + Storage.remove("noClose"); + } else { + Storage.set("noClose", true); + } + }, + onClose: () => { + if (!Storage.get("noClose")) { + showingMenu = false; + openMenu("elementsManager", true); + } + } + } + + menuScreens.elementsSettings = { + name: "Elements Settings", + show: false, + parentDiv: "elementsManagerSettingsParent", + loader: settingsLoader, + preOpen: () => { + const customElements = Storage.get("elements", []); + const message = document.getElementById("noCustomElementsMessage"); + const list = document.getElementById("customElementsList"); + if (customElements.length > 0) { + message.style.display = "none"; + list.style.display = ""; + list.replaceChildren(); + for (const element of customElements) { + const li = document.createElement("li"); + const text = span(element.name); + text.onclick = () => { + Storage.set("currentElement", element.name); + openMenu("elementManager", true); + } + li.appendChild(text); + const removeButton = span("X"); + removeButton.className = "elementRemoveButton"; + removeButton.onclick = () => { + if (confirm("Are you sure you want to delete that element?")) { + delete elements[element.name]; + Storage.filter("elements", a => a.name != element.name); + li.style.display = "none"; + document.getElementById("customElementsList/" + key).remove(); + if (document.getElementById("customElementsList").children.length == 0) { + document.getElementById("noCustomElementsMessage").style.display = ""; + } + } + } + const emptySpan = span(" "); + emptySpan.className = "elementRemoveButton"; + li.appendChild(emptySpan); + li.appendChild(removeButton); + list.appendChild(li); + } + } else { + list.style.display = "none"; + message.style.display = ""; + } + }, + onClose: () => { + const settings = Storage.get("settings", {allowFreeRemoval: false, clearElements: false}, true); + const elements_ = Storage.get("elements", []).map(e => e.name); + if (settings.allowFreeRemoval && !lastFreeRemoval) { + for (const li of document.getElementById("elementsList").children) { + const name = li.querySelector("span").innerText + if (!elements_.includes(name) && li.getElementsByClassName("elementRemoveButton").length == 0) { + const removeButton = span("X"); + removeButton.className = "elementRemoveButton"; + removeButton.onclick = () => { + if (confirm("Are you sure you want to delete that element?")) { + delete elements[name]; + li.style.display = "none"; + Storage.append("deletedElements", name); + document.getElementById("noDeletedElementsMessage").style.display = "none"; + const li2 = document.createElement("li"); + li2.innerText = name; + li2.onclick = () => { + if (confirm("Are you sure you want to re-add '" + name + "'?")) { + Storage.filter("deletedElements", a => a != name); + li2.style.display = "none"; + } + } + document.getElementById("deletedElementsList").appendChild(li2); + } + } + const emptySpan = span(" ") + emptySpan.className = "elementRemoveButton"; + li.appendChild(emptySpan); + li.appendChild(removeButton) + } + } + } else if (!settings.allowFreeRemoval && lastFreeRemoval) { + for (const li of document.getElementById("elementsList").children) { + if (!elements_.includes(li.querySelector("span").innerText)) + li.querySelectorAll(".elementRemoveButton").forEach(e => e.remove()); + } + } + lastFreeRemoval = settings.allowFreeRemoval; + openMenu("elementsManager", true); + } + } + + runAfterLoadList.push(cssInject, loadChanges); +} else { + enabledMods.unshift(mod); + localStorage.setItem("enabledMods", JSON.stringify(enabledMods)); + window.location.reload(); +} From b1356f82d464c0386f8edb4069ce2105b95ff60f Mon Sep 17 00:00:00 2001 From: GGodPL <46885632+GGodPL@users.noreply.github.com> Date: Mon, 31 Jul 2023 00:47:40 +0200 Subject: [PATCH 2/2] Update betterMenuScreens --- mods/betterMenuScreens.js | 44 ++++++++++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 5 deletions(-) diff --git a/mods/betterMenuScreens.js b/mods/betterMenuScreens.js index 0fa8977d..2e54c526 100644 --- a/mods/betterMenuScreens.js +++ b/mods/betterMenuScreens.js @@ -5,6 +5,7 @@ * @property {string} buttonDescription Description that is shown when the button is hovered * @property {boolean} show Whether menu screen button should get shown on tool controls * @property {() => void} [close] Closing method. Optional + * @property {() => void} [preOpen] Method called before opening. Optional * @property {() => void} [open] Opening method. Optional * @property {() => void} [onClose] Method that gets called on close (except when menu is force closed, like when clicking on a different menu button). Optional * @property {() => void} [loader] Method that injects the menu screen into HTML. Can be set to ModScreen build method. Optional @@ -46,7 +47,7 @@ var menuScreens = { } } -closeMenu = (force) => { +closeMenu = (force = false) => { if (!showingMenu) return; const menu = menuScreens[showingMenu]; if (!menu) { @@ -68,6 +69,8 @@ closeMenu = (force) => { const inject = () => { const toolControls = document.getElementById("toolControls"); const buttons = []; + const style = document.createElement("style"); + document.head.appendChild(style); for (const key in menuScreens) { const element = menuScreens[key]; if (element.show) { @@ -77,6 +80,7 @@ const inject = () => { button.onclick = () => { if (showingMenu != key) { closeMenu(true); + if (element.preOpen) element.preOpen(); if (element.open) element.open(); else { const menuParent = document.getElementById(element.parentDiv); @@ -100,12 +104,32 @@ const inject = () => { } +/** + * + * @param {string} menu Menu do be opened + * @param {boolean} [closeCurrent] Whether it should forcefully close the current screen + */ +const openMenu = (menu, closeCurrent = false) => { + if (closeCurrent) closeMenu(true); + const menuScreen = menuScreens[menu]; + if (menuScreen) { + showingMenu = menu; + if (menuScreen.preOpen) menuScreen.preOpen(); + if (menuScreen.open) menuScreen.open(); + else { + const menuParent = document.getElementById(menuScreen.parentDiv); + menuParent.style.display = "block"; + } + } +} + class MenuScreen { constructor () { this.nodes = []; this.innerHtml = ""; this.showCloseButton = true; this.closeButtonText = "-"; + this.closeButtonClass = "XButton"; } /** @@ -123,6 +147,7 @@ class MenuScreen { */ setShowCloseButton(show) { this.showCloseButton = show; + return this; } /** @@ -131,6 +156,16 @@ class MenuScreen { */ setCloseButtonText(text = "-") { this.closeButtonText = text; + return this; + } + + /** + * Sets the close button class + * @param {string} [className] Close button class. "XButton" by default + */ + setCloseButtonClass(className = "XButton") { + this.closeButtonClass = className; + return this; } /** @@ -199,7 +234,6 @@ class MenuScreen { /** * Checks whether the menu screen is ready for build. That method should not be called outside of build method - * @private */ _check() { if (!this.parentDivId) throw "No parent div id specified"; @@ -218,12 +252,12 @@ class MenuScreen { parent.style.display = "none"; const inner = document.createElement("div"); inner.className = this.innerDivClass ?? "menuScreen"; - inner.innerHTML = `${this.showCloseButton ? ` + inner.innerHTML = `${this.showCloseButton ? ` ${this.title ?? "Menu Screen"}

"; - inner.append(this.nodes); + this.nodes.forEach(n => inner.querySelector(".menuText").appendChild(n)); parent.appendChild(inner); document.getElementById(id).appendChild(parent); } } -runAfterLoadList.push(inject); \ No newline at end of file +runAfterLoadList.push(inject);