// ttN Dropdown let activeDropdown = null; class Dropdown { constructor(inputEl, options, onSelect, isDict, manualOffset, hostElement) { this.dropdown = document.createElement('ul'); this.dropdown.setAttribute('role', 'listbox'); this.dropdown.classList.add('ttN-dropdown'); this.selectedIndex = -1; this.inputEl = inputEl; this.options = options; this.onSelect = onSelect; this.isDict = isDict; this.manualOffsetX = manualOffset[0]; this.manualOffsetY = manualOffset[1]; this.hostElement = hostElement; this.focusedDropdown = this.dropdown; this.buildDropdown(); this.onKeyDownBound = this.onKeyDown.bind(this); this.onWheelBound = this.onWheel.bind(this); this.onClickBound = this.onClick.bind(this); this.addEventListeners(); } buildDropdown() { if (this.isDict) { this.buildNestedDropdown(this.options, this.dropdown); } else { this.options.forEach((suggestion, index) => { this.addListItem(suggestion, index, this.dropdown); }); } const inputRect = this.inputEl.getBoundingClientRect(); if (isNaN(this.manualOffsetX) && this.manualOffsetX.includes('%')) { this.manualOffsetX = (inputRect.height * (parseInt(this.manualOffsetX) / 100)) } if (isNaN(this.manualOffsetY) && this.manualOffsetY.includes('%')) { this.manualOffsetY = (inputRect.width * (parseInt(this.manualOffsetY) / 100)) } this.dropdown.style.top = (inputRect.top + inputRect.height - this.manualOffsetX) + 'px'; this.dropdown.style.left = (inputRect.left + inputRect.width - this.manualOffsetY) + 'px'; this.hostElement.appendChild(this.dropdown); activeDropdown = this; } buildNestedDropdown(dictionary, parentElement, currentPath = '') { let index = 0; Object.keys(dictionary).forEach((key) => { let extra_data; const item = dictionary[key]; if (typeof item === 'string') { extra_data = item; } let fullPath = currentPath ? `${currentPath}/${key}` : key; if (extra_data) { fullPath = `${fullPath}###${extra_data}`; } if (typeof item === "object" && item !== null) { const nestedDropdown = document.createElement('ul'); nestedDropdown.setAttribute('role', 'listbox'); nestedDropdown.classList.add('ttN-nested-dropdown'); const parentListItem = document.createElement('li'); parentListItem.classList.add('folder'); parentListItem.textContent = key; parentListItem.appendChild(nestedDropdown); parentListItem.addEventListener('mouseover', this.onMouseOver.bind(this, index, parentElement)); parentElement.appendChild(parentListItem); this.buildNestedDropdown(item, nestedDropdown, fullPath); index = index + 1; } else { const listItem = document.createElement('li'); listItem.classList.add('item'); listItem.setAttribute('role', 'option'); listItem.textContent = key; listItem.addEventListener('mouseover', this.onMouseOver.bind(this, index, parentElement)); listItem.addEventListener('mousedown', (e) => this.onMouseDown(key, e, fullPath)); parentElement.appendChild(listItem); index = index + 1; } }); } addListItem(item, index, parentElement) { const listItem = document.createElement('li'); listItem.setAttribute('role', 'option'); listItem.textContent = item; listItem.addEventListener('mouseover', (e) => this.onMouseOver(index)); listItem.addEventListener('mousedown', (e) => this.onMouseDown(item, e)); parentElement.appendChild(listItem); } addEventListeners() { document.addEventListener('keydown', this.onKeyDownBound); this.dropdown.addEventListener('wheel', this.onWheelBound); document.addEventListener('click', this.onClickBound); } removeEventListeners() { document.removeEventListener('keydown', this.onKeyDownBound); this.dropdown.removeEventListener('wheel', this.onWheelBound); document.removeEventListener('click', this.onClickBound); } onMouseOver(index, parentElement=null) { if (parentElement) { this.focusedDropdown = parentElement; } this.selectedIndex = index; this.updateSelection(); } onMouseOut() { this.selectedIndex = -1; this.updateSelection(); } onMouseDown(suggestion, event, fullPath='') { event.preventDefault(); this.onSelect(suggestion, fullPath); this.dropdown.remove(); this.removeEventListeners(); } onKeyDown(event) { const enterKeyCode = 13; const escKeyCode = 27; const arrowUpKeyCode = 38; const arrowDownKeyCode = 40; const arrowRightKeyCode = 39; const arrowLeftKeyCode = 37; const tabKeyCode = 9; const items = Array.from(this.focusedDropdown.children); const selectedItem = items[this.selectedIndex]; if (activeDropdown) { if (event.keyCode === arrowUpKeyCode) { event.preventDefault(); this.selectedIndex = Math.max(0, this.selectedIndex - 1); this.updateSelection(); } else if (event.keyCode === arrowDownKeyCode) { event.preventDefault(); this.selectedIndex = Math.min(items.length - 1, this.selectedIndex + 1); this.updateSelection(); } else if (event.keyCode === arrowRightKeyCode && selectedItem) { event.preventDefault(); if (selectedItem.classList.contains('folder')) { const nestedDropdown = selectedItem.querySelector('.ttN-nested-dropdown'); if (nestedDropdown) { this.focusedDropdown = nestedDropdown; this.selectedIndex = 0; this.updateSelection(); } } } else if (event.keyCode === arrowLeftKeyCode && this.focusedDropdown !== this.dropdown) { const parentDropdown = this.focusedDropdown.closest('.ttN-dropdown, .ttN-nested-dropdown').parentNode.closest('.ttN-dropdown, .ttN-nested-dropdown'); if (parentDropdown) { this.focusedDropdown = parentDropdown; this.selectedIndex = Array.from(parentDropdown.children).indexOf(this.focusedDropdown.parentNode); this.updateSelection(); } } else if ((event.keyCode === enterKeyCode || event.keyCode === tabKeyCode) && this.selectedIndex >= 0) { event.preventDefault(); if (selectedItem.classList.contains('item')) { this.onSelect(items[this.selectedIndex].textContent); this.dropdown.remove(); this.removeEventListeners(); } const nestedDropdown = selectedItem.querySelector('.ttN-nested-dropdown'); if (nestedDropdown) { this.focusedDropdown = nestedDropdown; this.selectedIndex = 0; this.updateSelection(); } } else if (event.keyCode === escKeyCode) { this.dropdown.remove(); this.removeEventListeners(); } } } onWheel(event) { const top = parseInt(this.dropdown.style.top); if (localStorage.getItem("Comfy.Settings.Comfy.InvertMenuScrolling")) { this.dropdown.style.top = (top + (event.deltaY < 0 ? 10 : -10)) + "px"; } else { this.dropdown.style.top = (top + (event.deltaY < 0 ? -10 : 10)) + "px"; } } onClick(event) { if (!this.dropdown.contains(event.target) && event.target !== this.inputEl) { this.dropdown.remove(); this.removeEventListeners(); } } updateSelection() { if (!this.focusedDropdown.children) { this.dropdown.classList.add('selected'); } else { Array.from(this.focusedDropdown.children).forEach((li, index) => { if (index === this.selectedIndex) { li.classList.add('selected'); } else { li.classList.remove('selected'); } }); } } } export function ttN_RemoveDropdown() { if (activeDropdown) { activeDropdown.removeEventListeners(); activeDropdown.dropdown.remove(); activeDropdown = null; } } export function ttN_CreateDropdown(inputEl, options, onSelect, isDict = false, manualOffset = [10,'100%'], hostElement = document.body) { ttN_RemoveDropdown(); new Dropdown(inputEl, options, onSelect, isDict, manualOffset, hostElement); }