feat: add double-click to edit tags with autocomplete support

This commit is contained in:
=
2026-05-19 13:26:32 -04:00
parent 066c33452c
commit aae0ffef02
2 changed files with 152 additions and 52 deletions
+1
View File
@@ -13,6 +13,7 @@ A sophisticated multi-line text node for ComfyUI that provides weighted autocomp
### 💊 Pill-Based Tag Management
* **Interactive UI:** Every tag is displayed as a "pill" with an `×` button for quick removal.
* **Double-Click to Edit:** Double-click any tag pill to edit it inline. Autocomplete remains active during editing.
* **Drag-and-Drop Reordering:** Simply click and drag a tag pill to change its position in the list.
* **Quick Delete:** Press `Backspace` when the input is empty to delete the last tag.
* **Visual Clarity:** Easily see and manage long lists of tags without the clutter of raw text.
+151 -52
View File
@@ -154,6 +154,18 @@ const STYLE = `
font-size: 10px;
margin-left: 10px;
}
.autotag-pill-edit {
background: transparent;
border: none;
color: inherit;
font-size: inherit;
font-family: inherit;
outline: none;
padding: 0;
margin: 0;
width: auto;
min-width: 20px;
}
`;
function addStyles() {
@@ -288,6 +300,55 @@ app.registerExtension({
draggedTagIndex = -1;
};
pill.ondblclick = (e) => {
e.stopPropagation();
const editInput = document.createElement("input");
editInput.type = "text";
editInput.value = tag;
editInput.className = "autotag-pill-edit";
pill.textContent = "";
pill.draggable = false;
pill.appendChild(editInput);
editInput.focus();
editInput.select();
const save = () => {
const newVal = editInput.value.trim();
if (newVal && newVal !== tag) {
const newTags = [...currentTags];
newTags[index] = newVal;
widget.value = newTags.join(", ");
app.graph.setDirtyCanvas(true);
}
updatePills();
};
const originalEditOnKeyDown = editInput.onkeydown;
editInput.onkeydown = (e) => {
if (e.key === "Enter") {
const handled = originalEditOnKeyDown ? originalEditOnKeyDown.apply(editInput, [e]) : false;
if (!handled) save();
} else if (e.key === "Escape") {
e.stopPropagation();
editInput.onblur = null;
updatePills();
} else {
if (originalEditOnKeyDown) originalEditOnKeyDown.apply(editInput, [e]);
}
};
editInput.onblur = save;
setupAutocomplete(editInput, (newVal) => {
if (newVal && newVal !== tag) {
const newTags = [...currentTags];
newTags[index] = newVal;
widget.value = newTags.join(", ");
app.graph.setDirtyCanvas(true);
}
updatePills();
});
};
const removeBtn = document.createElement("span");
removeBtn.className = "remove-btn";
removeBtn.innerHTML = "×";
@@ -419,61 +480,89 @@ app.registerExtension({
let filteredTags = [];
let highlightedIndex = -1;
input.oninput = () => {
input.style.height = "auto";
input.style.height = Math.max(20, input.scrollHeight) + "px";
const query = input.value.toLowerCase().trim();
if (!query) { autocomplete.style.display = "none"; return; }
const regexQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s+/g, '[ _]');
const regex = new RegExp(regexQuery, "i");
filteredTags = tagsData.filter(t => regex.test(t.tag)).slice(0, 20);
if (filteredTags.length === 0) { autocomplete.style.display = "none"; return; }
const setupAutocomplete = (targetInput, onSelect) => {
targetInput.oninput = () => {
if (targetInput.tagName === "TEXTAREA") {
targetInput.style.height = "auto";
targetInput.style.height = Math.max(20, targetInput.scrollHeight) + "px";
}
const query = targetInput.value.toLowerCase().trim();
if (!query) { autocomplete.style.display = "none"; return; }
const regexQuery = query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&').replace(/\s+/g, '[ _]');
const regex = new RegExp(regexQuery, "i");
filteredTags = tagsData.filter(t => regex.test(t.tag)).slice(0, 20);
if (filteredTags.length === 0) { autocomplete.style.display = "none"; return; }
autocomplete.innerHTML = "";
highlightedIndex = 0;
filteredTags.forEach((t, i) => {
const item = document.createElement("div");
item.className = "autotag-autocomplete-item";
if (i === highlightedIndex) item.classList.add("highlighted");
item.innerHTML = `<span>${t.tag}</span><span class="weight">${t.weight}</span>`;
item.onclick = (e) => {
e.stopPropagation();
addTag(t.tag);
input.focus();
};
autocomplete.appendChild(item);
});
autocomplete.innerHTML = "";
highlightedIndex = 0;
filteredTags.forEach((t, i) => {
const item = document.createElement("div");
item.className = "autotag-autocomplete-item";
if (i === highlightedIndex) item.classList.add("highlighted");
item.innerHTML = `<span>${t.tag}</span><span class="weight">${t.weight}</span>`;
item.onclick = (e) => {
e.stopPropagation();
onSelect(t.tag);
targetInput.focus();
};
autocomplete.appendChild(item);
});
const rect = input.getBoundingClientRect();
autocomplete.style.left = rect.left + "px";
autocomplete.style.top = rect.bottom + "px";
autocomplete.style.width = Math.max(200, rect.width) + "px";
autocomplete.style.display = "block";
const rect = targetInput.getBoundingClientRect();
autocomplete.style.left = rect.left + "px";
autocomplete.style.top = rect.bottom + "px";
autocomplete.style.width = Math.max(200, rect.width) + "px";
autocomplete.style.display = "block";
};
const originalOnKeyDown = targetInput.onkeydown;
targetInput.onkeydown = (e) => {
if (e.key === "Enter") {
if (autocomplete.style.display !== "none" && highlightedIndex >= 0) {
e.preventDefault();
onSelect(filteredTags[highlightedIndex].tag);
return true;
}
} else if (e.key === "ArrowDown" || e.key === "ArrowUp") {
if (autocomplete.style.display !== "none") {
e.preventDefault();
const delta = e.key === "ArrowDown" ? 1 : -1;
highlightedIndex = (highlightedIndex + delta + filteredTags.length) % filteredTags.length;
autocomplete.querySelectorAll(".autotag-autocomplete-item").forEach((item, i) => {
item.classList.toggle("highlighted", i === highlightedIndex);
if (i === highlightedIndex) item.scrollIntoView({ block: "nearest" });
});
return true;
}
} else if (e.key === "Escape") {
if (autocomplete.style.display !== "none") {
e.preventDefault();
e.stopPropagation();
autocomplete.style.display = "none";
return true;
}
}
if (originalOnKeyDown) return originalOnKeyDown.apply(targetInput, [e]);
return false;
};
const originalBlur = targetInput.onblur;
targetInput.onblur = (e) => {
setTimeout(() => {
autocomplete.style.display = "none";
if (originalBlur) originalBlur.apply(targetInput, [e]);
}, 150);
};
};
input.onkeydown = (e) => {
if (e.key === "Enter") {
e.preventDefault();
if (autocomplete.style.display !== "none" && highlightedIndex >= 0) {
addTag(filteredTags[highlightedIndex].tag);
} else {
addTag(input.value);
}
} else if (e.key === ",") {
setupAutocomplete(input, (tag) => { addTag(tag); });
input.addEventListener("keydown", (e) => {
if (e.key === ",") {
e.preventDefault();
addTag(input.value);
} else if (e.key === "ArrowDown" || e.key === "ArrowUp") {
if (autocomplete.style.display !== "none") {
e.preventDefault();
const delta = e.key === "ArrowDown" ? 1 : -1;
highlightedIndex = (highlightedIndex + delta + filteredTags.length) % filteredTags.length;
autocomplete.querySelectorAll(".autotag-autocomplete-item").forEach((item, i) => {
item.classList.toggle("highlighted", i === highlightedIndex);
if (i === highlightedIndex) item.scrollIntoView({ block: "nearest" });
});
}
} else if (e.key === "Backspace" && input.value === "") {
const currentTags = widget.value.split(",").map(t => t.trim()).filter(t => t !== "");
if (currentTags.length > 0) {
@@ -482,12 +571,22 @@ app.registerExtension({
updatePills();
app.graph.setDirtyCanvas(true);
}
} else if (e.key === "Escape") {
autocomplete.style.display = "none";
}
});
const originalInputOnKeyDown = input.onkeydown;
input.onkeydown = (e) => {
if (e.key === "Enter") {
const handled = originalInputOnKeyDown ? originalInputOnKeyDown.apply(input, [e]) : false;
if (!handled) {
e.preventDefault();
addTag(input.value);
}
} else {
if (originalInputOnKeyDown) originalInputOnKeyDown.apply(input, [e]);
}
};
input.onblur = () => { setTimeout(() => { autocomplete.style.display = "none"; }, 150); };
container.onclick = () => { input.focus(); };
setTimeout(updatePills, 0);