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:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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>`;
|
||||
|
||||
Reference in New Issue
Block a user