From b93f23efb79dd7e436d2ef3945f8de9b8ff2703c Mon Sep 17 00:00:00 2001 From: Haoming Date: Mon, 1 Jun 2026 17:57:35 +0800 Subject: [PATCH] undo redo --- .../javascript/prompt-bracket-checker.js | 2 +- javascript/history.js | 101 ++++++++++++++++++ javascript/promptField.js | 2 +- javascript/settings.js | 2 +- javascript/token-counters.js | 2 +- modules/shared_options.py | 1 + 6 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 javascript/history.js diff --git a/extensions-builtin/prompt-bracket-checker/javascript/prompt-bracket-checker.js b/extensions-builtin/prompt-bracket-checker/javascript/prompt-bracket-checker.js index 84a695cc..a88af255 100644 --- a/extensions-builtin/prompt-bracket-checker/javascript/prompt-bracket-checker.js +++ b/extensions-builtin/prompt-bracket-checker/javascript/prompt-bracket-checker.js @@ -61,7 +61,7 @@ const counter = gradioApp().getElementById(id_counter); if (textarea && counter) - onEdit(`${id_prompt}_BracketChecking`, textarea, 400, () => checkBrackets(textarea, counter)); + onEdit(`${id_prompt}_BracketChecking`, textarea, 600, () => checkBrackets(textarea, counter)); } onUiLoaded(function () { diff --git a/javascript/history.js b/javascript/history.js new file mode 100644 index 00000000..154499ca --- /dev/null +++ b/javascript/history.js @@ -0,0 +1,101 @@ +(function () { + const editTimers = {}; + + const delay = 600; + const limit = 16; + + class TextboxHistory { + /** @param {string} id */ + constructor(id) { + /** @type {HTMLTextAreaElement} */ + this.textarea = document.querySelector(`#${id} textarea`); + /** @type {string[]} */ + this.undoStack = []; + /** @type {string[]} */ + this.redoStack = []; + + this.#snapshot(); + + this.textarea.addEventListener("keydown", (e) => { + const prev = editTimers[id]; + + if ((e.ctrlKey || e.metaKey) && e.key === "z") { + e.preventDefault(); + if (prev) clearTimeout(prev); + this.#undo(); + return false; + } + if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === "Z") { + e.preventDefault(); + if (prev) clearTimeout(prev); + this.#redo(); + return false; + } + + if (["Control", "Meta", "Shift", "Alt"].includes(e.key)) return true; + + if (prev) clearTimeout(prev); + editTimers[id] = setTimeout(() => this.#snapshot(), delay); + }); + } + + #undo() { + this.#snapshot(false); + + if (this.undoStack.length < 2) return; + + const current = this.undoStack.pop(); + this.redoStack.push(current); + + const prev = this.undoStack.at(-1); + this.textarea.value = prev; + updateInput(this.textarea); + } + + #redo() { + if (this.redoStack.length < 1) return; + + const current = this.textarea.value; + this.undoStack.push(current); + + const prev = this.redoStack.pop(); + this.textarea.value = prev; + updateInput(this.textarea); + } + + #snapshot(reset = true) { + const current = this.textarea.value; + if (current === this.undoStack.at(-1)) return; + + this.undoStack.push(current); + if (this.undoStack.length > limit) this.undoStack.shift(); + if (reset) this.redoStack.length = 0; + } + } + + function setup() { + const IDs = [ + "txt2img_prompt", + "txt2img_neg_prompt", + "img2img_prompt", + "img2img_neg_prompt", + "hires_prompt", + "hires_neg_prompt", + ]; + + for (const id of IDs) new TextboxHistory(id); + } + + onUiLoaded(() => { + function checkSettings() { + if (Object.keys(opts).length === 0) { + setTimeout(checkSettings, 100); + return; + } + + if (opts.undo_redo) setup(); + } + + checkSettings(); + }); +})(); diff --git a/javascript/promptField.js b/javascript/promptField.js index c02377d9..cc3147bb 100644 --- a/javascript/promptField.js +++ b/javascript/promptField.js @@ -21,7 +21,7 @@ function shushTextfield() { onUiLoaded(() => { function checkSettings() { - if (Object.keys(opts).length == 0) { + if (Object.keys(opts).length === 0) { setTimeout(checkSettings, 100); return; } diff --git a/javascript/settings.js b/javascript/settings.js index a1c9b2d2..21c4dd79 100644 --- a/javascript/settings.js +++ b/javascript/settings.js @@ -29,7 +29,7 @@ onUiLoaded(function () { ); let settings_tabs = gradioApp().querySelector("#settings div"); - onEdit("settingsSearch", editTextarea, 250, function () { + onEdit("settingsSearch", editTextarea, 300, function () { let searchText = (editTextarea.value || "").trim().toLowerCase(); gradioApp() diff --git a/javascript/token-counters.js b/javascript/token-counters.js index 30d12f33..85065751 100644 --- a/javascript/token-counters.js +++ b/javascript/token-counters.js @@ -54,7 +54,7 @@ function setupTokenCounting(id, id_counter, id_button) { prompt.parentElement.insertBefore(counter, prompt); prompt.parentElement.style.position = "relative"; - let func = onEdit(id, textarea, 800, function () { + let func = onEdit(id, textarea, 600, function () { if (counter.classList.contains("token-counter-visible")) { gradioApp().getElementById(id_button)?.click(); } diff --git a/modules/shared_options.py b/modules/shared_options.py index 42f5fcb4..6604ab86 100644 --- a/modules/shared_options.py +++ b/modules/shared_options.py @@ -175,6 +175,7 @@ options_templates.update( "list_hidden_files": OptionInfo(True, "List the models/files under hidden directories").info('directory is hidden if its name starts with "."'), "dump_stacks_on_signal": OptionInfo(False, "Print the stack trace before terminating the webui via Ctrl + C"), "no_spellcheck": OptionInfo(False, "Disable auto-correct / spellcheck for prompt fields").needs_reload_ui(), + "undo_redo": OptionInfo(False, "Enable undo / redo history for prompt fields").needs_reload_ui(), }, ) )