ui: replace select elements with custom dropdowns for better styling
- Replace HTML <select> elements with custom div-based dropdown components - Add CSS variable --ml-download-control-height for consistent control sizing - Update search source and category selectors to use custom styled dropdowns - Improve responsive layout with grid template columns and media queries - Add keyboard and mouse event handling for custom dropdowns - Ensure consistent styling across all form controls
This commit is contained in:
+52
-25
@@ -648,8 +648,9 @@
|
||||
|
||||
/* Download Section */
|
||||
.ml-download-section {
|
||||
--ml-download-control-height: 36px;
|
||||
display: grid;
|
||||
gap: var(--ml-spacing-8);
|
||||
gap: 10px;
|
||||
padding: var(--ml-spacing-10);
|
||||
background: var(--ml-surface-elevated);
|
||||
border-radius: 0;
|
||||
@@ -673,39 +674,64 @@
|
||||
margin-left: var(--ml-spacing-8);
|
||||
}
|
||||
.ml-search-source-bar {
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(178px, 1fr);
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
gap: 10px;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.ml-download-section .ml-search-source-bar > .ml-btn {
|
||||
height: var(--ml-download-control-height);
|
||||
min-height: var(--ml-download-control-height);
|
||||
padding: 0 12px;
|
||||
border-radius: 9px;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
}
|
||||
.ml-search-source-picker {
|
||||
display: flex;
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr);
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
margin-left: auto;
|
||||
width: 100%;
|
||||
}
|
||||
.ml-search-source-picker-label {
|
||||
color: var(--ml-text-muted);
|
||||
font-size: 11px;
|
||||
font-size: 10px;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.ml-search-source-select {
|
||||
min-height: 30px;
|
||||
padding: 5px 10px;
|
||||
width: 100%;
|
||||
height: var(--ml-download-control-height);
|
||||
min-height: var(--ml-download-control-height);
|
||||
padding: 0 11px;
|
||||
background: rgba(255,255,255,0.04);
|
||||
color: var(--ml-text);
|
||||
border: 1px solid var(--ml-border);
|
||||
border-radius: 10px;
|
||||
font-size: 11px;
|
||||
border-radius: 9px;
|
||||
font-size: 12px;
|
||||
line-height: 1;
|
||||
outline: none;
|
||||
appearance: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
.ml-search-source-select:focus {
|
||||
border-color: rgba(114,211,138,0.4);
|
||||
box-shadow: 0 0 0 3px rgba(114,211,138,0.12);
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.ml-search-source-bar {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.ml-download-section .ml-search-source-bar > .ml-btn {
|
||||
justify-self: start;
|
||||
}
|
||||
}
|
||||
.ml-search-results {
|
||||
margin-top: 8px;
|
||||
display: none;
|
||||
@@ -2260,7 +2286,7 @@
|
||||
}
|
||||
}
|
||||
.ml-download-target {
|
||||
margin-top: 10px;
|
||||
margin-top: 0;
|
||||
padding-top: 10px;
|
||||
border-top: 1px solid rgba(255,255,255,0.06);
|
||||
display: grid;
|
||||
@@ -2270,7 +2296,7 @@
|
||||
display: grid;
|
||||
grid-template-columns: minmax(120px, 33%) minmax(0, 1fr);
|
||||
grid-template-rows: auto auto;
|
||||
gap: 10px;
|
||||
gap: 8px 10px;
|
||||
align-items: start;
|
||||
}
|
||||
.ml-download-target-row {
|
||||
@@ -2297,15 +2323,16 @@
|
||||
.ml-download-target-select {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 40px;
|
||||
margin: 3px;
|
||||
padding: 8px 10px;
|
||||
height: var(--ml-download-control-height, 36px);
|
||||
min-height: var(--ml-download-control-height, 36px);
|
||||
margin: 0;
|
||||
padding: 0 11px;
|
||||
background: rgba(255,255,255,0.04);
|
||||
color: var(--ml-text);
|
||||
border: 1px solid var(--ml-border);
|
||||
border-radius: 10px;
|
||||
border-radius: 9px;
|
||||
font-size: 12px;
|
||||
line-height: 1.2;
|
||||
line-height: 1;
|
||||
outline: none;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -2313,6 +2340,10 @@
|
||||
appearance: none;
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
.ml-search-source-select {
|
||||
cursor: pointer;
|
||||
}
|
||||
.ml-download-target-input:focus,
|
||||
.ml-download-target-select:focus {
|
||||
@@ -2323,6 +2354,8 @@
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
}
|
||||
@media (max-width: 720px) {
|
||||
.ml-download-target-label {
|
||||
@@ -2357,9 +2390,3 @@
|
||||
.ml-download-target-option.is-highlighted {
|
||||
background: rgba(114,211,138,0.12);
|
||||
}
|
||||
.ml-search-source-select option,
|
||||
.ml-download-target-select option {
|
||||
background: var(--ml-panel-bg-strong);
|
||||
color: var(--ml-text);
|
||||
}
|
||||
|
||||
|
||||
+124
-31
@@ -389,7 +389,7 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
const state = this.getSearchState(missing);
|
||||
const selectEl = container.querySelector(`#search-source-select-${missing.node_id}-${missing.widget_index}`);
|
||||
if (selectEl) {
|
||||
selectEl.value = state.selectedSource;
|
||||
this.setDropdownValue(selectEl, state.selectedSource, this.getSearchSourceLabel(state.selectedSource));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,6 +402,16 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
this.syncSearchSourceUi(missing, container);
|
||||
}
|
||||
|
||||
getDropdownValue(el) {
|
||||
return el?.dataset?.value || el?.value || '';
|
||||
}
|
||||
|
||||
setDropdownValue(el, value, label = value) {
|
||||
if (!el) return;
|
||||
el.dataset.value = value || '';
|
||||
el.value = label || value || '';
|
||||
}
|
||||
|
||||
normalizeVersionName(versionName) {
|
||||
return String(versionName || '').trim().replace(/^v{2,}(?=\d)/i, 'v');
|
||||
}
|
||||
@@ -693,7 +703,10 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
renderSearchControls(missing, options = {}) {
|
||||
const searchSourcesId = `search-sources-${missing.node_id}-${missing.widget_index}`;
|
||||
const searchSourceSelectId = `search-source-select-${missing.node_id}-${missing.widget_index}`;
|
||||
const searchSourceListId = `search-source-list-${missing.node_id}-${missing.widget_index}`;
|
||||
const buttonText = options.buttonText || 'Search';
|
||||
const state = this.getSearchState(missing);
|
||||
const selectedSource = state.selectedSource || 'all';
|
||||
|
||||
let html = `<div id="${searchSourcesId}" class="ml-search-source-bar">`;
|
||||
html += `<button id="search-${missing.node_id}-${missing.widget_index}" class="ml-btn ml-btn-link">`;
|
||||
@@ -701,15 +714,10 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
html += `</button>`;
|
||||
html += `<div class="ml-search-source-picker">`;
|
||||
html += `<label class="ml-search-source-picker-label" for="${searchSourceSelectId}">Source</label>`;
|
||||
html += `<select id="${searchSourceSelectId}" class="ml-search-source-select">`;
|
||||
html += `<option value="all">Everything</option>`;
|
||||
html += `<option value="local">Local Database</option>`;
|
||||
html += `<option value="huggingface">HuggingFace</option>`;
|
||||
html += `<option value="civitai">CivitAI</option>`;
|
||||
if (this.isSourceAvailable('lora_manager_archive')) {
|
||||
html += `<option value="lora_manager_archive">LoRA Manager Archive</option>`;
|
||||
}
|
||||
html += `</select>`;
|
||||
html += `<div class="ml-download-target-wrap">`;
|
||||
html += `<input id="${searchSourceSelectId}" class="ml-download-target-input ml-search-source-select" type="text" readonly autocomplete="off" data-value="${this.escapeHtml(selectedSource)}" value="${this.escapeHtml(this.getSearchSourceLabel(selectedSource))}">`;
|
||||
html += `<div id="${searchSourceListId}" class="ml-download-target-list ml-search-source-list"></div>`;
|
||||
html += `</div>`;
|
||||
html += `</div>`;
|
||||
html += `</div>`;
|
||||
return html;
|
||||
@@ -2206,6 +2214,17 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
return ordered.length > 0 ? ordered : [preferred];
|
||||
}
|
||||
|
||||
getSearchSourceOptions() {
|
||||
const sources = ['all', 'local', 'huggingface', 'civitai'];
|
||||
if (this.isSourceAvailable('lora_manager_archive')) {
|
||||
sources.push('lora_manager_archive');
|
||||
}
|
||||
return sources.map(source => ({
|
||||
value: source,
|
||||
label: this.getSearchSourceLabel(source)
|
||||
}));
|
||||
}
|
||||
|
||||
getAvailableSubfolders(category = '') {
|
||||
return this.downloadSubfolders.get((category || '').toLowerCase()) || [];
|
||||
}
|
||||
@@ -2278,9 +2297,10 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
async applySuggestedCivitaiSubfolder(missing, categoryEl, subfolderEl) {
|
||||
if (!categoryEl || !subfolderEl || subfolderEl.value.trim()) return;
|
||||
|
||||
await this.ensureDownloadSubfoldersLoaded(categoryEl.value);
|
||||
const folders = this.getAvailableSubfolders(categoryEl.value);
|
||||
const suggestion = this.getSuggestedCivitaiSubfolder(missing, categoryEl.value, folders);
|
||||
const category = this.getDropdownValue(categoryEl);
|
||||
await this.ensureDownloadSubfoldersLoaded(category);
|
||||
const folders = this.getAvailableSubfolders(category);
|
||||
const suggestion = this.getSuggestedCivitaiSubfolder(missing, category, folders);
|
||||
if (suggestion) {
|
||||
subfolderEl.value = suggestion;
|
||||
}
|
||||
@@ -2321,16 +2341,18 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
renderDownloadTargetControls(missing, defaultCategory = 'checkpoints') {
|
||||
const selectId = `download-category-${missing.node_id}-${missing.widget_index}`;
|
||||
const subfolderId = `download-subfolder-${missing.node_id}-${missing.widget_index}`;
|
||||
const categoryListId = `download-category-list-${missing.node_id}-${missing.widget_index}`;
|
||||
const subfolderListId = `download-subfolder-list-${missing.node_id}-${missing.widget_index}`;
|
||||
const options = this.getDownloadCategoryOptions(defaultCategory)
|
||||
.map(category => `<option value="${category}" ${category === defaultCategory ? 'selected' : ''}>${this.getCategoryDisplayName(category)}</option>`)
|
||||
.join('');
|
||||
const selectedCategory = defaultCategory || 'checkpoints';
|
||||
|
||||
let html = `<div class="ml-download-target">`;
|
||||
html += `<div class="ml-download-target-grid">`;
|
||||
html += `<label class="ml-download-target-label" for="${selectId}">Folder</label>`;
|
||||
html += `<label class="ml-download-target-label" for="${subfolderId}">Subfolder (optional)</label>`;
|
||||
html += `<select id="${selectId}" class="ml-download-target-select">${options}</select>`;
|
||||
html += `<div class="ml-download-target-wrap">`;
|
||||
html += `<input id="${selectId}" class="ml-download-target-input ml-download-target-select" type="text" readonly autocomplete="off" data-value="${this.escapeHtml(selectedCategory)}" value="${this.escapeHtml(this.getCategoryDisplayName(selectedCategory))}">`;
|
||||
html += `<div id="${categoryListId}" class="ml-download-target-list"></div>`;
|
||||
html += `</div>`;
|
||||
html += `<div class="ml-download-target-wrap">`;
|
||||
html += `<input id="${subfolderId}" class="ml-download-target-input" type="text" placeholder="e.g. ponyxl\\styles" autocomplete="off">`;
|
||||
html += `<div id="${subfolderListId}" class="ml-download-target-list"></div>`;
|
||||
@@ -2344,7 +2366,7 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
const categoryEl = this.contentElement?.querySelector(`#download-category-${missing.node_id}-${missing.widget_index}`);
|
||||
const subfolderEl = this.contentElement?.querySelector(`#download-subfolder-${missing.node_id}-${missing.widget_index}`);
|
||||
return {
|
||||
category: categoryEl?.value || fallbackCategory || 'checkpoints',
|
||||
category: this.getDropdownValue(categoryEl) || fallbackCategory || 'checkpoints',
|
||||
subfolder: (subfolderEl?.value || '').trim()
|
||||
};
|
||||
}
|
||||
@@ -2379,21 +2401,34 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
wireDownloadTargetAutocomplete(container, missing) {
|
||||
const categoryEl = container.querySelector(`#download-category-${missing.node_id}-${missing.widget_index}`);
|
||||
const subfolderEl = container.querySelector(`#download-subfolder-${missing.node_id}-${missing.widget_index}`);
|
||||
const categoryListEl = container.querySelector(`#download-category-list-${missing.node_id}-${missing.widget_index}`);
|
||||
const listEl = container.querySelector(`#download-subfolder-list-${missing.node_id}-${missing.widget_index}`);
|
||||
if (!categoryEl || !subfolderEl || !listEl) return;
|
||||
|
||||
this.enableWheelScrollChaining(listEl);
|
||||
if (categoryListEl) {
|
||||
this.enableWheelScrollChaining(categoryListEl);
|
||||
}
|
||||
|
||||
const renderOptions = (targetEl, values, onSelect) => {
|
||||
if (!values.length) {
|
||||
const options = values.map(value => (
|
||||
typeof value === 'object'
|
||||
? value
|
||||
: { value, label: value }
|
||||
));
|
||||
if (!options.length) {
|
||||
targetEl.innerHTML = '';
|
||||
targetEl.style.display = 'none';
|
||||
return;
|
||||
}
|
||||
|
||||
targetEl.innerHTML = values
|
||||
targetEl.innerHTML = options
|
||||
.slice(0, 50)
|
||||
.map(value => `<div class="ml-download-target-option" data-value="${encodeURIComponent(value)}">${value}</div>`)
|
||||
.map(option => {
|
||||
const value = String(option.value || '');
|
||||
const label = String(option.label || value);
|
||||
return `<div class="ml-download-target-option" data-value="${encodeURIComponent(value)}" data-label="${encodeURIComponent(label)}">${this.escapeHtml(label)}</div>`;
|
||||
})
|
||||
.join('');
|
||||
|
||||
targetEl.style.display = 'block';
|
||||
@@ -2402,16 +2437,33 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
option.addEventListener('mousedown', (event) => {
|
||||
event.preventDefault();
|
||||
const value = decodeURIComponent(option.dataset.value || '');
|
||||
onSelect(value);
|
||||
const label = decodeURIComponent(option.dataset.label || option.dataset.value || '');
|
||||
onSelect(value, label);
|
||||
targetEl.style.display = 'none';
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const populateCategoryOptions = () => {
|
||||
if (!categoryListEl) return;
|
||||
const options = this.getDownloadCategoryOptions(this.getDropdownValue(categoryEl) || 'checkpoints')
|
||||
.map(category => ({
|
||||
value: category,
|
||||
label: this.getCategoryDisplayName(category)
|
||||
}));
|
||||
renderOptions(categoryListEl, options, (value, label) => {
|
||||
this.setDropdownValue(categoryEl, value, label);
|
||||
subfolderEl.value = '';
|
||||
populateSubfolderOptions('');
|
||||
this.applySuggestedCivitaiSubfolder(missing, categoryEl, subfolderEl);
|
||||
});
|
||||
};
|
||||
|
||||
const populateSubfolderOptions = async (filterText = '') => {
|
||||
const filter = (filterText || '').toLowerCase();
|
||||
await this.ensureDownloadSubfoldersLoaded(categoryEl.value);
|
||||
const folders = this.getAvailableSubfolders(categoryEl.value);
|
||||
const category = this.getDropdownValue(categoryEl);
|
||||
await this.ensureDownloadSubfoldersLoaded(category);
|
||||
const folders = this.getAvailableSubfolders(category);
|
||||
const filtered = filter
|
||||
? folders.filter(folder => folder.toLowerCase().includes(filter))
|
||||
: folders;
|
||||
@@ -2427,10 +2479,18 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
}, 150);
|
||||
};
|
||||
|
||||
categoryEl.addEventListener('change', () => {
|
||||
populateSubfolderOptions(subfolderEl.value);
|
||||
this.applySuggestedCivitaiSubfolder(missing, categoryEl, subfolderEl);
|
||||
});
|
||||
if (categoryListEl && categoryEl.dataset.mlCategoryBound !== 'true') {
|
||||
categoryEl.dataset.mlCategoryBound = 'true';
|
||||
categoryEl.addEventListener('focus', () => populateCategoryOptions());
|
||||
categoryEl.addEventListener('click', () => populateCategoryOptions());
|
||||
categoryEl.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
populateCategoryOptions();
|
||||
}
|
||||
});
|
||||
categoryEl.addEventListener('blur', () => hideList(categoryListEl));
|
||||
}
|
||||
|
||||
subfolderEl.addEventListener('focus', () => {
|
||||
populateSubfolderOptions(subfolderEl.value);
|
||||
@@ -5194,11 +5254,44 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
}
|
||||
|
||||
const sourceSelect = container.querySelector(`#search-source-select-${missing.node_id}-${missing.widget_index}`);
|
||||
if (sourceSelect && sourceSelect.dataset.mlSearchSourceBound !== 'true') {
|
||||
const sourceList = container.querySelector(`#search-source-list-${missing.node_id}-${missing.widget_index}`);
|
||||
if (sourceSelect && sourceList && sourceSelect.dataset.mlSearchSourceBound !== 'true') {
|
||||
sourceSelect.dataset.mlSearchSourceBound = 'true';
|
||||
sourceSelect.addEventListener('change', () => {
|
||||
this.setSearchSource(missing, sourceSelect.value, container);
|
||||
|
||||
const renderSourceOptions = () => {
|
||||
const options = this.getSearchSourceOptions();
|
||||
sourceList.innerHTML = options
|
||||
.map(option => `<div class="ml-download-target-option" data-value="${encodeURIComponent(option.value)}" data-label="${encodeURIComponent(option.label)}">${this.escapeHtml(option.label)}</div>`)
|
||||
.join('');
|
||||
sourceList.style.display = 'block';
|
||||
sourceList.querySelectorAll('.ml-download-target-option').forEach(optionEl => {
|
||||
optionEl.addEventListener('mousedown', (event) => {
|
||||
event.preventDefault();
|
||||
const value = decodeURIComponent(optionEl.dataset.value || '');
|
||||
const label = decodeURIComponent(optionEl.dataset.label || optionEl.dataset.value || '');
|
||||
this.setDropdownValue(sourceSelect, value, label);
|
||||
this.setSearchSource(missing, value, container);
|
||||
sourceList.style.display = 'none';
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const hideSourceList = () => {
|
||||
setTimeout(() => {
|
||||
sourceList.style.display = 'none';
|
||||
}, 150);
|
||||
};
|
||||
|
||||
this.enableWheelScrollChaining(sourceList);
|
||||
sourceSelect.addEventListener('focus', () => renderSourceOptions());
|
||||
sourceSelect.addEventListener('click', () => renderSourceOptions());
|
||||
sourceSelect.addEventListener('keydown', (event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowDown') {
|
||||
event.preventDefault();
|
||||
renderSourceOptions();
|
||||
}
|
||||
});
|
||||
sourceSelect.addEventListener('blur', hideSourceList);
|
||||
this.syncSearchSourceUi(missing, container);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user