feat(web): add search/filter input support for base model and download target dropdowns

- Remove readonly from base model and download target category inputs to allow typing
- Add input event listeners that filter dropdown options in real-time
- Normalize base model tokens for fuzzy search matching
- Introduce getSearchBaseModelInputValue to resolve typed input to known options
- Add optional { sync } parameter to setSearchBaseModel to avoid redundant UI sync during input
- Show/hide dropdown lists based on filtered result count
This commit is contained in:
Dariusz L
2026-06-21 02:50:58 +02:00
parent 92be0d6213
commit 8ab567238e
3 changed files with 105 additions and 20 deletions
@@ -1968,17 +1968,31 @@ export const resolveDownloadMethods = {
if (baseSelect && baseList && baseSelect.dataset.mlSearchBaseBound !== 'true') {
baseSelect.dataset.mlSearchBaseBound = 'true';
const renderBaseOptions = () => {
const options = this.getKnownBaseModelOptions();
baseList.innerHTML = options
const renderBaseOptions = (filterText = '') => {
const filter = String(filterText || '').trim().toLowerCase();
const normalizedFilter = this.normalizeBaseModelToken?.(filter) || filter.replace(/[^a-z0-9]+/g, '');
const options = this.getKnownBaseModelOptions()
.map(option => {
const label = option.value === 'auto'
? this.getSearchBaseModelLabel('auto', missing)
: option.label;
return { ...option, label };
})
.filter(option => {
if (!filter) return true;
const searchText = `${option.value} ${option.label}`.toLowerCase();
const normalizedSearchText = this.normalizeBaseModelToken?.(searchText)
|| searchText.replace(/[^a-z0-9]+/g, '');
return searchText.includes(filter)
|| (normalizedFilter && normalizedSearchText.includes(normalizedFilter));
});
baseList.innerHTML = options
.map(option => {
const label = option.label;
return `<div class="mr-download-target-option" data-value="${encodeURIComponent(option.value)}" data-label="${encodeURIComponent(label)}">${this.escapeHtml(label)}</div>`;
})
.join('');
baseList.style.display = 'block';
baseList.style.display = options.length ? 'block' : 'none';
baseList.querySelectorAll('.mr-download-target-option').forEach(optionEl => {
optionEl.addEventListener('mousedown', (event) => {
event.preventDefault();
@@ -1998,15 +2012,25 @@ export const resolveDownloadMethods = {
};
this.enableWheelScrollChaining(baseList);
baseSelect.addEventListener('focus', () => renderBaseOptions());
baseSelect.addEventListener('click', () => renderBaseOptions());
baseSelect.addEventListener('focus', () => renderBaseOptions(''));
baseSelect.addEventListener('click', () => renderBaseOptions(''));
baseSelect.addEventListener('input', () => {
const value = this.getSearchBaseModelInputValue(baseSelect.value, missing);
baseSelect.dataset.value = value;
this.setSearchBaseModel(missing, value, container, { sync: false });
renderBaseOptions(baseSelect.value);
});
baseSelect.addEventListener('keydown', (event) => {
if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowDown') {
if (event.key === 'Enter' || event.key === 'ArrowDown') {
event.preventDefault();
renderBaseOptions();
renderBaseOptions(baseSelect.value);
}
});
baseSelect.addEventListener('blur', hideBaseList);
baseSelect.addEventListener('blur', () => {
const value = this.getDropdownValue(baseSelect);
this.setDropdownValue(baseSelect, value, this.getSearchBaseModelLabel(value, missing));
hideBaseList();
});
this.syncSearchSourceUi(missing, container);
}
+45 -8
View File
@@ -1447,7 +1447,7 @@ export const downloadTargetMethods = {
html += `<label class="mr-download-target-label" for="${selectId}">Folder</label>`;
html += `<label class="mr-download-target-label" for="${subfolderId}">Subfolder (optional)</label>`;
html += `<div class="mr-download-target-wrap">`;
html += `<input id="${selectId}" class="mr-download-target-input mr-download-target-select" type="text" readonly autocomplete="off" data-value="${this.escapeHtml(selectedCategory)}" value="${this.escapeHtml(this.getCategoryDisplayName(selectedCategory))}">`;
html += `<input id="${selectId}" class="mr-download-target-input mr-download-target-select" type="text" autocomplete="off" data-value="${this.escapeHtml(selectedCategory)}" value="${this.escapeHtml(this.getCategoryDisplayName(selectedCategory))}">`;
html += `<div id="${categoryListId}" class="mr-download-target-list"></div>`;
html += `</div>`;
html += `<div class="mr-download-target-wrap">`;
@@ -1559,13 +1559,22 @@ export const downloadTargetMethods = {
});
};
const populateCategoryOptions = () => {
const populateCategoryOptions = (filterText = '') => {
if (!categoryListEl) return;
const filter = String(filterText || '').trim().toLowerCase();
const normalizedFilter = filter.replace(/[^a-z0-9]+/g, '');
const options = this.getDownloadCategoryOptions(this.getDropdownValue(categoryEl) || 'checkpoints')
.map(category => ({
value: category,
label: this.getCategoryDisplayName(category)
}));
}))
.filter(option => {
if (!filter) return true;
const searchText = `${option.value} ${option.label}`.toLowerCase();
const normalizedSearchText = searchText.replace(/[^a-z0-9]+/g, '');
return searchText.includes(filter)
|| (normalizedFilter && normalizedSearchText.includes(normalizedFilter));
});
renderOptions(categoryListEl, options, (value, label) => {
this.setDropdownValue(categoryEl, value, label);
subfolderEl.value = '';
@@ -1614,15 +1623,43 @@ export const downloadTargetMethods = {
if (categoryListEl && categoryEl.dataset.mlCategoryBound !== 'true') {
categoryEl.dataset.mlCategoryBound = 'true';
categoryEl.addEventListener('focus', () => populateCategoryOptions());
categoryEl.addEventListener('click', () => populateCategoryOptions());
categoryEl.addEventListener('focus', () => populateCategoryOptions(''));
categoryEl.addEventListener('click', () => populateCategoryOptions(''));
categoryEl.addEventListener('input', () => {
const typed = categoryEl.value.trim();
const category = typed ? this.normalizeDownloadCategory(typed) : '';
const previousCategory = this.getDropdownValue(categoryEl);
categoryEl.dataset.value = category;
if (category !== previousCategory) {
subfolderEl.value = '';
subfolderEl.dataset.baseDirectory = '';
listEl.innerHTML = '';
listEl.style.display = 'none';
}
this.saveDownloadTargetSelection(missing, {
category,
subfolder: subfolderEl.value || '',
subfolderBaseDirectory: subfolderEl.dataset.baseDirectory || '',
categoryTouched: true,
subfolderTouched: Boolean(subfolderEl.value)
});
this.syncDownloadTargetFolderContext(categoryEl, subfolderEl);
populateCategoryOptions(typed);
});
categoryEl.addEventListener('keydown', (event) => {
if (event.key === 'Enter' || event.key === ' ' || event.key === 'ArrowDown') {
if (event.key === 'Enter' || event.key === 'ArrowDown') {
event.preventDefault();
populateCategoryOptions();
populateCategoryOptions(categoryEl.value);
}
});
categoryEl.addEventListener('blur', () => hideList(categoryListEl));
categoryEl.addEventListener('blur', () => {
const category = this.getDropdownValue(categoryEl);
const knownCategories = this.getKnownDownloadCategorySet();
if (category && knownCategories.has(this.normalizeDownloadCategory(category))) {
this.setDropdownValue(categoryEl, category, this.getCategoryDisplayName(category));
}
hideList(categoryListEl);
});
}
subfolderEl.addEventListener('focus', () => {
+27 -3
View File
@@ -408,6 +408,25 @@ export const searchPanelMethods = {
return option?.label || value || 'Auto';
},
getSearchBaseModelInputValue(text = '', missing = {}) {
const raw = String(text || '').trim();
if (!raw) return 'none';
const normalized = this.normalizeBaseModelToken(raw);
for (const option of this.getKnownBaseModelOptions()) {
const label = option.value === 'auto'
? this.getSearchBaseModelLabel('auto', missing)
: option.label;
const optionTokens = [
option.value,
label
].flatMap(value => [...this.getBaseModelTokenVariants(value)]);
if (optionTokens.some(token => token === normalized)) {
return option.value;
}
}
return this.resolveBaseModelAliasExact(raw) || raw;
},
getSearchBaseModelContext(missing = {}) {
const state = this.getSearchState(missing);
const selected = state.selectedBaseModel || this.getDefaultSearchBaseModel();
@@ -1116,7 +1135,8 @@ export const searchPanelMethods = {
this.syncSearchSourceUi(missing, container);
},
setSearchBaseModel(missing, baseModel, container) {
setSearchBaseModel(missing, baseModel, container, options = {}) {
const { sync = true } = options || {};
const state = this.getSearchState(missing);
const nextBaseModel = baseModel || this.getDefaultSearchBaseModel();
const changed = (state.selectedBaseModel || this.getDefaultSearchBaseModel()) !== nextBaseModel;
@@ -1143,7 +1163,11 @@ export const searchPanelMethods = {
}
state.selectedBaseModel = nextBaseModel;
this.persistSearchStateForActiveWorkflow();
this.syncSearchSourceUi(missing, container);
if (sync) {
this.syncSearchSourceUi(missing, container);
} else {
this.updateSearchBaseModelHelp(container, missing);
}
this.refreshSearchUiForMissing?.(missing, state);
},
@@ -1600,7 +1624,7 @@ export const searchPanelMethods = {
html += `<div class="mr-search-source-picker mr-search-base-picker">`;
html += `<label class="mr-search-source-picker-label" for="${searchBaseSelectId}">Model <span class="mr-tooltip-badge mr-search-model-help" data-tooltip="${this.escapeHtml(baseModelTooltip)}" tabindex="0">?</span></label>`;
html += `<div class="mr-download-target-wrap">`;
html += `<input id="${searchBaseSelectId}" class="mr-download-target-input mr-search-base-select" type="text" readonly autocomplete="off" data-value="${this.escapeHtml(selectedBaseModel)}" data-missing-search-key="${this.escapeHtml(this.getMissingSearchKey(missing))}" value="${this.escapeHtml(this.getSearchBaseModelLabel(selectedBaseModel, missing))}">`;
html += `<input id="${searchBaseSelectId}" class="mr-download-target-input mr-search-base-select" type="text" autocomplete="off" data-value="${this.escapeHtml(selectedBaseModel)}" data-missing-search-key="${this.escapeHtml(this.getMissingSearchKey(missing))}" value="${this.escapeHtml(this.getSearchBaseModelLabel(selectedBaseModel, missing))}">`;
html += `<div id="${searchBaseListId}" class="mr-download-target-list mr-search-base-list"></div>`;
html += `</div>`;
html += `</div>`;