Files
AEmotionStudio-ComfyUI-Magn…/web/shared/CustomDropdown.js
T

265 lines
9.5 KiB
JavaScript

var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
class CustomDropdown {
// Track active item for keyboard navigation
constructor(config) {
__publicField(this, "dropdown", null);
__publicField(this, "config");
__publicField(this, "closeHandler", null);
__publicField(this, "keyHandler", null);
__publicField(this, "scrollHandler", null);
__publicField(this, "currentIndex", 0);
this.config = config;
}
/**
* Show the dropdown
*/
show() {
this.hide();
const dropdown = document.createElement("div");
dropdown.className = `custom-dropdown-menu${this.config.theme ? ` theme-${this.config.theme}` : ""}`;
dropdown.style.cssText = `
position: fixed;
z-index: 100001;
overflow-y: auto;
background: var(--comfy-menu-bg, #1a1a1f);
border: 1px solid var(--border-color, #444);
border-radius: 6px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
min-width: 120px;
`;
this.config.options.forEach((optionValue) => {
const item = document.createElement("div");
item.className = "custom-dropdown-item";
item.dataset.value = optionValue;
const isSelected = optionValue === this.config.currentValue;
item.style.cssText = `
padding: 8px 12px;
cursor: pointer;
border-bottom: 1px solid var(--border-color, #333);
font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
font-size: 13px;
color: ${isSelected ? "var(--info-panel-accent-color, #4ecdc4)" : "inherit"};
background: ${isSelected ? "rgba(78, 205, 196, 0.1)" : "transparent"};
white-space: nowrap;
`;
item.textContent = optionValue;
item.addEventListener("mouseenter", () => {
if (!isSelected) {
item.style.background = "var(--comfy-input-bg, #3a3a3a)";
}
});
item.addEventListener("mouseleave", () => {
item.style.background = isSelected ? "rgba(78, 205, 196, 0.1)" : "transparent";
});
item.addEventListener("click", (e) => {
e.stopPropagation();
this.config.onChange(optionValue);
this.hide();
});
dropdown.appendChild(item);
});
const lastItem = dropdown.lastElementChild;
if (lastItem) {
lastItem.style.borderBottom = "none";
}
document.body.appendChild(dropdown);
this.dropdown = dropdown;
this.currentIndex = this.config.options.indexOf(this.config.currentValue);
if (this.currentIndex < 0) this.currentIndex = 0;
dropdown.setAttribute("tabindex", "-1");
this.positionWithinViewport();
this.scrollToSelected();
this.setupCloseHandlers();
dropdown.focus();
}
/**
* Hide the dropdown
*/
hide() {
var _a, _b;
if (this.dropdown && this.dropdown.parentNode) {
this.dropdown.parentNode.removeChild(this.dropdown);
this.dropdown = null;
}
this.cleanupHandlers();
(_b = (_a = this.config).onClose) == null ? void 0 : _b.call(_a);
}
/**
* Position dropdown within viewport bounds
*/
positionWithinViewport() {
if (!this.dropdown) return;
const margin = 10;
const gap = 4;
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
const anchorRect = this.config.anchor.getBoundingClientRect();
this.dropdown.style.maxWidth = "none";
this.dropdown.style.maxHeight = "none";
const naturalRect = this.dropdown.getBoundingClientRect();
const spaceBelow = viewportHeight - anchorRect.bottom - margin - gap;
const spaceAbove = anchorRect.top - margin - gap;
const spaceRight = viewportWidth - anchorRect.left - margin;
let top;
let maxHeight;
if (spaceBelow >= naturalRect.height || spaceBelow >= spaceAbove) {
top = anchorRect.bottom + gap;
maxHeight = Math.max(100, spaceBelow);
} else {
maxHeight = Math.max(100, spaceAbove);
top = anchorRect.top - gap - Math.min(naturalRect.height, maxHeight);
}
let left = anchorRect.left;
let maxWidth;
if (naturalRect.width <= spaceRight) {
maxWidth = spaceRight;
} else {
left = Math.max(margin, viewportWidth - naturalRect.width - margin);
maxWidth = viewportWidth - margin * 2;
}
const minWidth = Math.max(120, anchorRect.width);
this.dropdown.style.top = `${Math.max(margin, top)}px`;
this.dropdown.style.left = `${Math.max(margin, left)}px`;
this.dropdown.style.minWidth = `${minWidth}px`;
this.dropdown.style.maxWidth = `${maxWidth}px`;
this.dropdown.style.maxHeight = `${maxHeight}px`;
}
/**
* Scroll to the currently selected item
*/
scrollToSelected() {
if (!this.dropdown) return;
const selectedItem = this.dropdown.querySelector(
`[data-value="${CSS.escape(this.config.currentValue)}"]`
);
if (selectedItem) {
selectedItem.scrollIntoView({ block: "nearest" });
}
}
/**
* Setup event handlers for closing the dropdown
*/
setupCloseHandlers() {
this.closeHandler = (e) => {
if (this.dropdown && !this.dropdown.contains(e.target) && !this.config.anchor.contains(e.target)) {
this.hide();
}
};
const updateActiveItem = (newIndex) => {
if (!this.dropdown) return;
const items = this.dropdown.querySelectorAll(".custom-dropdown-item");
if (items.length === 0) return;
if (newIndex < 0) newIndex = 0;
if (newIndex >= items.length) newIndex = items.length - 1;
if (this.currentIndex >= 0 && this.currentIndex < items.length) {
const oldItem = items[this.currentIndex];
const isOldSelected = this.config.options[this.currentIndex] === this.config.currentValue;
oldItem.style.background = isOldSelected ? "rgba(78, 205, 196, 0.1)" : "transparent";
}
this.currentIndex = newIndex;
const newItem = items[this.currentIndex];
newItem.style.background = "var(--comfy-input-bg, #3a3a3a)";
newItem.scrollIntoView({ block: "nearest" });
};
this.keyHandler = (e) => {
if (e.key === "Escape") {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
this.hide();
return;
}
if (e.key === "ArrowDown") {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
updateActiveItem(this.currentIndex + 1);
} else if (e.key === "ArrowUp") {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
updateActiveItem(this.currentIndex - 1);
} else if (e.key === "Enter") {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
if (this.currentIndex >= 0 && this.currentIndex < this.config.options.length) {
this.config.onChange(this.config.options[this.currentIndex]);
this.hide();
}
}
};
this.scrollHandler = (e) => {
if (this.dropdown && e.target instanceof Node && this.dropdown.contains(e.target)) {
return;
}
this.hide();
};
setTimeout(() => {
window.addEventListener("keydown", this.keyHandler, true);
document.addEventListener("mousedown", this.closeHandler, true);
document.addEventListener("click", this.closeHandler, true);
window.addEventListener("scroll", this.scrollHandler, true);
}, 50);
}
/**
* Cleanup event handlers
*/
cleanupHandlers() {
if (this.closeHandler) {
document.removeEventListener("mousedown", this.closeHandler, true);
document.removeEventListener("click", this.closeHandler, true);
this.closeHandler = null;
}
if (this.keyHandler) {
window.removeEventListener("keydown", this.keyHandler, true);
this.keyHandler = null;
}
if (this.scrollHandler) {
window.removeEventListener("scroll", this.scrollHandler, true);
this.scrollHandler = null;
}
}
}
function createDropdownTrigger(currentValue, className = "custom-dropdown-trigger") {
const trigger = document.createElement("div");
trigger.className = className;
trigger.setAttribute("tabindex", "0");
trigger.setAttribute("role", "combobox");
trigger.setAttribute("aria-haspopup", "listbox");
const valueSpan = document.createElement("span");
valueSpan.className = "custom-dropdown-value";
valueSpan.textContent = currentValue;
valueSpan.style.cssText = `
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
const arrow = document.createElement("span");
arrow.className = "custom-dropdown-arrow";
arrow.innerHTML = `<svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 12 15 18 9"></polyline></svg>`;
arrow.style.cssText = `
flex-shrink: 0;
display: flex;
align-items: center;
opacity: 0.7;
`;
trigger.appendChild(valueSpan);
trigger.appendChild(arrow);
return trigger;
}
function updateDropdownTriggerValue(trigger, value) {
const valueSpan = trigger.querySelector(".custom-dropdown-value");
if (valueSpan) {
valueSpan.textContent = value;
}
}
export {
CustomDropdown,
createDropdownTrigger,
updateDropdownTriggerValue
};