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:
parent
c2e0cf4615
commit
2cda410b41
5 changed files with 329 additions and 119 deletions
|
@ -35,6 +35,10 @@ body {
|
|||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.display-none {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.collapsible:hover {
|
||||
background-color: #777;
|
||||
}
|
||||
|
|
|
@ -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: "";
|
||||
|
|
17
index.html
17
index.html
|
@ -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>
|
||||
|
|
171
js/index.js
171
js/index.js
|
@ -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, {
|
||||
try {
|
||||
const response = await 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);
|
||||
}
|
||||
});
|
||||
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,40 +648,33 @@ 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;
|
||||
modelAutoComplete.onchange.on(async ({value}) => {
|
||||
console.log(`[index] Changing model to [${value}]`);
|
||||
var payload = {
|
||||
sd_model_checkpoint: model_title,
|
||||
sd_model_checkpoint: value,
|
||||
};
|
||||
var url = document.getElementById("host").value + "/sdapi/v1/options/";
|
||||
fetch(url, {
|
||||
try {
|
||||
await 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) => {
|
||||
});
|
||||
|
||||
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"
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
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) {
|
||||
samplerSelect.value = localStorage.getItem("sampler");
|
||||
samplerAutoComplete.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);
|
||||
samplerAutoComplete.value = data[0].name;
|
||||
localStorage.setItem("sampler", samplerAutoComplete.value);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
alert(
|
||||
"Error getting samplers, please check console for additional info\n" +
|
||||
error
|
||||
);
|
||||
|
||||
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);
|
||||
|
|
173
js/lib/ui.js
173
js/lib/ui.js
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue