mirror of
https://github.com/willmiao/ComfyUI-Lora-Manager.git
synced 2026-03-21 21:22:11 -03:00
feat(settings): add search functionality to settings modal (P2)
Implement Phase 2 search bar feature for settings modal: - Add search input to settings modal header with icon and clear button - Implement real-time filtering with 150ms debounce for performance - Add visual highlighting for matched search terms using accent color - Implement empty search results state with user-friendly message - Add keyboard shortcuts (Escape to clear search) - Auto-expand sections containing matching content during search - Fix header layout to prevent overlap with close button - Update progress tracker documenting P2 completion - Add translation keys for search feature (placeholder, clear, no results) - Sync translations across all language files Files changed: - templates/components/modals/settings_modal.html - static/css/components/modal/settings-modal.css - static/js/managers/SettingsManager.js - locales/*.json (10 language files) - docs/ui-ux-optimization/progress-tracker.md
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
## Project Overview
|
||||
**Goal**: Optimize Settings Modal UI/UX with left navigation sidebar
|
||||
**Started**: 2026-02-23
|
||||
**Current Phase**: P0 - Left Navigation Sidebar
|
||||
**Current Phase**: P2 - Search Bar (Completed)
|
||||
|
||||
---
|
||||
|
||||
@@ -74,25 +74,42 @@ None currently
|
||||
|
||||
## Phase 1: Section Collapse/Expand (P1)
|
||||
|
||||
### Status: Planned
|
||||
### Status: Completed ✓
|
||||
|
||||
### Completion Notes
|
||||
- All sections now have collapse/expand functionality
|
||||
- Chevron icon rotates smoothly on toggle
|
||||
- State persistence via localStorage working correctly
|
||||
- CSS animations for smooth height transitions
|
||||
- Settings order reorganized to match sidebar navigation
|
||||
|
||||
### Tasks
|
||||
- [ ] Add collapse/expand toggle to section headers
|
||||
- [ ] Add chevron icon with rotation animation
|
||||
- [ ] Implement localStorage for state persistence
|
||||
- [ ] Add CSS animations for smooth transitions
|
||||
- [x] Add collapse/expand toggle to section headers
|
||||
- [x] Add chevron icon with rotation animation
|
||||
- [x] Implement localStorage for state persistence
|
||||
- [x] Add CSS animations for smooth transitions
|
||||
- [x] Reorder settings sections to match sidebar navigation
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: Search Bar (P1)
|
||||
|
||||
### Status: Planned
|
||||
### Status: Completed ✓
|
||||
|
||||
### Completion Notes
|
||||
- Search input added to settings modal header with icon and clear button
|
||||
- Real-time filtering with debounced input (150ms delay)
|
||||
- Highlight matching terms with accent color background
|
||||
- Handle empty search results with user-friendly message
|
||||
- Keyboard shortcuts: Escape to clear search
|
||||
- Sections with matches are automatically expanded
|
||||
- All translation keys added and synchronized across languages
|
||||
|
||||
### Tasks
|
||||
- [ ] Add search input to header area
|
||||
- [ ] Implement real-time filtering
|
||||
- [ ] Add highlight for matched terms
|
||||
- [ ] Handle empty search results
|
||||
- [x] Add search input to header area
|
||||
- [x] Implement real-time filtering
|
||||
- [x] Add highlight for matched terms
|
||||
- [x] Handle empty search results
|
||||
|
||||
---
|
||||
|
||||
@@ -121,7 +138,29 @@ None currently
|
||||
|
||||
## Change Log
|
||||
|
||||
### 2026-02-23
|
||||
### 2026-02-23 (P2)
|
||||
- Completed Phase 2: Search Bar
|
||||
- Added search input to settings modal header with search icon and clear button
|
||||
- Implemented real-time filtering with 150ms debounce for performance
|
||||
- Added visual highlighting for matched search terms using accent color
|
||||
- Implemented empty search results state with user-friendly message
|
||||
- Added keyboard shortcuts (Escape to clear search)
|
||||
- Sections with matching content are automatically expanded during search
|
||||
- Updated SettingsManager.js with search initialization and filtering logic
|
||||
- Added comprehensive CSS styles for search input, highlights, and responsive design
|
||||
- Added translation keys for search feature (placeholder, clear, no results)
|
||||
- Synchronized translations across all language files
|
||||
|
||||
### 2026-02-23 (P1)
|
||||
- Completed Phase 1: Section Collapse/Expand
|
||||
- Added collapse/expand functionality to all settings sections
|
||||
- Implemented chevron icon with smooth rotation animation
|
||||
- Added localStorage persistence for collapse state
|
||||
- Reorganized settings sections to match sidebar navigation order
|
||||
- Updated SettingsManager.js with section collapse initialization
|
||||
- Added CSS styles for smooth transitions and animations
|
||||
|
||||
### 2026-02-23 (P0)
|
||||
- Created project documentation
|
||||
- Started Phase 0 implementation
|
||||
- Analyzed existing code structure
|
||||
|
||||
@@ -275,6 +275,11 @@
|
||||
"download": "[TODO: Translate] Download",
|
||||
"advanced": "[TODO: Translate] Advanced"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "[TODO: Translate] Search settings...",
|
||||
"clear": "[TODO: Translate] Clear search",
|
||||
"noResults": "[TODO: Translate] No settings found matching \"{query}\""
|
||||
},
|
||||
"storage": {
|
||||
"locationLabel": "Portabler Modus",
|
||||
"locationHelp": "Aktiviere, um settings.json im Repository zu belassen; deaktiviere, um es im Benutzerkonfigurationsordner zu speichern."
|
||||
|
||||
@@ -275,6 +275,11 @@
|
||||
"download": "Download",
|
||||
"advanced": "Advanced"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Search settings...",
|
||||
"clear": "Clear search",
|
||||
"noResults": "No settings found matching \"{query}\""
|
||||
},
|
||||
"storage": {
|
||||
"locationLabel": "Portable mode",
|
||||
"locationHelp": "Enable to keep settings.json inside the repository; disable to store it in your user config directory."
|
||||
|
||||
@@ -275,6 +275,11 @@
|
||||
"download": "[TODO: Translate] Download",
|
||||
"advanced": "[TODO: Translate] Advanced"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "[TODO: Translate] Search settings...",
|
||||
"clear": "[TODO: Translate] Clear search",
|
||||
"noResults": "[TODO: Translate] No settings found matching \"{query}\""
|
||||
},
|
||||
"storage": {
|
||||
"locationLabel": "Modo portátil",
|
||||
"locationHelp": "Activa para mantener settings.json dentro del repositorio; desactívalo para guardarlo en tu directorio de configuración de usuario."
|
||||
|
||||
@@ -275,6 +275,11 @@
|
||||
"download": "[TODO: Translate] Download",
|
||||
"advanced": "[TODO: Translate] Advanced"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "[TODO: Translate] Search settings...",
|
||||
"clear": "[TODO: Translate] Clear search",
|
||||
"noResults": "[TODO: Translate] No settings found matching \"{query}\""
|
||||
},
|
||||
"storage": {
|
||||
"locationLabel": "Mode portable",
|
||||
"locationHelp": "Activez pour garder settings.json dans le dépôt ; désactivez pour le placer dans votre dossier de configuration utilisateur."
|
||||
|
||||
@@ -275,6 +275,11 @@
|
||||
"download": "[TODO: Translate] Download",
|
||||
"advanced": "[TODO: Translate] Advanced"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "[TODO: Translate] Search settings...",
|
||||
"clear": "[TODO: Translate] Clear search",
|
||||
"noResults": "[TODO: Translate] No settings found matching \"{query}\""
|
||||
},
|
||||
"storage": {
|
||||
"locationLabel": "מצב נייד",
|
||||
"locationHelp": "הפעל כדי לשמור את settings.json בתוך המאגר; בטל כדי לשמור אותו בתיקיית ההגדרות של המשתמש."
|
||||
|
||||
@@ -275,6 +275,11 @@
|
||||
"download": "[TODO: Translate] Download",
|
||||
"advanced": "[TODO: Translate] Advanced"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "[TODO: Translate] Search settings...",
|
||||
"clear": "[TODO: Translate] Clear search",
|
||||
"noResults": "[TODO: Translate] No settings found matching \"{query}\""
|
||||
},
|
||||
"storage": {
|
||||
"locationLabel": "ポータブルモード",
|
||||
"locationHelp": "有効にすると settings.json をリポジトリ内に保持し、無効にするとユーザー設定ディレクトリに格納します。"
|
||||
|
||||
@@ -275,6 +275,11 @@
|
||||
"download": "[TODO: Translate] Download",
|
||||
"advanced": "[TODO: Translate] Advanced"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "[TODO: Translate] Search settings...",
|
||||
"clear": "[TODO: Translate] Clear search",
|
||||
"noResults": "[TODO: Translate] No settings found matching \"{query}\""
|
||||
},
|
||||
"storage": {
|
||||
"locationLabel": "휴대용 모드",
|
||||
"locationHelp": "활성화하면 settings.json을 리포지토리에 유지하고, 비활성화하면 사용자 구성 디렉터리에 저장합니다."
|
||||
|
||||
@@ -275,6 +275,11 @@
|
||||
"download": "[TODO: Translate] Download",
|
||||
"advanced": "[TODO: Translate] Advanced"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "[TODO: Translate] Search settings...",
|
||||
"clear": "[TODO: Translate] Clear search",
|
||||
"noResults": "[TODO: Translate] No settings found matching \"{query}\""
|
||||
},
|
||||
"storage": {
|
||||
"locationLabel": "Портативный режим",
|
||||
"locationHelp": "Включите, чтобы хранить settings.json в репозитории; выключите, чтобы сохранить его в папке конфигурации пользователя."
|
||||
|
||||
@@ -275,6 +275,11 @@
|
||||
"download": "[TODO: Translate] Download",
|
||||
"advanced": "[TODO: Translate] Advanced"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "[TODO: Translate] Search settings...",
|
||||
"clear": "[TODO: Translate] Clear search",
|
||||
"noResults": "[TODO: Translate] No settings found matching \"{query}\""
|
||||
},
|
||||
"storage": {
|
||||
"locationLabel": "便携模式",
|
||||
"locationHelp": "开启可将 settings.json 保存在仓库中;关闭则保存在用户配置目录。"
|
||||
|
||||
@@ -275,6 +275,11 @@
|
||||
"download": "[TODO: Translate] Download",
|
||||
"advanced": "[TODO: Translate] Advanced"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "[TODO: Translate] Search settings...",
|
||||
"clear": "[TODO: Translate] Clear search",
|
||||
"noResults": "[TODO: Translate] No settings found matching \"{query}\""
|
||||
},
|
||||
"storage": {
|
||||
"locationLabel": "可攜式模式",
|
||||
"locationHelp": "啟用可將 settings.json 保存在儲存庫中;停用則保存在使用者設定目錄。"
|
||||
|
||||
@@ -120,9 +120,115 @@
|
||||
.settings-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-start;
|
||||
gap: var(--space-1);
|
||||
margin-bottom: var(--space-2);
|
||||
padding-right: 40px; /* Space for close button */
|
||||
}
|
||||
|
||||
.settings-header .settings-search-wrapper {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
/* Search Input Styles */
|
||||
.settings-search-wrapper {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 240px;
|
||||
}
|
||||
|
||||
.settings-search-icon {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
color: var(--text-color);
|
||||
opacity: 0.5;
|
||||
font-size: 0.9em;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.settings-search-input {
|
||||
width: 100%;
|
||||
padding: 6px 28px 6px 32px;
|
||||
height: 32px;
|
||||
border-radius: var(--border-radius-xs);
|
||||
border: 1px solid var(--border-color);
|
||||
background-color: var(--lora-surface);
|
||||
color: var(--text-color);
|
||||
font-size: 0.9em;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.settings-search-input:focus {
|
||||
border-color: var(--lora-accent);
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 2px rgba(var(--lora-accent-rgb, 79, 70, 229), 0.1);
|
||||
}
|
||||
|
||||
.settings-search-input::placeholder {
|
||||
color: var(--text-color);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.settings-search-clear {
|
||||
position: absolute;
|
||||
right: 6px;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: none;
|
||||
background: rgba(var(--border-color-rgb, 148, 163, 184), 0.3);
|
||||
color: var(--text-color);
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 0.7em;
|
||||
opacity: 0.6;
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
.settings-search-clear:hover {
|
||||
opacity: 1;
|
||||
background: rgba(var(--border-color-rgb, 148, 163, 184), 0.5);
|
||||
}
|
||||
|
||||
/* Search Highlight Styles */
|
||||
.settings-search-highlight {
|
||||
background-color: rgba(var(--lora-accent-rgb, 79, 70, 229), 0.3);
|
||||
color: var(--lora-accent);
|
||||
padding: 0 2px;
|
||||
border-radius: 2px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Section visibility during search */
|
||||
.settings-section.search-match,
|
||||
.setting-item.search-match {
|
||||
display: block !important;
|
||||
}
|
||||
|
||||
.settings-section.search-hidden,
|
||||
.setting-item.search-hidden {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* Empty search results state */
|
||||
.settings-search-empty {
|
||||
text-align: center;
|
||||
padding: var(--space-4);
|
||||
color: var(--text-color);
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.settings-search-empty i {
|
||||
font-size: 2em;
|
||||
margin-bottom: var(--space-2);
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.settings-search-empty p {
|
||||
margin: 0;
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
.settings-header h2 {
|
||||
@@ -461,13 +567,66 @@
|
||||
padding-top: var(--space-2);
|
||||
}
|
||||
|
||||
.settings-section h3 {
|
||||
font-size: 1.1em;
|
||||
.settings-section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
padding: var(--space-1) 0;
|
||||
margin-bottom: var(--space-2);
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.settings-section-header:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.settings-section-header h3 {
|
||||
font-size: 1.1em;
|
||||
margin: 0;
|
||||
color: var(--text-color);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.settings-section-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
transition: transform 0.3s ease;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.settings-section-toggle:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.settings-section-toggle .chevron {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.settings-section.collapsed .settings-section-toggle .chevron {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
|
||||
.settings-section-content {
|
||||
overflow: hidden;
|
||||
transition: max-height 0.3s ease, opacity 0.3s ease;
|
||||
max-height: 2000px;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.settings-section.collapsed .settings-section-content {
|
||||
max-height: 0;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.setting-item {
|
||||
display: flex;
|
||||
flex-direction: column; /* Changed to column for help text placement */
|
||||
@@ -806,6 +965,17 @@ input:checked + .toggle-slider:before {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.settings-header {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
|
||||
.settings-header .settings-search-wrapper {
|
||||
margin-left: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.settings-nav {
|
||||
width: 100%;
|
||||
max-height: 200px;
|
||||
|
||||
@@ -365,6 +365,7 @@ export class SettingsManager {
|
||||
|
||||
this.setupPriorityTagInputs();
|
||||
this.initializeNavigation();
|
||||
this.initializeSearch();
|
||||
|
||||
this.initialized = true;
|
||||
}
|
||||
@@ -435,10 +436,266 @@ export class SettingsManager {
|
||||
}
|
||||
});
|
||||
|
||||
// Initialize section collapse/expand
|
||||
this.initializeSectionCollapse();
|
||||
|
||||
// Initial update
|
||||
updateActiveNav();
|
||||
}
|
||||
|
||||
initializeSectionCollapse() {
|
||||
const sections = document.querySelectorAll('.settings-section, .setting-item[id^="section-"]');
|
||||
const STORAGE_KEY = 'settingsModal_collapsedSections';
|
||||
|
||||
// Load collapsed state from localStorage
|
||||
let collapsedSections = {};
|
||||
try {
|
||||
const stored = localStorage.getItem(STORAGE_KEY);
|
||||
if (stored) {
|
||||
collapsedSections = JSON.parse(stored);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to load collapsed sections state:', e);
|
||||
}
|
||||
|
||||
sections.forEach(section => {
|
||||
const sectionId = section.getAttribute('data-section') || section.id;
|
||||
const header = section.querySelector('.settings-section-header');
|
||||
const toggleBtn = section.querySelector('.settings-section-toggle');
|
||||
|
||||
if (!header || !toggleBtn) return;
|
||||
|
||||
// Apply initial collapsed state
|
||||
if (collapsedSections[sectionId]) {
|
||||
section.classList.add('collapsed');
|
||||
}
|
||||
|
||||
// Handle toggle click
|
||||
const toggleSection = () => {
|
||||
const isCollapsed = section.classList.toggle('collapsed');
|
||||
|
||||
// Save state to localStorage
|
||||
collapsedSections[sectionId] = isCollapsed;
|
||||
try {
|
||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(collapsedSections));
|
||||
} catch (e) {
|
||||
console.warn('Failed to save collapsed sections state:', e);
|
||||
}
|
||||
};
|
||||
|
||||
// Click on header or toggle button
|
||||
header.addEventListener('click', (e) => {
|
||||
// Don't toggle if clicking on interactive elements within header
|
||||
if (e.target.closest('a, button:not(.settings-section-toggle), input, select')) {
|
||||
return;
|
||||
}
|
||||
toggleSection();
|
||||
});
|
||||
|
||||
toggleBtn.addEventListener('click', (e) => {
|
||||
e.stopPropagation();
|
||||
toggleSection();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
initializeSearch() {
|
||||
const searchInput = document.getElementById('settingsSearchInput');
|
||||
const searchClear = document.getElementById('settingsSearchClear');
|
||||
|
||||
if (!searchInput) return;
|
||||
|
||||
// Debounced search handler
|
||||
let searchTimeout;
|
||||
const debouncedSearch = (query) => {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => {
|
||||
this.performSearch(query);
|
||||
}, 150);
|
||||
};
|
||||
|
||||
// Handle input changes
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
const query = e.target.value.trim();
|
||||
|
||||
// Show/hide clear button
|
||||
if (searchClear) {
|
||||
searchClear.style.display = query ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
debouncedSearch(query);
|
||||
});
|
||||
|
||||
// Handle clear button click
|
||||
if (searchClear) {
|
||||
searchClear.addEventListener('click', () => {
|
||||
searchInput.value = '';
|
||||
searchClear.style.display = 'none';
|
||||
searchInput.focus();
|
||||
this.performSearch('');
|
||||
});
|
||||
}
|
||||
|
||||
// Handle Escape key to clear search
|
||||
searchInput.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Escape') {
|
||||
if (searchInput.value) {
|
||||
searchInput.value = '';
|
||||
if (searchClear) searchClear.style.display = 'none';
|
||||
this.performSearch('');
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
performSearch(query) {
|
||||
const sections = document.querySelectorAll('.settings-section, .setting-item[id^="section-"]');
|
||||
const settingsForm = document.querySelector('.settings-form');
|
||||
|
||||
// Remove existing empty state
|
||||
const existingEmptyState = settingsForm?.querySelector('.settings-search-empty');
|
||||
if (existingEmptyState) {
|
||||
existingEmptyState.remove();
|
||||
}
|
||||
|
||||
if (!query) {
|
||||
// Reset all sections to visible and remove highlights
|
||||
sections.forEach(section => {
|
||||
section.classList.remove('search-hidden', 'search-match');
|
||||
this.removeSearchHighlights(section);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const lowerQuery = query.toLowerCase();
|
||||
let matchCount = 0;
|
||||
|
||||
sections.forEach(section => {
|
||||
const sectionText = this.getSectionSearchableText(section);
|
||||
const hasMatch = sectionText.includes(lowerQuery);
|
||||
|
||||
if (hasMatch) {
|
||||
section.classList.remove('search-hidden');
|
||||
section.classList.add('search-match');
|
||||
this.highlightSearchMatches(section, lowerQuery);
|
||||
matchCount++;
|
||||
|
||||
// Expand section if it has matches
|
||||
section.classList.remove('collapsed');
|
||||
} else {
|
||||
section.classList.add('search-hidden');
|
||||
section.classList.remove('search-match');
|
||||
this.removeSearchHighlights(section);
|
||||
}
|
||||
});
|
||||
|
||||
// Show empty state if no matches found
|
||||
if (matchCount === 0 && settingsForm) {
|
||||
const emptyState = document.createElement('div');
|
||||
emptyState.className = 'settings-search-empty';
|
||||
emptyState.innerHTML = `
|
||||
<i class="fas fa-search"></i>
|
||||
<p>${translate('settings.search.noResults', { query }, `No settings found matching "${query}"`)}</p>
|
||||
`;
|
||||
settingsForm.appendChild(emptyState);
|
||||
}
|
||||
}
|
||||
|
||||
getSectionSearchableText(section) {
|
||||
// Get all text content from labels, help text, and headers
|
||||
const labels = section.querySelectorAll('label');
|
||||
const helpTexts = section.querySelectorAll('.input-help');
|
||||
const headers = section.querySelectorAll('h3');
|
||||
|
||||
let text = '';
|
||||
|
||||
labels.forEach(el => text += ' ' + el.textContent);
|
||||
helpTexts.forEach(el => text += ' ' + el.textContent);
|
||||
headers.forEach(el => text += ' ' + el.textContent);
|
||||
|
||||
return text.toLowerCase();
|
||||
}
|
||||
|
||||
highlightSearchMatches(section, query) {
|
||||
// Remove existing highlights first
|
||||
this.removeSearchHighlights(section);
|
||||
|
||||
if (!query) return;
|
||||
|
||||
// Highlight in labels and help text
|
||||
const textElements = section.querySelectorAll('label, .input-help, h3');
|
||||
|
||||
textElements.forEach(element => {
|
||||
const walker = document.createTreeWalker(
|
||||
element,
|
||||
NodeFilter.SHOW_TEXT,
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
const textNodes = [];
|
||||
let node;
|
||||
while (node = walker.nextNode()) {
|
||||
if (node.textContent.toLowerCase().includes(query)) {
|
||||
textNodes.push(node);
|
||||
}
|
||||
}
|
||||
|
||||
textNodes.forEach(textNode => {
|
||||
const parent = textNode.parentElement;
|
||||
const text = textNode.textContent;
|
||||
const lowerText = text.toLowerCase();
|
||||
|
||||
// Split text by query and wrap matches in highlight spans
|
||||
const parts = [];
|
||||
let lastIndex = 0;
|
||||
let index;
|
||||
|
||||
while ((index = lowerText.indexOf(query, lastIndex)) !== -1) {
|
||||
// Add text before match
|
||||
if (index > lastIndex) {
|
||||
parts.push(document.createTextNode(text.substring(lastIndex, index)));
|
||||
}
|
||||
|
||||
// Add highlighted match
|
||||
const highlight = document.createElement('span');
|
||||
highlight.className = 'settings-search-highlight';
|
||||
highlight.textContent = text.substring(index, index + query.length);
|
||||
parts.push(highlight);
|
||||
|
||||
lastIndex = index + query.length;
|
||||
}
|
||||
|
||||
// Add remaining text
|
||||
if (lastIndex < text.length) {
|
||||
parts.push(document.createTextNode(text.substring(lastIndex)));
|
||||
}
|
||||
|
||||
// Replace original text node with highlighted version
|
||||
if (parts.length > 1) {
|
||||
parts.forEach(part => parent.insertBefore(part, textNode));
|
||||
parent.removeChild(textNode);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
removeSearchHighlights(section) {
|
||||
const highlights = section.querySelectorAll('.settings-search-highlight');
|
||||
|
||||
highlights.forEach(highlight => {
|
||||
const parent = highlight.parentElement;
|
||||
if (parent) {
|
||||
// Replace highlight with its text content
|
||||
parent.insertBefore(document.createTextNode(highlight.textContent), highlight);
|
||||
parent.removeChild(highlight);
|
||||
|
||||
// Normalize to merge adjacent text nodes
|
||||
parent.normalize();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async openSettingsFileLocation() {
|
||||
try {
|
||||
const response = await fetch('/api/lm/settings/open-location', {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user