Improve prompt completion focus handling

This commit is contained in:
NineKey
2026-09-13 03:31:14 +08:00
parent b6f959097b
commit f8504cdb7a
+60 -5
View File
@@ -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;