| | 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) { |
| | |
| | } |
| |
|
| | 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") { |
| | |
| | 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 (!app.ui.autoQueueEnabled || app.ui.autoQueueMode === "instant") { |
| | activeEl = document.activeElement; |
| | if (activeEl?.tagName === "INPUT" || activeEl?.type === "textarea") { |
| | |
| | return; |
| | } |
| | } |
| | |
| | keyIgnored = e.key === "Control" || e.key === "Shift" || e.key === "Alt" || e.key === "Meta"; |
| | if (keyIgnored) return; |
| |
|
| | |
| | if (await undoRedo(e)) return; |
| |
|
| | |
| | if (bindInput(activeEl)) return; |
| | checkState(); |
| | }); |
| | }, |
| | true |
| | ); |
| |
|
| | window.addEventListener("keyup", (e) => { |
| | if (keyIgnored) { |
| | keyIgnored = false; |
| | checkState(); |
| | } |
| | }); |
| |
|
| | |
| | window.addEventListener("mouseup", () => { |
| | checkState(); |
| | }); |
| |
|
| | |
| | api.addEventListener("promptQueued", () => { |
| | checkState(); |
| | }); |
| |
|
| | |
| | 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; |
| | }; |
| |
|
| | |
| | const close = LiteGraph.ContextMenu.prototype.close; |
| | LiteGraph.ContextMenu.prototype.close = function(e) { |
| | const v = close.apply(this, arguments); |
| | checkState(); |
| | return v; |
| | } |