From f8504cdb7afe39830150217c0e81942f1ca31793 Mon Sep 17 00:00:00 2001 From: NineKey Date: Sun, 13 Sep 2026 03:31:14 +0800 Subject: [PATCH] Improve prompt completion focus handling --- web/dynamic_loader.js | 65 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 60 insertions(+), 5 deletions(-) diff --git a/web/dynamic_loader.js b/web/dynamic_loader.js index 3b0212c..555ec7b 100644 --- a/web/dynamic_loader.js +++ b/web/dynamic_loader.js @@ -193,6 +193,13 @@ app.registerExtension({ let menuIndex = 0; const closeMenu = () => { menu.hidden = true; menu.replaceChildren(); }; const closeMenuOutside = event => { + if (exTagCompleter?.dropdownController?.dropdown.contains(event.target)) return; + if (!composer.contains(event.target) && !menu.contains(event.target)) { + exTagCompleter?.dropdownController?.hide(); + if (document.activeElement === composer || document.activeElement === exTagProxy) { + document.activeElement.blur(); + } + } if (!menu.hidden && !composer.contains(event.target) && !menu.contains(event.target)) closeMenu(); }; const closeMenuForCanvasChange = () => closeMenu(); @@ -367,6 +374,12 @@ app.registerExtension({ }; let exTagCompleter = null; + let completionDismissed = false; + const dismissCompletion = () => { + completionDismissed = true; + exTagCompleter?.dropdownController?.hide(); + closeMenu(); + }; let exTagProxyValue = ""; let syncingExTagProxy = false; const textOffsetAt = (container, offset) => { @@ -420,6 +433,7 @@ app.registerExtension({ exTagProxy.value = exTagProxyValue; exTagProxy.setSelectionRange(offsets.start, offsets.end); if (!emitInput) return; + completionDismissed = false; syncingExTagProxy = true; exTagProxy.dispatchEvent(new Event("input", { bubbles: true })); syncingExTagProxy = false; @@ -455,6 +469,33 @@ app.registerExtension({ // has already removed this node. if (composerDisposed) return; exTagCompleter = new TagCompleter(exTagProxy); + const dropdown = exTagCompleter.dropdownController; + const originalShow = dropdown.show; + dropdown.show = function(...args) { + // A debounced search may finish after the user has + // moved to another node. + if (composerDisposed || completionDismissed || document.activeElement !== composer || !menu.hidden) return; + const selection = window.getSelection(); + if (!selection?.rangeCount || !composer.contains(selection.focusNode)) return; + const caretRange = document.createRange(); + caretRange.setStart(selection.focusNode, selection.focusOffset); + caretRange.collapse(true); + let rect = caretRange.getClientRects()[0]; + if (!rect?.height && selection.focusNode.nodeType === Node.TEXT_NODE + && selection.focusOffset > 0) { + caretRange.setStart(selection.focusNode, selection.focusOffset - 1); + const previous = caretRange.getBoundingClientRect(); + rect = { left: previous.right, bottom: previous.bottom, height: previous.height }; + } + if (!rect?.height) return; + originalShow.apply(this, args); + const top = rect.bottom + 4; + const available = Math.max(0, window.innerHeight - top - 8); + this.viewport.style.height = ""; + this.viewport.style.maxHeight = `${Math.min(this.calculateViewportHeight(), Math.max(0, available - 16))}px`; + this.dropdown.style.top = `${top}px`; + this.dropdown.style.left = `${Math.max(4, Math.min(rect.left, window.innerWidth - this.dropdown.offsetWidth - 4))}px`; + }; }) // ExTagComplete is optional; this node remains usable when // the extension is not installed or uses a different URL. @@ -464,6 +505,11 @@ app.registerExtension({ serializeComposer(); if (!showMenu()) syncExTagProxy(); }); + composer.addEventListener("blur", event => { + if (exTagCompleter?.dropdownController?.dropdown.contains(event.relatedTarget)) return; + dismissCompletion(); + }); + composer.addEventListener("pointerdown", dismissCompletion); composer.addEventListener("click", event => { if (event.target === composer) { node.inlineComposerSelectedChip?.classList.remove("selected"); @@ -486,12 +532,14 @@ app.registerExtension({ // bubbling to ComfyUI's native command handler. if (!event.ctrlKey && !event.metaKey && !event.altKey) event.stopPropagation(); if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "a") { + dismissCompletion(); // Preserve the browser's native Select All inside the // editor without letting LiteGraph select graph nodes. event.stopPropagation(); return; } if ((event.ctrlKey || event.metaKey) && (event.key === "ArrowUp" || event.key === "ArrowDown")) { + dismissCompletion(); if (editAttentionWithNativeRules(event.key === "ArrowUp" ? 1 : -1)) { event.preventDefault(); event.stopImmediatePropagation(); @@ -515,6 +563,11 @@ app.registerExtension({ })); return; } + // Candidate navigation above returns before caret movement + // dismisses the list. A new input enables completion again. + if (["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight", "Home", "End", "PageUp", "PageDown"].includes(event.key)) { + dismissCompletion(); + } if ((event.key === "ArrowUp" || event.key === "ArrowDown") && moveFromSelectedChip(event.key === "ArrowUp" ? -1 : 1)) { event.preventDefault(); @@ -633,12 +686,14 @@ app.registerExtension({ if (!document.__dynamicTagClipboardGuardInstalled) { document.__dynamicTagClipboardGuardInstalled = true; const activeComposer = () => { - const focused = document.activeElement?.closest?.('.dynamic-tag-composer'); - if (focused) return focused; - const anchor = window.getSelection()?.anchorNode; - return (anchor?.nodeType === Node.ELEMENT_NODE ? anchor : anchor?.parentElement) - ?.closest?.('.dynamic-tag-composer'); + return document.activeElement?.closest?.('.dynamic-tag-composer'); }; + window.addEventListener("keydown", event => { + if (activeComposer() && (event.ctrlKey || event.metaKey) + && !event.altKey && event.key.toLowerCase() === "f") { + event.stopImmediatePropagation(); + } + }, true); window.addEventListener("copy", event => { const active = activeComposer(); if (!active?.__dynamicTagClipboard || !event.clipboardData) return;