Add Search bar

This commit is contained in:
Alexopus
2025-07-21 15:33:16 +02:00
parent d7d51b8419
commit f33a849bc9
3 changed files with 250 additions and 4 deletions
+17 -4
View File
@@ -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
+124
View File
@@ -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 = '<i class="pi pi-times-circle"></i>';
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 = '<div class="notes-search-no-results">No matching lines found</div>';
} else {
this.searchResults.innerHTML = matchingLines.map(line =>
`<div class="notes-search-result" onclick="this.parentNode.parentNode.parentNode.querySelector('.notes-textarea').focus(); this.parentNode.parentNode.parentNode.querySelector('.notes-textarea').setSelectionRange(${this.getLineStartPosition(content, line.lineNumber - 1)}, ${this.getLineEndPosition(content, line.lineNumber - 1)});">
<span class="notes-search-line-number">${line.lineNumber}:</span>
<span class="notes-search-line-content">${this.highlightMatch(this.escapeHtml(line.content), searchText)}</span>
</div>`
).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 `<mark>${match}</mark>`;
});
}
}
+109
View File
@@ -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;
}