diff --git a/css/ui/generic.css b/css/ui/generic.css index 7be86b1..128d679 100644 --- a/css/ui/generic.css +++ b/css/ui/generic.css @@ -145,6 +145,24 @@ div.autocomplete > .autocomplete-list > .autocomplete-option:hover { background-color: #dddf; } +div.autocomplete > .autocomplete-list > .autocomplete-option.selected::after { + content: ""; + + position: absolute; + right: 5px; + top: 0; + + height: 100%; + aspect-ratio: 1; + + background-color: darkgreen; + + -webkit-mask-image: url("/res/icons/check.svg"); + -webkit-mask-size: contain; + mask-image: url("/res/icons/check.svg"); + mask-size: contain; +} + /* Select Input */ select > option:checked::after { content: ""; diff --git a/index.html b/index.html index 79ef8dd..1c9b8c9 100644 --- a/index.html +++ b/index.html @@ -62,11 +62,7 @@ - +
diff --git a/js/index.js b/js/index.js index 35eb3dc..63340c1 100644 --- a/js/index.js +++ b/js/index.js @@ -447,6 +447,12 @@ const makeSlider = ( }); }; +const styleAutoComplete = createAutoComplete( + "Style", + document.getElementById("style-ac-mselect"), + {multiple: true} +); + const modelAutoComplete = createAutoComplete( "Model", document.getElementById("models-ac-select") @@ -797,28 +803,23 @@ async function getStyles() { stored = []; } - data.forEach((style) => { - const option = document.createElement("option"); - option.classList.add("style-select-option"); - option.text = style.name; - option.value = style.name; - option.title = `prompt: ${style.prompt}\nnegative: ${style.negative_prompt}`; - if (stored.length === 0) option.selected = style.name === "None"; - else - option.selected = !!stored.find( - (styleName) => style.name === styleName - ); - - styleSelect.add(option); - }); - - changeStyles(); - - stored.forEach((styleName, index) => { - if (!data.findIndex((style) => style.name === styleName)) { - stored.splice(index, 1); + styleAutoComplete.options = data.map((style) => ({ + name: style.name, + value: style.name, + title: `prompt: ${style.prompt}\nnegative: ${style.negative_prompt}`, + })); + styleAutoComplete.onchange.on(({value}) => { + let selected = []; + if (value.find((v) => v === "None")) { + styleAutoComplete.value = []; + } else { + selected = value; } + stableDiffusionData.styles = selected; + localStorage.setItem("promptStyle", JSON.stringify(selected)); }); + + styleAutoComplete.value = stored; localStorage.setItem("promptStyle", JSON.stringify(stored)); } catch (e) { console.warn("[index] Failed to fetch prompt styles"); diff --git a/js/lib/ui.js b/js/lib/ui.js index bbb3f3a..a835567 100644 --- a/js/lib/ui.js +++ b/js/lib/ui.js @@ -199,11 +199,13 @@ function createSlider(name, wrapper, options = {}) { * @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 + * @param {boolean} options.multiple Whether multiple options can be selected + * @param {{name: string, value: string}[]} options.options Options to add to the selector * @returns {AutoCompleteElement} */ function createAutoComplete(name, wrapper, options = {}) { defaultOpt(options, { + multiple: false, options: [], }); @@ -226,27 +228,46 @@ function createAutoComplete(name, wrapper, options = {}) { const acobj = { name, wrapper, - _title: null, - _value: null, + _selectedOptions: new Set(), _options: [], /** @type {Observer<{name:string, value: string}>} */ onchange: new Observer(), get value() { - return this._value; + const v = this._selectedOptions.map((opt) => opt.value); + return options.multiple ? v : v[0]; }, - set value(val) { - const opt = this.options.find((option) => option.value === val); + set value(values) { + this._selectedOptions.clear(); - if (!opt) return; + for (const val of options.multiple ? values : [values]) { + const opt = this.options.find((option) => option.value === val); - this._title = opt.name; - this._value = opt.value; - inputEl.value = opt.name; - inputEl.title = opt.name; + if (!opt) continue; // Ignore invalid options - this.onchange.emit({name: opt.name, value: opt.value}); + this._selectedOptions.add(opt); + } + + this._sync(); + }, + + _sync() { + const val = Array.from(this._selectedOptions).map((opt) => opt.value); + const name = Array.from(this._selectedOptions).map((opt) => opt.name); + + for (const opt of this._options) { + if (acobj._selectedOptions.has(opt)) + opt.optionElement.classList.add("selected"); + else opt.optionElement.classList.remove("selected"); + } + + updateInputField(); + + this.onchange.emit({ + name: options.multiple ? name : name[0], + value: options.multiple ? val : val[0], + }); }, get options() { @@ -261,15 +282,17 @@ function createAutoComplete(name, wrapper, options = {}) { // Add options val.forEach((opt) => { - const {name, value} = opt; - const option = {name, value}; + const {name, value, title} = opt; const optionEl = document.createElement("option"); optionEl.classList.add("autocomplete-option"); - optionEl.title = option.name; - optionEl.addEventListener("click", () => select(option)); + optionEl.title = title || name; - this._options.push({name, value, optionElement: optionEl}); + const option = {name, value, optionElement: optionEl}; + + this._options.push(option); + + optionEl.addEventListener("click", () => select(option)); autocompleteEl.appendChild(optionEl); }); @@ -278,6 +301,15 @@ function createAutoComplete(name, wrapper, options = {}) { }, }; + function updateInputField() { + inputEl.value = Array.from(acobj._selectedOptions) + .map((o) => o.name) + .join(", "); + inputEl.title = Array.from(acobj._selectedOptions) + .map((o) => o.name) + .join(", "); + } + function updateOptions() { const text = inputEl.value.toLowerCase().trim(); @@ -310,15 +342,26 @@ function createAutoComplete(name, wrapper, options = {}) { }); } - function select(options) { + function select(opt) { ontext = false; - onlist = false; + if (!options.multiple) { + onlist = false; + acobj._selectedOptions.clear(); + autocompleteEl.classList.add("display-none"); + for (const child of autocompleteEl.children) { + child.classList.remove("selected"); + } + } - acobj._title = options.name; - inputEl.value = options.name; - acobj.value = options.value; + if (options.multiple && acobj._selectedOptions.has(opt)) { + acobj._selectedOptions.delete(opt); + opt.optionElement.classList.remove("selected"); + } else { + acobj._selectedOptions.add(opt); + opt.optionElement.classList.add("selected"); + } - autocompleteEl.classList.add("display-none"); + acobj._sync(); } inputEl.addEventListener("focus", () => { @@ -331,9 +374,7 @@ function createAutoComplete(name, wrapper, options = {}) { ontext = false; if (!onlist && !ontext) { - inputEl.value = ""; - updateOptions(); - inputEl.value = acobj._title; + updateInputField(); autocompleteEl.classList.add("display-none"); } @@ -347,9 +388,7 @@ function createAutoComplete(name, wrapper, options = {}) { onlist = false; if (!onlist && !ontext) { - inputEl.value = ""; - updateOptions(); - inputEl.value = acobj._title; + updateInputField(); autocompleteEl.classList.add("display-none"); }