"use strict" let modelsCache let modelsOptions /* *** SEARCHABLE MODELS *** Creates searchable dropdowns for SD, VAE, or HN models. Also adds a reload models button (placed next to SD models, reloads everything including VAE and HN models). More reload buttons may be added at strategic UI locations as needed. Merely calling getModels() makes all the magic happen behind the scene to refresh the dropdowns. HOW TO CREATE A MODEL DROPDOWN: 1) Create an input element. Make sure to add a data-path property, as this is how model dropdowns are identified in auto-save.js. 2) Just declare one of these for your own dropdown (remember to change the element id, e.g. #stable_diffusion_models to your own input's id). let stableDiffusionModelField = new ModelDropdown(document.querySelector('#stable_diffusion_model'), 'stable-diffusion') let vaeModelField = new ModelDropdown(document.querySelector('#vae_model'), 'vae', 'None') let hypernetworkModelField = new ModelDropdown(document.querySelector('#hypernetwork_model'), 'hypernetwork', 'None') 3) Model dropdowns will be refreshed automatically when the reload models button is invoked. */ class ModelDropdown { modelFilter //= document.querySelector("#model-filter") modelFilterArrow //= document.querySelector("#model-filter-arrow") modelList //= document.querySelector("#model-list") modelResult //= document.querySelector("#model-result") modelNoResult //= document.querySelector("#model-no-result") currentSelection //= { elem: undefined, value: '', path: ''} highlightedModelEntry //= undefined activeModel //= undefined inputModels //= undefined modelKey //= undefined flatModelList //= [] noneEntry //= '' modelFilterInitialized //= undefined /* MIMIC A REGULAR INPUT FIELD */ get parentElement() { return this.modelFilter.parentElement } get parentNode() { return this.modelFilter.parentNode } get value() { return this.modelFilter.dataset.path } set value(path) { this.modelFilter.dataset.path = path this.selectEntry(path) } addEventListener(type, listener, options) { return this.modelFilter.addEventListener(type, listener, options) } dispatchEvent(event) { return this.modelFilter.dispatchEvent(event) } appendChild(option) { // do nothing } // remember 'this' - http://blog.niftysnippets.org/2008/04/you-must-remember-this.html bind(f, obj) { return function() { return f.apply(obj, arguments) } } /* SEARCHABLE INPUT */ constructor (input, modelKey, noneEntry = '') { this.modelFilter = input this.noneEntry = noneEntry this.modelKey = modelKey if (modelsOptions !== undefined) { // reuse models from cache (only useful for plugins, which are loaded after models) this.inputModels = modelsOptions[this.modelKey] this.populateModels() } document.addEventListener("refreshModels", this.bind(function(e) { // reload the models this.inputModels = modelsOptions[this.modelKey] this.populateModels() }, this)) document.addEventListener("collapsibleClick", this.bind(function(e) { // update the input size when the element becomes visible this.updateInputSize() }, this)) document.addEventListener("tabClick", this.bind(function(e) { // update the input size when the tab changes this.updateInputSize() }, this)) } saveCurrentSelection(elem, value, path) { this.currentSelection.elem = elem this.currentSelection.value = value this.currentSelection.path = path this.modelFilter.dataset.path = path this.modelFilter.value = value this.modelFilter.dispatchEvent(new Event('change')) } processClick(e) { e.preventDefault() if (e.srcElement.classList.contains('model-file')) { this.saveCurrentSelection(e.srcElement, e.srcElement.innerText, e.srcElement.dataset.path) this.hideModelList() this.modelFilter.focus() this.modelFilter.select() } } getPreviousVisibleSibling(elem) { let prevSibling = elem.previousElementSibling while (prevSibling && prevSibling.classList.contains('model-file')) { if (prevSibling.style.display == 'list-item') return prevSibling prevSibling = prevSibling.previousElementSibling } if (prevSibling && prevSibling.style.display == 'list-item') return prevSibling } getLastVisibleChild(elem) { let lastElementChild = elem.lastElementChild if (lastElementChild.style.display == 'list-item') return lastElementChild return this.getPreviousVisibleSibling(lastElementChild) } findPreviousSibling(elem) { // is there an immediate sibling? let prevSibling = this.getPreviousVisibleSibling(elem) if (prevSibling) { // if the previous sibling is a model file, just select it if (prevSibling.classList.contains('model-file')) return prevSibling // if the previous sibling is a model folder, select the last model file it contains if (prevSibling.classList.contains('model-folder')) return prevSibling.firstElementChild.lastElementChild } // no sibling model file and no sibling model folder. look for siblings around the parent element. prevSibling = this.getPreviousVisibleSibling(elem.parentElement.parentElement) if (prevSibling) { // if the previous entry is a model file, select it if (prevSibling.classList.contains('model-file')) return prevSibling // is there another model folder to jump to before the current one? if (prevSibling.classList.contains('model-folder')) return this.getLastVisibleChild(prevSibling.firstElementChild) } } getNextVisibleSibling(elem) { let nextSibling = elem.nextElementSibling while (nextSibling && nextSibling.classList.contains('model-file')) { if (nextSibling.style.display == 'list-item') return nextSibling nextSibling = nextSibling.nextElementSibling } if (nextSibling && nextSibling.style.display == 'list-item') return nextSibling } getFirstVisibleChild(elem) { let firstElementChild = elem.firstElementChild if (firstElementChild.style.display == 'list-item') return firstElementChild return this.getNextVisibleSibling(firstElementChild) } findNextSibling(elem) { // is there an immediate sibling? let nextSibling = this.getNextVisibleSibling(elem) if (nextSibling) { // if the next sibling is a model file, just select it if (nextSibling.classList.contains('model-file')) return nextSibling // if the next sibling is a model folder, select the first model file it contains if (nextSibling.classList.contains('model-folder')) return nextSibling.firstElementChild.firstElementChild } // no sibling model file and no sibling model folder. look for siblings around the parent element. nextSibling = this.getNextVisibleSibling(elem.parentElement.parentElement) if (nextSibling) { // if the next entry is a model file, select it if (nextSibling.classList.contains('model-file')) return nextSibling // is there another model folder to jump to after the current one? if (nextSibling.classList.contains('model-folder')) return this.getFirstVisibleChild(nextSibling.firstElementChild) } } selectModelEntry(elem) { if (elem) { if (this.highlightedModelEntry !== undefined) { this.highlightedModelEntry.classList.remove('selected') } this.saveCurrentSelection(elem, elem.innerText, elem.dataset.path) elem.classList.add('selected') elem.scrollIntoView({block: 'nearest'}) this.highlightedModelEntry = elem } } selectPreviousFile() { const elem = this.findPreviousSibling(this.highlightedModelEntry) if (elem) { this.selectModelEntry(elem) } else { //this.highlightedModelEntry.parentElement.parentElement.scrollIntoView({block: 'nearest'}) this.highlightedModelEntry.closest('.model-list').scrollTop = 0 } this.modelFilter.select() } selectNextFile() { this.selectModelEntry(this.findNextSibling(this.highlightedModelEntry)) this.modelFilter.select() } selectFirstFile() { this.selectModelEntry(this.modelList.querySelector('.model-file')) this.highlightedModelEntry.scrollIntoView({block: 'nearest'}) this.modelFilter.select() } selectLastFile() { const elems = this.modelList.querySelectorAll('.model-file:last-child') this.selectModelEntry(elems[elems.length -1]) this.modelFilter.select() } resetSelection() { this.hideModelList() this.showAllEntries() this.modelFilter.value = this.currentSelection.value this.modelFilter.focus() this.modelFilter.select() } validEntrySelected() { return (this.modelNoResult.style.display === 'none') } processKey(e) { switch (e.key) { case 'Escape': e.preventDefault() this.resetSelection() break case 'Enter': e.preventDefault() if (this.validEntrySelected()) { if (this.modelList.style.display != 'block') { this.showModelList() } else { this.saveCurrentSelection(this.highlightedModelEntry, this.highlightedModelEntry.innerText, this.highlightedModelEntry.dataset.path) this.hideModelList() this.showAllEntries() } this.modelFilter.focus() } else { this.resetSelection() } break case 'ArrowUp': e.preventDefault() if (this.validEntrySelected()) { this.selectPreviousFile() } break case 'ArrowDown': e.preventDefault() if (this.validEntrySelected()) { this.selectNextFile() } break case 'ArrowLeft': if (this.modelList.style.display != 'block') { e.preventDefault() } break case 'ArrowRight': if (this.modelList.style.display != 'block') { e.preventDefault() } break case 'PageUp': e.preventDefault() if (this.validEntrySelected()) { this.selectPreviousFile() this.selectPreviousFile() this.selectPreviousFile() this.selectPreviousFile() this.selectPreviousFile() this.selectPreviousFile() this.selectPreviousFile() this.selectPreviousFile() } break case 'PageDown': e.preventDefault() if (this.validEntrySelected()) { this.selectNextFile() this.selectNextFile() this.selectNextFile() this.selectNextFile() this.selectNextFile() this.selectNextFile() this.selectNextFile() this.selectNextFile() } break case 'Home': //if (this.modelList.style.display != 'block') { e.preventDefault() if (this.validEntrySelected()) { this.selectFirstFile() } //} break case 'End': //if (this.modelList.style.display != 'block') { e.preventDefault() if (this.validEntrySelected()) { this.selectLastFile() } //} break default: //console.log(e.key) } } modelListFocus() { this.selectEntry() this.showAllEntries() } showModelList() { this.modelList.style.display = 'block' this.selectEntry() this.showAllEntries() this.modelFilter.value = '' this.modelFilter.focus() this.modelFilter.style.cursor = 'auto' } hideModelList() { this.modelList.style.display = 'none' this.modelFilter.value = this.currentSelection.value this.modelFilter.style.cursor = '' } toggleModelList(e) { e.preventDefault() if (this.modelList.style.display != 'block') { this.showModelList() } else { this.hideModelList() this.modelFilter.select() } } selectEntry(path) { if (path !== undefined) { const entries = this.modelList.querySelectorAll('.model-file'); for (let i = 0; i < entries.length; i++) { if (entries[i].dataset.path == path) { this.saveCurrentSelection(entries[i], entries[i].innerText, entries[i].dataset.path) this.highlightedModelEntry = entries[i] entries[i].scrollIntoView({block: 'nearest'}) break } } } if (this.currentSelection.elem !== undefined) { // select the previous element if (this.highlightedModelEntry !== undefined && this.highlightedModelEntry != this.currentSelection.elem) { this.highlightedModelEntry.classList.remove('selected') } this.currentSelection.elem.classList.add('selected') this.highlightedModelEntry = this.currentSelection.elem this.currentSelection.elem.scrollIntoView({block: 'nearest'}) } else { this.selectFirstFile() } } highlightModelAtPosition(e) { let elem = document.elementFromPoint(e.clientX, e.clientY) if (elem.classList.contains('model-file')) { this.highlightModel(elem) } } highlightModel(elem) { if (elem.classList.contains('model-file')) { if (this.highlightedModelEntry !== undefined && this.highlightedModelEntry != elem) { this.highlightedModelEntry.classList.remove('selected') } elem.classList.add('selected') this.highlightedModelEntry = elem } } showAllEntries() { this.modelList.querySelectorAll('li').forEach(function(li) { if (li.id !== 'model-no-result') { li.style.display = 'list-item' } }) this.modelNoResult.style.display = 'none' } filterList(e) { const filter = this.modelFilter.value.toLowerCase() let found = false let showAllChildren = false this.modelList.querySelectorAll('li').forEach(function(li) { if (li.classList.contains('model-folder')) { showAllChildren = false } if (filter == '') { li.style.display = 'list-item' found = true } else if (showAllChildren || li.textContent.toLowerCase().match(filter)) { li.style.display = 'list-item' if (li.classList.contains('model-folder') && li.firstChild.textContent.toLowerCase().match(filter)) { showAllChildren = true } found = true } else { li.style.display = 'none' } }) if (found) { this.modelResult.style.display = 'list-item' this.modelNoResult.style.display = 'none' const elem = this.findNextSibling(this.modelList.querySelector('.model-file')) this.highlightModel(elem) elem.scrollIntoView({block: 'nearest'}) } else { this.modelResult.style.display = 'none' this.modelNoResult.style.display = 'list-item' } this.modelList.style.display = 'block' } /* MODEL LOADER */ getElementDimensions(element) { // Clone the element const clone = element.cloneNode(true) // Copy the styles of the original element to the cloned element const originalStyles = window.getComputedStyle(element) for (let i = 0; i < originalStyles.length; i++) { const property = originalStyles[i] clone.style[property] = originalStyles.getPropertyValue(property) } // Set its visibility to hidden and display to inline-block clone.style.visibility = "hidden" clone.style.display = "inline-block" // Put the cloned element next to the original element element.parentNode.insertBefore(clone, element.nextSibling) // Get its width and height const width = clone.offsetWidth const height = clone.offsetHeight // Remove it from the DOM clone.remove() // Return its width and height return { width, height } } updateInputSize() { if (this.modelList !== undefined) { const dimensions = this.getElementDimensions(this.modelList) this.modelFilter.style.width = dimensions.width + 'px' this.modelList.style.width = dimensions.width + 'px' this.modelFilterArrow.style.height = dimensions.height + 'px' if (this.modelFilter.offsetLeft > 0) { this.modelList.style.left = this.modelFilter.offsetLeft + 'px' } } } flattenModelList(models, path) { models.forEach(entry => { if (Array.isArray(entry)) { this.flattenModelList(entry[1], path + '/' + entry[0]) } else { this.flatModelList.push(path == '' ? entry : path + '/' + entry) } }) } // sort models getFolder(model) { return model.substring(0, model.lastIndexOf('/') + 1) } sortModels(models) { // sort the models in alphabetical order, root folder models last models.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase())) /* // sort the models in alphabetical order, root folder models first models = models.sort() let found do { found = false for (let i = 0; i < models.length - 1; i++) { if ( (this.getFolder(models[i]) === this.getFolder(models[i+1]) && models[i].toLowerCase().localeCompare(models[i+1].toLowerCase()) > 0) // same folder, sort by alphabetical order || (this.getFolder(models[i]).toLowerCase().localeCompare(this.getFolder(models[i+1]).toLowerCase()) > 0) // L1 folder > L2 folder ) { [models[i], models[i+1]] = [models[i+1], models[i]] found = true } } } while (found) */ } populateModels() { this.activeModel = this.modelFilter.dataset.path this.currentSelection = { elem: undefined, value: '', path: ''} this.highlightedModelEntry = undefined this.flatModelList = [] if(this.modelList !== undefined) { this.modelList.remove() this.modelFilterArrow.remove() } this.createDropdown() } createDropdown() { // prepare to sort the models this.flattenModelList(this.inputModels, '') this.sortModels(this.flatModelList) // create dropdown entries this.modelFilter.insertAdjacentHTML('afterend', this.parseModels(this.flatModelList)) this.modelFilter.classList.add('model-selector') this.modelFilterArrow = document.querySelector(`#${this.modelFilter.id}-model-filter-arrow`) this.modelList = document.querySelector(`#${this.modelFilter.id}-model-list`) this.modelResult = document.querySelector(`#${this.modelFilter.id}-model-result`) this.modelNoResult = document.querySelector(`#${this.modelFilter.id}-model-no-result`) this.updateInputSize() if (this.modelFilterInitialized !== true) { this.modelFilter.addEventListener('input', this.bind(this.filterList, this)) this.modelFilter.addEventListener('focus', this.bind(this.modelListFocus, this)) this.modelFilter.addEventListener('blur', this.bind(this.hideModelList, this)) this.modelFilter.addEventListener('click', this.bind(this.showModelList, this)) this.modelFilter.addEventListener('keydown', this.bind(this.processKey, this)) this.modelFilterInitialized = true } this.modelFilterArrow.addEventListener('mousedown', this.bind(this.toggleModelList, this)) this.modelList.addEventListener('mousemove', this.bind(this.highlightModelAtPosition, this)) this.modelList.addEventListener('mousedown', this.bind(this.processClick, this)) this.selectEntry(this.activeModel) } parseModels(models) { let html = `