From f33a849bc9243b2709cba5cc929bf4a24ba5ddd5 Mon Sep 17 00:00:00 2001 From: Alexopus Date: Mon, 21 Jul 2025 15:33:16 +0200 Subject: [PATCH] Add Search bar --- CLAUDE.md | 21 ++++++-- js/extension.js | 124 +++++++++++++++++++++++++++++++++++++++++++ js/notes-sidebar.css | 109 +++++++++++++++++++++++++++++++++++++ 3 files changed, 250 insertions(+), 4 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b3b11fd..94ab0f1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -66,11 +66,22 @@ The extension exposes these REST endpoints: - Content persistence across sidebar open/close cycles - Real-time tab rendering on note operations - Button layout: Save/Rename/LoRA submenu on left, Refresh/Delete on right +- **Search Functionality**: Live search through note content with highlighted results and line navigation - **Unsaved Changes Tracking**: Visual indicators (*) for modified notes - **Content Preservation**: Tab switching preserves unsaved content - **Change Detection**: Automatic tracking of modifications vs saved state - **Append Integration**: Submenu with three formatting options for appending LoRA lists (extensible for other content types) +### Search Functionality +- **Search Input**: Located below button container with "Search lines..." placeholder +- **Live Search**: Results update as you type, case-insensitive matching +- **Split View**: Original textarea remains fully editable while search results appear below +- **Result Navigation**: Click any search result to jump to that line in the main editor +- **Highlighting**: Matching text highlighted with yellow background in search results +- **Clear Button**: PrimeVue `pi-times-circle` icon for quick search clearing +- **Auto-Update**: Search results refresh when switching between notes +- **HTML Safety**: Proper escaping prevents HTML injection in search results + ### Unsaved Changes Behavior - Notes with modifications show asterisk (*) in tab name - Switching tabs preserves unsaved content in inactive notes @@ -99,8 +110,9 @@ No automated tests defined. Manual testing workflow: 2. Verify sidebar tab appears with "Notes" label and book icon 3. Test CRUD operations: create, edit, save, rename, delete notes 4. Verify tab switching and content persistence -5. Test LoRA integration: verify submenu appears on click with three format options -6. Verify unsaved changes tracking with asterisk (*) indicators +5. Test search functionality: enter search terms, verify results highlighting and line navigation +6. Test LoRA integration: verify submenu appears on click with three format options +7. Verify unsaved changes tracking with asterisk (*) indicators ### Development Workflow 1. Modify Python code in `__init__.py` for backend changes @@ -115,17 +127,18 @@ No automated tests defined. Manual testing workflow: - **UI state changes**: Modify `NotesStore` class in `js/stores/NotesStore.js` - **Append functionality changes**: Update `js/components/AppendMenu.js` and related backend endpoints - **Adding new components**: Create new files in `js/components/` and import them in `extension.js` +- **Search functionality changes**: Modify `onSearchChange()` and related methods in `extension.js` - **API changes**: Update `js/api/notesApi.js` static methods and error handling ### File Structure - `notes/` - Storage directory for markdown notes (auto-created) - `js/` - Frontend assets loaded by ComfyUI - - `extension.js` - Main extension entry point (~450 lines) + - `extension.js` - Main extension entry point (~580 lines, includes search functionality) - `stores/NotesStore.js` - State management (~80 lines) - `components/AppendMenu.js` - Append functionality (~170 lines) - `api/notesApi.js` - API service layer (~90 lines) - `utils/validation.js` - Validation utilities (~10 lines) - - `notes-sidebar.css` - UI styling + - `notes-sidebar.css` - UI styling (includes search component styles) - `__init__.py` - Main extension code - `pyproject.toml` - Project metadata diff --git a/js/extension.js b/js/extension.js index cbd413f..89f1e6b 100644 --- a/js/extension.js +++ b/js/extension.js @@ -20,6 +20,8 @@ class NotesSidebar { this.renameBtn = null; this.deleteBtn = null; this.appendMenu = null; + this.searchInput = null; + this.searchResults = null; this.isInitialized = false; this.sidebarElement = null; @@ -47,6 +49,11 @@ class NotesSidebar { if (updates.activeNote || updates.unsavedChanges) { this.updateButtonState(); } + + // Update search results if active note changed and search is active + if (updates.activeNote && this.searchInput && this.searchInput.value.trim()) { + this.onSearchChange(); + } } get notes() { return this.store.state.notes; } @@ -123,6 +130,7 @@ class NotesSidebar { this.createTabsContainer(this.sidebarElement); this.createNoteButtonContainer(this.sidebarElement); + this.createSearchContainer(this.sidebarElement); this.createContentArea(this.sidebarElement); el.appendChild(this.sidebarElement); @@ -208,6 +216,36 @@ class NotesSidebar { parent.appendChild(buttonContainer); } + createSearchContainer(parent) { + const searchContainer = document.createElement('div'); + searchContainer.className = 'notes-search-container'; + + const inputWrapper = document.createElement('div'); + inputWrapper.className = 'notes-search-input-wrapper'; + + this.searchInput = document.createElement('input'); + this.searchInput.type = 'text'; + this.searchInput.className = 'notes-search-input'; + this.searchInput.placeholder = 'Search lines...'; + this.searchInput.addEventListener('input', () => this.onSearchChange()); + + const clearButton = document.createElement('button'); + clearButton.className = 'notes-search-clear'; + clearButton.innerHTML = ''; + clearButton.title = 'Clear search'; + clearButton.addEventListener('click', () => this.clearSearch()); + + this.searchResults = document.createElement('div'); + this.searchResults.className = 'notes-search-results'; + this.searchResults.style.display = 'none'; + + inputWrapper.appendChild(this.searchInput); + inputWrapper.appendChild(clearButton); + searchContainer.appendChild(inputWrapper); + searchContainer.appendChild(this.searchResults); + parent.appendChild(searchContainer); + } + createContentArea(parent) { this.contentArea = document.createElement('div'); this.contentArea.className = 'notes-content-area'; @@ -444,6 +482,92 @@ class NotesSidebar { // Check if content has changed from saved version const hasUnsavedChanges = currentContent !== savedContent; this.store.setUnsavedChanges(this.activeNote, hasUnsavedChanges); + + // Update search results if search is active + if (this.searchInput && this.searchInput.value.trim()) { + this.onSearchChange(); + } + } + + onSearchChange() { + const searchText = this.searchInput.value.trim(); + + if (!searchText) { + // Hide search results when search is empty + this.searchResults.style.display = 'none'; + return; + } + + if (!this.activeNote || !this.textArea.value) { + // No active note or content to search + this.searchResults.style.display = 'none'; + return; + } + + const content = this.textArea.value; + const lines = content.split('\n'); + const matchingLines = []; + + lines.forEach((line, index) => { + if (line.toLowerCase().includes(searchText.toLowerCase())) { + matchingLines.push({ + lineNumber: index + 1, + content: line + }); + } + }); + + if (matchingLines.length === 0) { + this.searchResults.innerHTML = '
No matching lines found
'; + } else { + this.searchResults.innerHTML = matchingLines.map(line => + `
+ ${line.lineNumber}: + ${this.highlightMatch(this.escapeHtml(line.content), searchText)} +
` + ).join(''); + } + + this.searchResults.style.display = 'block'; + } + + clearSearch() { + this.searchInput.value = ''; + this.searchResults.style.display = 'none'; + this.searchInput.focus(); + } + + getLineStartPosition(content, lineIndex) { + const lines = content.split('\n'); + let position = 0; + for (let i = 0; i < lineIndex; i++) { + position += lines[i].length + 1; // +1 for newline character + } + return position; + } + + getLineEndPosition(content, lineIndex) { + const lines = content.split('\n'); + let position = 0; + for (let i = 0; i <= lineIndex; i++) { + position += lines[i].length; + if (i < lineIndex) position += 1; // +1 for newline character + } + return position; + } + + escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + + highlightMatch(text, filterText) { + const escapedFilter = filterText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + const regex = new RegExp(escapedFilter, 'gi'); + return text.replace(regex, function(match) { + return `${match}`; + }); } } diff --git a/js/notes-sidebar.css b/js/notes-sidebar.css index a0887d3..7bdc370 100644 --- a/js/notes-sidebar.css +++ b/js/notes-sidebar.css @@ -186,4 +186,113 @@ .notes-submenu-item:hover { background: #444; +} + +/* Search Container */ +.notes-search-container { + padding: 10px; + background: #1a1a1a; + border-bottom: 1px solid #444; +} + +.notes-search-input-wrapper { + position: relative; + display: flex; + align-items: center; +} + +.notes-search-input { + width: 100%; + padding: 8px 35px 8px 12px; + background: #2a2a2a; + border: 1px solid #444; + border-radius: 4px; + color: #fff; + font-size: 14px; + box-sizing: border-box; +} + +.notes-search-clear { + position: absolute; + right: 8px; + background: transparent; + border: none; + color: #888; + cursor: pointer; + padding: 4px; + border-radius: 4px; + display: flex; + align-items: center; + justify-content: center; +} + +.notes-search-clear:hover { + color: #fff; + background: rgba(255, 255, 255, 0.1); +} + +.notes-search-input:focus { + outline: none; + border-color: #666; + background: #333; +} + +.notes-search-input::placeholder { + color: #888; +} + +/* Search Results */ +.notes-search-results { + max-height: 200px; + overflow-y: auto; + margin-top: 10px; + background: #2a2a2a; + border: 1px solid #444; + border-radius: 4px; +} + +.notes-search-result { + padding: 6px 10px; + border-bottom: 1px solid #444; + cursor: pointer; + display: flex; + align-items: flex-start; + gap: 8px; + font-size: 12px; + line-height: 1.4; +} + +.notes-search-result:last-child { + border-bottom: none; +} + +.notes-search-result:hover { + background: #444; +} + +.notes-search-line-number { + color: #888; + font-weight: bold; + flex-shrink: 0; + min-width: 30px; +} + +.notes-search-line-content { + color: #fff; + word-break: break-all; +} + +.notes-search-line-content mark { + background: #ffeb3b; + color: #000; + padding: 1px 2px; + border-radius: 2px; +} + +.notes-search-no-results { + padding: 15px; + text-align: center; + color: #888; + font-style: italic; + font-size: 12px; } \ No newline at end of file