Spaces:
Paused
Paused
| import { app } from "../../scripts/app.js"; | |
| import { api } from "../../scripts/api.js" | |
| const MAX_HISTORY = 50; | |
| let undo = []; | |
| let redo = []; | |
| let activeState = null; | |
| let isOurLoad = false; | |
| function checkState() { | |
| const currentState = app.graph.serialize(); | |
| if (!graphEqual(activeState, currentState)) { | |
| undo.push(activeState); | |
| if (undo.length > MAX_HISTORY) { | |
| undo.shift(); | |
| } | |
| activeState = clone(currentState); | |
| redo.length = 0; | |
| api.dispatchEvent(new CustomEvent("graphChanged", { detail: activeState })); | |
| } | |
| } | |
| const loadGraphData = app.loadGraphData; | |
| app.loadGraphData = async function () { | |
| const v = await loadGraphData.apply(this, arguments); | |
| if (isOurLoad) { | |
| isOurLoad = false; | |
| } else { | |
| checkState(); | |
| } | |
| return v; | |
| }; | |
| function clone(obj) { | |
| try { | |
| if (typeof structuredClone !== "undefined") { | |
| return structuredClone(obj); | |
| } | |
| } catch (error) { | |
| // structuredClone is stricter than using JSON.parse/stringify so fallback to that | |
| } | |
| return JSON.parse(JSON.stringify(obj)); | |
| } | |
| function graphEqual(a, b, root = true) { | |
| if (a === b) return true; | |
| if (typeof a == "object" && a && typeof b == "object" && b) { | |
| const keys = Object.getOwnPropertyNames(a); | |
| if (keys.length != Object.getOwnPropertyNames(b).length) { | |
| return false; | |
| } | |
| for (const key of keys) { | |
| let av = a[key]; | |
| let bv = b[key]; | |
| if (root && key === "nodes") { | |
| // Nodes need to be sorted as the order changes when selecting nodes | |
| av = [...av].sort((a, b) => a.id - b.id); | |
| bv = [...bv].sort((a, b) => a.id - b.id); | |
| } | |
| if (!graphEqual(av, bv, false)) { | |
| return false; | |
| } | |
| } | |
| return true; | |
| } | |
| return false; | |
| } | |
| const undoRedo = async (e) => { | |
| const updateState = async (source, target) => { | |
| const prevState = source.pop(); | |
| if (prevState) { | |
| target.push(activeState); | |
| isOurLoad = true; | |
| await app.loadGraphData(prevState, false); | |
| activeState = prevState; | |
| } | |
| } | |
| if (e.ctrlKey || e.metaKey) { | |
| if (e.key === "y") { | |
| updateState(redo, undo); | |
| return true; | |
| } else if (e.key === "z") { | |
| updateState(undo, redo); | |
| return true; | |
| } | |
| } | |
| }; | |
| const bindInput = (activeEl) => { | |
| if (activeEl && activeEl.tagName !== "CANVAS" && activeEl.tagName !== "BODY") { | |
| for (const evt of ["change", "input", "blur"]) { | |
| if (`on${evt}` in activeEl) { | |
| const listener = () => { | |
| checkState(); | |
| activeEl.removeEventListener(evt, listener); | |
| }; | |
| activeEl.addEventListener(evt, listener); | |
| return true; | |
| } | |
| } | |
| } | |
| }; | |
| let keyIgnored = false; | |
| window.addEventListener( | |
| "keydown", | |
| (e) => { | |
| requestAnimationFrame(async () => { | |
| let activeEl; | |
| // If we are auto queue in change mode then we do want to trigger on inputs | |
| if (!app.ui.autoQueueEnabled || app.ui.autoQueueMode === "instant") { | |
| activeEl = document.activeElement; | |
| if (activeEl?.tagName === "INPUT" || activeEl?.type === "textarea") { | |
| // Ignore events on inputs, they have their native history | |
| return; | |
| } | |
| } | |
| keyIgnored = e.key === "Control" || e.key === "Shift" || e.key === "Alt" || e.key === "Meta"; | |
| if (keyIgnored) return; | |
| // Check if this is a ctrl+z ctrl+y | |
| if (await undoRedo(e)) return; | |
| // If our active element is some type of input then handle changes after they're done | |
| if (bindInput(activeEl)) return; | |
| checkState(); | |
| }); | |
| }, | |
| true | |
| ); | |
| window.addEventListener("keyup", (e) => { | |
| if (keyIgnored) { | |
| keyIgnored = false; | |
| checkState(); | |
| } | |
| }); | |
| // Handle clicking DOM elements (e.g. widgets) | |
| window.addEventListener("mouseup", () => { | |
| checkState(); | |
| }); | |
| // Handle prompt queue event for dynamic widget changes | |
| api.addEventListener("promptQueued", () => { | |
| checkState(); | |
| }); | |
| // Handle litegraph clicks | |
| const processMouseUp = LGraphCanvas.prototype.processMouseUp; | |
| LGraphCanvas.prototype.processMouseUp = function (e) { | |
| const v = processMouseUp.apply(this, arguments); | |
| checkState(); | |
| return v; | |
| }; | |
| const processMouseDown = LGraphCanvas.prototype.processMouseDown; | |
| LGraphCanvas.prototype.processMouseDown = function (e) { | |
| const v = processMouseDown.apply(this, arguments); | |
| checkState(); | |
| return v; | |
| }; | |
| // Handle litegraph context menu for COMBO widgets | |
| const close = LiteGraph.ContextMenu.prototype.close; | |
| LiteGraph.ContextMenu.prototype.close = function(e) { | |
| const v = close.apply(this, arguments); | |
| checkState(); | |
| return v; | |
| } |