add custom select element with autocomplete

Same as a select, but you can kind of search for the item you want. Good
for when you have a lot of models.

Signed-off-by: Victor Seiji Hariki <victorseijih@gmail.com>
This commit is contained in:
Victor Seiji Hariki 2022-12-08 18:23:17 -03:00
parent c2e0cf4615
commit 2cda410b41
5 changed files with 329 additions and 119 deletions

View file

@ -35,6 +35,10 @@ body {
margin-bottom: 5px;
}
.display-none {
display: none;
}
.collapsible:hover {
background-color: #777;
}

View file

@ -40,6 +40,8 @@ div.slider-wrapper {
position: relative;
height: 20px;
border-radius: 5px;
overflow-y: visible;
}
div.slider-wrapper * {
@ -95,6 +97,51 @@ div.slider-wrapper > input.text {
background-color: transparent;
}
/* Autocomplete Select */
div.autocomplete {
border-radius: 5px;
}
div.autocomplete > .autocomplete-text {
box-sizing: border-box;
border-radius: 5px;
width: 100%;
}
div.autocomplete > .autocomplete-list {
position: absolute;
background-color: white;
overflow-y: auto;
margin-top: 0;
margin-left: 0;
max-height: 200px;
min-width: 100%;
max-width: 800px;
width: fit-content;
z-index: 200;
}
div.autocomplete > .autocomplete-list > .autocomplete-option {
cursor: pointer;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
padding: 3px;
}
div.autocomplete > .autocomplete-list > .autocomplete-option:hover {
background-color: #dddf;
}
/* Select Input */
select > option:checked::after {
content: "";

View file

@ -70,15 +70,10 @@
Stable Diffusion settings
</button>
<div class="content">
<label for="models">Model:</label>
<select
id="models"
class="wideSelect"
onchange="changeModel()"></select>
<br />
<label for="samplerSelect">Sampler:</label>
<select id="samplerSelect" onchange="changeSampler()"></select>
<br />
<label>Model:</label>
<div id="models-ac-select"></div>
<label>Sampler:</label>
<div id="sampler-ac-select"></div>
<label for="seed">Seed (-1 for random):</label>
<br />
<input
@ -123,8 +118,8 @@
<div class="content">
<button onclick="downloadCanvas()">Save canvas</button>
<br />
<label for="upscalers">Choose upscaler</label>
<select id="upscalers" class="wideSelect"></select>
<label>Choose upscaler</label>
<div id="upscaler-ac-select"></div>
<button onclick="upscaleAndDownload()">
Upscale (might take a sec)
</button>

View file

@ -111,7 +111,6 @@ function startup() {
};
drawBackground();
changeSampler();
changeMaskBlur();
changeSmoothRendering();
changeSeed();
@ -392,16 +391,6 @@ function drawMarchingAnts(ctx, bb, offset, options) {
ctx.restore();
}
function changeSampler() {
if (!document.getElementById("samplerSelect").value == "") {
// must be done, since before getSamplers is done, the options are empty
console.log(document.getElementById("samplerSelect").value == "");
stableDiffusionData.sampler_index =
document.getElementById("samplerSelect").value;
localStorage.setItem("sampler", stableDiffusionData.sampler_index);
}
}
const makeSlider = (
label,
el,
@ -435,6 +424,21 @@ const makeSlider = (
});
};
const modelAutoComplete = createAutoComplete(
"Model",
document.getElementById("models-ac-select")
);
const samplerAutoComplete = createAutoComplete(
"Sampler",
document.getElementById("sampler-ac-select")
);
const upscalerAutoComplete = createAutoComplete(
"Upscaler",
document.getElementById("upscaler-ac-select")
);
makeSlider(
"Resolution",
document.getElementById("resolution"),
@ -528,7 +532,7 @@ function drawBackground() {
}
}
function getUpscalers() {
async function getUpscalers() {
/*
so for some reason when upscalers request returns upscalers, the real-esrgan model names are incorrect, and need to be fetched from /sdapi/v1/realesrgan-models
also the realesrgan models returned are not all correct, extra fun!
@ -541,12 +545,9 @@ function getUpscalers() {
*/
// hacky way to get the correct list of upscalers
var upscalerSelect = document.getElementById("upscalers");
var extras_url =
document.getElementById("host").value + "/sdapi/v1/extra-single-image/"; // endpoint for upscaling, needed for the hacky way to get the correct list of upscalers
var empty_image = new Image(512, 512);
empty_image.src =
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAFCAAAAABCAYAAAChpRsuAAAALklEQVR42u3BAQ0AAAgDoJvc6LeHAybtBgAAAAAAAAAAAAAAAAAAAAAAAAB47QD2wAJ/LnnqGgAAAABJRU5ErkJggg=="; //transparent pixel
var empty_image = new Image(1, 1);
var purposefully_incorrect_data = {
"resize-mode": 0, // 0 = just resize, 1 = crop and resize, 2 = resize and fill i assume based on theimg2img tabs options
upscaling_resize: 2,
@ -554,27 +555,36 @@ function getUpscalers() {
image: empty_image.src,
};
fetch(extras_url, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(purposefully_incorrect_data),
})
.then((response) => response.json())
.then((data) => {
console.log("purposefully_incorrect_data response, ignore above error");
// result = purposefully_incorrect_data response: Invalid upscaler, needs to be on of these: None , Lanczos , Nearest , LDSR , BSRGAN , R-ESRGAN General 4xV3 , R-ESRGAN 4x+ Anime6B , ScuNET , ScuNET PSNR , SwinIR_4x
let upscalers = data.detail.split(": ")[1].trim().split(" , "); // converting the result to a list of upscalers
for (var i = 0; i < upscalers.length; i++) {
// if(upscalers[i] == "LDSR") continue; // Skip LDSR, see reason in the first comment // readded because worksonmymachine.jpg but leaving it here in case of, uh, future disaster?
var option = document.createElement("option");
option.text = upscalers[i];
option.value = upscalers[i];
upscalerSelect.add(option);
}
try {
const response = await fetch(extras_url, {
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(purposefully_incorrect_data),
});
const data = await response.json();
console.log(
"[index] purposefully_incorrect_data response, ignore above error"
);
// result = purposefully_incorrect_data response: Invalid upscaler, needs to be on of these: None , Lanczos , Nearest , LDSR , BSRGAN , R-ESRGAN General 4xV3 , R-ESRGAN 4x+ Anime6B , ScuNET , ScuNET PSNR , SwinIR_4x
const upscalers = data.detail
.split(": ")[1]
.split(",")
.map((v) => v.trim())
.filter((v) => v !== "None"); // converting the result to a list of upscalers
upscalerAutoComplete.options = upscalers.map((u) => {
return {name: u, value: u};
});
upscalerAutoComplete.value = upscalers[0];
} catch (e) {
console.warn("[index] Failed to fetch upscalers:");
console.warn(e);
}
/* THE NON HACKY WAY THAT I SIMPLY COULD NOT GET TO PRODUCE A LIST WITHOUT NON WORKING UPSCALERS, FEEL FREE TO TRY AND FIGURE IT OUT
@ -621,18 +631,14 @@ function getUpscalers() {
}
async function getModels() {
var modelSelect = document.getElementById("models");
var url = document.getElementById("host").value + "/sdapi/v1/sd-models";
await fetch(url)
.then((response) => response.json())
.then((data) => {
//console.log(data); All models
for (var i = 0; i < data.length; i++) {
var option = document.createElement("option");
option.text = data[i].model_name;
option.value = data[i].title;
modelSelect.add(option);
}
modelAutoComplete.options = data.map((option) => ({
name: option.title,
value: option.title,
}));
});
// get currently loaded model
@ -642,41 +648,34 @@ async function getModels() {
.then((data) => {
var model = data.sd_model_checkpoint;
console.log("Current model: " + model);
modelSelect.value = model;
console.debug((modelAutoComplete.value = model));
});
}
function changeModel() {
// change the model
console.log("changing model to " + document.getElementById("models").value);
var model_title = document.getElementById("models").value;
var payload = {
sd_model_checkpoint: model_title,
};
var url = document.getElementById("host").value + "/sdapi/v1/options/";
fetch(url, {
method: "POST",
mode: "cors", // no-cors, *cors, same-origin
cache: "default", // *default, no-cache, reload, force-cache, only-if-cached
credentials: "same-origin", // include, *same-origin, omit
redirect: "follow", // manual, *follow, error
referrerPolicy: "no-referrer", // no-referrer, *no-referrer-when-downgrade, origin, origin-when-cross-origin, same-origin, strict-origin, strict-origin-when-cross-origin, unsafe-url
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
})
.then((response) => response.json())
.then(() => {
alert("Model changed to " + model_title);
})
.catch((error) => {
modelAutoComplete.onchange.on(async ({value}) => {
console.log(`[index] Changing model to [${value}]`);
var payload = {
sd_model_checkpoint: value,
};
var url = document.getElementById("host").value + "/sdapi/v1/options/";
try {
await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
alert(`Model changed to [${value}]`);
} catch (e) {
console.warn("[index] Error changing model");
console.warn(e);
alert(
"Error changing model, please check console for additional info\n" +
error
"Error changing model, please check console for additional information"
);
});
}
});
}
async function getConfig() {
@ -810,42 +809,40 @@ function changeStyles() {
stableDiffusionData.styles = selectedString;
}
function getSamplers() {
var samplerSelect = document.getElementById("samplerSelect");
async function getSamplers() {
var url = document.getElementById("host").value + "/sdapi/v1/samplers";
fetch(url)
.then((response) => response.json())
.then((data) => {
//console.log(data); All samplers
for (var i = 0; i < data.length; i++) {
// PLMS SAMPLER DOES NOT WORK FOR ANY IMAGES BEYOND FOR THE INITIAL IMAGE (for me at least), GIVES ASGI Exception; AttributeError: 'PLMSSampler' object has no attribute 'stochastic_encode'
var option = document.createElement("option");
option.text = data[i].name;
option.value = data[i].name;
samplerSelect.add(option);
}
if (localStorage.getItem("sampler") != null) {
samplerSelect.value = localStorage.getItem("sampler");
} else {
// needed now, as hardcoded sampler cant be guaranteed to be in the list
samplerSelect.value = data[0].name;
localStorage.setItem("sampler", samplerSelect.value);
}
})
.catch((error) => {
alert(
"Error getting samplers, please check console for additional info\n" +
error
);
try {
const response = await fetch(url);
const data = await response.json();
samplerAutoComplete.options = data.map((sampler) => ({
name: sampler.name,
value: sampler.name,
}));
// Initial sampler
if (localStorage.getItem("sampler") != null) {
samplerAutoComplete.value = localStorage.getItem("sampler");
} else {
samplerAutoComplete.value = data[0].name;
localStorage.setItem("sampler", samplerAutoComplete.value);
}
samplerAutoComplete.onchange.on(({value}) => {
stableDiffusionData.sampler_index = value;
localStorage.setItem("sampler", value);
});
} catch (e) {
console.warn("[index] Failed to fetch samplers");
console.warn(e);
}
}
async function upscaleAndDownload() {
// Future improvements: some upscalers take a while to upscale, so we should show a loading bar or something, also a slider for the upscale amount
// get cropped canvas, send it to upscaler, download result
var upscale_factor = 2; // TODO: make this a user input 1.x - 4.0 or something
var upscaler = document.getElementById("upscalers").value;
var upscaler = upscalerAutoComplete.value;
var croppedCanvas = cropCanvas(
uil.getVisible({
x: 0,
@ -855,7 +852,6 @@ async function upscaleAndDownload() {
})
);
if (croppedCanvas != null) {
var upscaler = document.getElementById("upscalers").value;
var url =
document.getElementById("host").value + "/sdapi/v1/extra-single-image/";
var imgdata = croppedCanvas.canvas.toDataURL("image/png");
@ -903,10 +899,6 @@ function loadSettings() {
localStorage.getItem("neg_prompt") == null
? "people, person, humans, human, divers, diver, glitch, error, text, watermark, bad quality, blurry"
: localStorage.getItem("neg_prompt");
var _sampler =
localStorage.getItem("sampler") == null
? "DDIM"
: localStorage.getItem("sampler");
var _mask_blur =
localStorage.getItem("mask_blur") == null
? 0
@ -924,7 +916,6 @@ function loadSettings() {
document.getElementById("prompt").title = String(_prompt);
document.getElementById("negPrompt").value = String(_negprompt);
document.getElementById("negPrompt").title = String(_negprompt);
document.getElementById("samplerSelect").value = String(_sampler);
document.getElementById("maskBlur").value = Number(_mask_blur);
document.getElementById("seed").value = Number(_seed);
document.getElementById("cbxHRFix").checked = Boolean(_enable_hr);

View file

@ -192,3 +192,176 @@ function createSlider(name, wrapper, options = {}) {
},
};
}
/**
* A function to transform a div into a autocompletable select element
*
* @param {string} name Name of the AutoComplete Select Element
* @param {HTMLDivElement} wrapper The div element that will wrap the input elements
* @param {object} options Extra options
* @param {{name: string, value: string}} options.options Options to add to the selector
* @returns {AutoCompleteElement}
*/
function createAutoComplete(name, wrapper, options = {}) {
defaultOpt(options, {
options: [],
});
wrapper.classList.add("autocomplete");
const inputEl = document.createElement("input");
inputEl.type = "text";
inputEl.classList.add("autocomplete-text");
const autocompleteEl = document.createElement("div");
autocompleteEl.classList.add("autocomplete-list", "display-none");
let timeout = null;
let ontext = false;
let onlist = false;
wrapper.appendChild(inputEl);
wrapper.appendChild(autocompleteEl);
const acobj = {
name,
wrapper,
_title: null,
_value: null,
_options: [],
/** @type {Observer<{name:string, value: string}>} */
onchange: new Observer(),
get value() {
return this._value;
},
set value(val) {
const opt = this.options.find((option) => option.value === val);
if (!opt) return;
this._title = opt.name;
this._value = opt.value;
inputEl.value = opt.name;
inputEl.title = opt.name;
this.onchange.emit({name: opt.name, value: opt.value});
},
get options() {
return this._options;
},
set options(val) {
console.debug(val);
this._options = [];
while (autocompleteEl.lastChild) {
autocompleteEl.removeChild(autocompleteEl.lastChild);
}
// Add options
val.forEach((opt) => {
const {name, value} = opt;
const option = {name, value};
const optionEl = document.createElement("option");
optionEl.classList.add("autocomplete-option");
optionEl.title = option.name;
optionEl.addEventListener("click", () => select(option));
this._options.push({name, value, optionElement: optionEl});
autocompleteEl.appendChild(optionEl);
});
updateOptions();
},
};
function updateOptions() {
const text = inputEl.value.toLowerCase().trim();
acobj._options.forEach((opt) => {
const textLocation = opt.name.toLowerCase().indexOf(text);
while (opt.optionElement.lastChild) {
opt.optionElement.removeChild(opt.optionElement.lastChild);
}
opt.optionElement.append(
document.createTextNode(opt.name.substring(0, textLocation))
);
const span = document.createElement("span");
span.style.fontWeight = "bold";
span.textContent = opt.name.substring(
textLocation,
textLocation + text.length
);
opt.optionElement.appendChild(span);
opt.optionElement.appendChild(
document.createTextNode(
opt.name.substring(textLocation + text.length, opt.name.length)
)
);
if (textLocation !== -1) {
opt.optionElement.classList.remove("display-none");
} else opt.optionElement.classList.add("display-none");
});
}
function select(options) {
ontext = false;
onlist = false;
acobj._title = options.name;
inputEl.value = options.name;
acobj.value = options.value;
autocompleteEl.classList.add("display-none");
}
inputEl.addEventListener("focus", () => {
ontext = true;
autocompleteEl.classList.remove("display-none");
inputEl.select();
});
inputEl.addEventListener("blur", () => {
ontext = false;
if (!onlist && !ontext) {
inputEl.value = "";
updateOptions();
inputEl.value = acobj._title;
autocompleteEl.classList.add("display-none");
}
});
autocompleteEl.addEventListener("mouseenter", () => {
onlist = true;
});
autocompleteEl.addEventListener("mouseleave", () => {
onlist = false;
if (!onlist && !ontext) {
inputEl.value = "";
updateOptions();
inputEl.value = acobj._title;
autocompleteEl.classList.add("display-none");
}
});
// Filter
inputEl.addEventListener("input", () => {
updateOptions();
});
acobj.options = options.options;
return acobj;
}