Add Search bar
This commit is contained in:
@@ -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
@@ -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>`;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user