From 18ddadc9ec7be0c4c90a5d98ae9af3ea980abf19 Mon Sep 17 00:00:00 2001 From: Will Miao Date: Wed, 8 Apr 2026 07:55:41 +0800 Subject: [PATCH] feat(autocomplete): auto-format textarea on blur (#884) --- .../components/autocomplete.behavior.test.js | 62 +++++++++++++++++++ web/comfyui/autocomplete.js | 35 +++++++++++ web/comfyui/settings.js | 38 ++++++++++++ 3 files changed, 135 insertions(+) diff --git a/tests/frontend/components/autocomplete.behavior.test.js b/tests/frontend/components/autocomplete.behavior.test.js index 51dd67ea..5f1b55e4 100644 --- a/tests/frontend/components/autocomplete.behavior.test.js +++ b/tests/frontend/components/autocomplete.behavior.test.js @@ -69,6 +69,9 @@ describe('AutoComplete widget interactions', () => { if (key === 'loramanager.autocomplete_append_comma') { return true; } + if (key === 'loramanager.autocomplete_auto_format') { + return true; + } if (key === 'loramanager.autocomplete_accept_key') { return 'both'; } @@ -188,6 +191,59 @@ describe('AutoComplete widget interactions', () => { expect(insertSelectionSpy).toHaveBeenCalledWith('example_completion'); }); + it('formats duplicate commas and extra spaces when the textarea loses focus', async () => { + const input = document.createElement('textarea'); + input.value = 'foo bar, , baz ,, qux'; + document.body.append(input); + + const inputListener = vi.fn(); + input.addEventListener('input', inputListener); + + const { AutoComplete } = await import(AUTOCOMPLETE_MODULE); + new AutoComplete(input,'prompt', { showPreview: false }); + + input.dispatchEvent(new Event('blur', { bubbles: true })); + + expect(input.value).toBe('foo bar, baz, qux'); + expect(inputListener).toHaveBeenCalledTimes(1); + }); + + it('skips blur formatting when autocomplete auto format is disabled', async () => { + settingGetMock.mockImplementation((key) => { + if (key === 'loramanager.autocomplete_append_comma') { + return true; + } + if (key === 'loramanager.autocomplete_auto_format') { + return false; + } + if (key === 'loramanager.autocomplete_accept_key') { + return 'both'; + } + if (key === 'loramanager.prompt_tag_autocomplete') { + return true; + } + if (key === 'loramanager.tag_space_replacement') { + return false; + } + return undefined; + }); + + const input = document.createElement('textarea'); + input.value = 'foo bar, , baz ,, qux'; + document.body.append(input); + + const inputListener = vi.fn(); + input.addEventListener('input', inputListener); + + const { AutoComplete } = await import(AUTOCOMPLETE_MODULE); + new AutoComplete(input,'prompt', { showPreview: false }); + + input.dispatchEvent(new Event('blur', { bubbles: true })); + + expect(input.value).toBe('foo bar, , baz ,, qux'); + expect(inputListener).not.toHaveBeenCalled(); + }); + it('accepts the selected suggestion with Enter', async () => { caretHelperInstance.getBeforeCursor.mockReturnValue('example'); @@ -275,6 +331,9 @@ describe('AutoComplete widget interactions', () => { if (key === 'loramanager.autocomplete_append_comma') { return true; } + if (key === 'loramanager.autocomplete_auto_format') { + return true; + } if (key === 'loramanager.autocomplete_accept_key') { return 'tab_only'; } @@ -322,6 +381,9 @@ describe('AutoComplete widget interactions', () => { if (key === 'loramanager.autocomplete_append_comma') { return true; } + if (key === 'loramanager.autocomplete_auto_format') { + return true; + } if (key === 'loramanager.autocomplete_accept_key') { return 'enter_only'; } diff --git a/web/comfyui/autocomplete.js b/web/comfyui/autocomplete.js index 2e682988..abb9d141 100644 --- a/web/comfyui/autocomplete.js +++ b/web/comfyui/autocomplete.js @@ -3,6 +3,7 @@ import { app } from "../../scripts/app.js"; import { TextAreaCaretHelper } from "./textarea_caret_helper.js"; import { getAutocompleteAppendCommaPreference, + getAutocompleteAutoFormatPreference, getAutocompleteAcceptKeyPreference, getPromptTagAutocompletePreference, getTagSpaceReplacementPreference, @@ -122,6 +123,32 @@ function formatAutocompleteInsertion(text = '') { return getAutocompleteAppendCommaPreference() ? `${trimmed},` : `${trimmed} `; } +function normalizeAutocompleteSegment(segment = '') { + return segment.replace(/\s+/g, ' ').trim(); +} + +export function formatAutocompleteTextOnBlur(text = '') { + if (typeof text !== 'string') { + return ''; + } + + return text + .split('\n') + .map((line) => { + if (!line.trim()) { + return ''; + } + + const cleanedSegments = line + .split(',') + .map(normalizeAutocompleteSegment) + .filter(Boolean); + + return cleanedSegments.join(', '); + }) + .join('\n'); +} + function shouldAcceptAutocompleteKey(key) { const mode = getAutocompleteAcceptKeyPreference(); @@ -481,6 +508,14 @@ class AutoComplete { // Handle focus out to hide dropdown this.onBlur = () => { + if (getAutocompleteAutoFormatPreference()) { + const formattedValue = formatAutocompleteTextOnBlur(this.inputElement.value); + if (formattedValue !== this.inputElement.value) { + this.inputElement.value = formattedValue; + this.inputElement.dispatchEvent(new Event('input', { bubbles: true })); + } + } + // Delay hiding to allow for clicks on dropdown items setTimeout(() => { this.hide(); diff --git a/web/comfyui/settings.js b/web/comfyui/settings.js index 4df11719..fedd4957 100644 --- a/web/comfyui/settings.js +++ b/web/comfyui/settings.js @@ -16,6 +16,9 @@ const PROMPT_TAG_AUTOCOMPLETE_DEFAULT = true; const AUTOCOMPLETE_APPEND_COMMA_SETTING_ID = "loramanager.autocomplete_append_comma"; const AUTOCOMPLETE_APPEND_COMMA_DEFAULT = true; +const AUTOCOMPLETE_AUTO_FORMAT_SETTING_ID = "loramanager.autocomplete_auto_format"; +const AUTOCOMPLETE_AUTO_FORMAT_DEFAULT = true; + const AUTOCOMPLETE_ACCEPT_KEY_SETTING_ID = "loramanager.autocomplete_accept_key"; const AUTOCOMPLETE_ACCEPT_KEY_DEFAULT = "both"; const AUTOCOMPLETE_ACCEPT_KEY_OPTION_BOTH = "Tab or Enter"; @@ -192,6 +195,32 @@ const getAutocompleteAppendCommaPreference = (() => { }; })(); +const getAutocompleteAutoFormatPreference = (() => { + let settingsUnavailableLogged = false; + + return () => { + const settingManager = app?.extensionManager?.setting; + if (!settingManager || typeof settingManager.get !== "function") { + if (!settingsUnavailableLogged) { + console.warn("LoRA Manager: settings API unavailable, using default autocomplete auto format setting."); + settingsUnavailableLogged = true; + } + return AUTOCOMPLETE_AUTO_FORMAT_DEFAULT; + } + + try { + const value = settingManager.get(AUTOCOMPLETE_AUTO_FORMAT_SETTING_ID); + return value ?? AUTOCOMPLETE_AUTO_FORMAT_DEFAULT; + } catch (error) { + if (!settingsUnavailableLogged) { + console.warn("LoRA Manager: unable to read autocomplete auto format setting, using default.", error); + settingsUnavailableLogged = true; + } + return AUTOCOMPLETE_AUTO_FORMAT_DEFAULT; + } + }; +})(); + const getAutocompleteAcceptKeyPreference = (() => { let settingsUnavailableLogged = false; @@ -375,6 +404,14 @@ app.registerExtension({ tooltip: "When enabled, accepted autocomplete suggestions append ', ' to the inserted text.", category: ["LoRA Manager", "Autocomplete", "Append comma"], }, + { + id: AUTOCOMPLETE_AUTO_FORMAT_SETTING_ID, + name: "Auto format autocomplete text on blur", + type: "boolean", + defaultValue: AUTOCOMPLETE_AUTO_FORMAT_DEFAULT, + tooltip: "When enabled, leaving an autocomplete textarea removes duplicate commas and collapses unnecessary spaces.", + category: ["LoRA Manager", "Autocomplete", "Auto Format"], + }, { id: AUTOCOMPLETE_ACCEPT_KEY_SETTING_ID, name: "Autocomplete accept key", @@ -505,6 +542,7 @@ export { getWheelSensitivity, getAutoPathCorrectionPreference, getAutocompleteAppendCommaPreference, + getAutocompleteAutoFormatPreference, getAutocompleteAcceptKeyPreference, getPromptTagAutocompletePreference, getTagSpaceReplacementPreference,