Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
296 changes: 245 additions & 51 deletions static/js/clusters-search.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,49 +23,76 @@
}

function createSearchInterface() {
// Find the intro section to insert search box after it
const introSection = document.querySelector('.wg-blank');
if (!introSection) return;

// Create search container
const searchContainer = document.createElement('div');
searchContainer.className = 'cluster-search-container';
searchContainer.innerHTML = `
<div class="container" style="margin-bottom: 30px; margin-top: 20px;">
<div class="row justify-content-center">
<div class="col-lg-8">
<div class="input-group">
<input type="text"
id="clusterSearchInput"
class="form-control"
placeholder="Search within all clusters and sub-categories..."
aria-label="Search clusters">
<div class="input-group-append">
<button class="btn btn-primary" type="button" id="clusterSearchBtn">
<i class="fas fa-search"></i> Search
</button>
<button class="btn btn-outline-secondary" type="button" id="clusterClearBtn" style="display: none;">
<i class="fas fa-times"></i> Clear
</button>
</div>
// Create sidebar container
const sidebar = document.createElement('div');
sidebar.id = 'clusterSearchSidebar';
sidebar.className = 'cluster-search-sidebar';
sidebar.innerHTML = `
<button class="cluster-search-toggle" id="clusterSearchToggle" aria-label="Toggle search panel">
<i class="fas fa-search"></i> Search Clusters
</button>
<div class="cluster-search-panel" id="clusterSearchPanel">
<div class="cluster-search-header">
<h4>Search Clusters</h4>
<button class="cluster-search-close" id="clusterSearchClose" aria-label="Close search">
<i class="fas fa-times"></i>
</button>
</div>
<div class="cluster-search-body">
<div class="input-group">
<input type="text"
id="clusterSearchInput"
class="form-control"
placeholder="Search clusters..."
aria-label="Search clusters">
<div class="input-group-append">
<button class="btn btn-primary btn-sm" type="button" id="clusterSearchBtn">
<i class="fas fa-search"></i>
</button>
</div>
<div id="clusterSearchResults" style="margin-top: 10px; font-size: 0.9rem;"></div>
</div>
<button class="btn btn-outline-secondary btn-sm btn-block mt-2" type="button" id="clusterClearBtn" style="display: none;">
<i class="fas fa-times"></i> Clear Results
</button>
<div id="clusterSearchResults" style="margin-top: 15px; font-size: 0.85rem;"></div>
</div>
</div>
`;

// Insert after intro section
introSection.parentNode.insertBefore(searchContainer, introSection.nextSibling);
// Insert at beginning of body
document.body.insertBefore(sidebar, document.body.firstChild);
}

function setupSearchHandlers() {
const searchInput = document.getElementById('clusterSearchInput');
const searchBtn = document.getElementById('clusterSearchBtn');
const clearBtn = document.getElementById('clusterClearBtn');
const resultsDiv = document.getElementById('clusterSearchResults');
const toggleBtn = document.getElementById('clusterSearchToggle');
const closeBtn = document.getElementById('clusterSearchClose');
const panel = document.getElementById('clusterSearchPanel');

if (!searchInput || !searchBtn || !clearBtn || !toggleBtn || !closeBtn || !panel) return;

// Toggle sidebar
toggleBtn.addEventListener('click', function() {
panel.classList.toggle('open');
if (panel.classList.contains('open')) {
searchInput.focus();
}
});

if (!searchInput || !searchBtn || !clearBtn) return;
// Close sidebar
closeBtn.addEventListener('click', function() {
panel.classList.remove('open');
});

// Close on escape key
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape' && panel.classList.contains('open')) {
panel.classList.remove('open');
}
});

// Search on button click
searchBtn.addEventListener('click', performSearch);
Expand Down Expand Up @@ -198,43 +225,46 @@
return;
}

let html = `<div class="alert alert-success">Found ${results.length} tab(s) containing "${escapeHtml(query)}" (${results.reduce((sum, r) => sum + r.matches, 0)} total matches). Click on a result to view it:</div>`;
html += '<div class="list-group" style="margin-top: 10px;">';
let html = `<div class="search-summary">Found ${results.length} tab(s) with ${results.reduce((sum, r) => sum + r.matches, 0)} matches</div>`;
html += '<div class="search-results-list">';

results.forEach(function(result) {
html += `
<a href="#" class="list-group-item list-group-item-action cluster-search-result"
data-tab-id="${result.tabId}">
<div class="d-flex w-100 justify-content-between">
<h6 class="mb-1">${escapeHtml(result.cluster)} → ${escapeHtml(result.tab)}</h6>
<small>${result.matches} match${result.matches > 1 ? 'es' : ''}</small>
<div class="search-result-item" data-tab-id="${result.tabId}">
<div class="search-result-header">
<strong>${escapeHtml(result.tab)}</strong>
<span class="badge badge-primary">${result.matches}</span>
</div>
<p class="mb-1"><small>${result.snippet}</small></p>
</a>
<div class="search-result-cluster">${escapeHtml(result.cluster)}</div>
<div class="search-result-snippet">${result.snippet}</div>
</div>
`;
});

html += '</div>';
resultsDiv.innerHTML = html;

// Add click handlers to results
const resultLinks = resultsDiv.querySelectorAll('.cluster-search-result');
resultLinks.forEach(function(link) {
link.addEventListener('click', function(e) {
e.preventDefault();
const resultItems = resultsDiv.querySelectorAll('.search-result-item');
resultItems.forEach(function(item) {
item.addEventListener('click', function() {
const tabId = this.getAttribute('data-tab-id');
const result = results.find(r => r.tabId === tabId);
if (result) {
activateTab(result);
highlightMatches(result.tabPane, query);
// Scroll to the section
result.section.scrollIntoView({ behavior: 'smooth', block: 'start' });
// Mark as active
resultItems.forEach(r => r.classList.remove('active'));
this.classList.add('active');
}
});
});

// Auto-expand first result
if (results.length > 0) {
resultItems[0].classList.add('active');
activateTab(results[0]);
highlightMatches(results[0].tabPane, query);
}
Expand Down Expand Up @@ -322,31 +352,195 @@
});
}

// Add CSS for highlighting
// Add CSS for sidebar and highlighting
const style = document.createElement('style');
style.textContent = `
/* Highlight styling */
.cluster-highlight {
background-color: #ffeb3b;
padding: 2px 0;
font-weight: bold;
}

.cluster-search-container {
position: sticky;
top: 70px;
/* Sidebar toggle button */
.cluster-search-toggle {
position: fixed;
left: 0;
top: 200px;
background: #007bff;
color: white;
border: none;
border-radius: 0 5px 5px 0;
padding: 12px 15px;
cursor: pointer;
z-index: 1000;
box-shadow: 2px 2px 5px rgba(0,0,0,0.2);
font-size: 14px;
transition: background 0.3s;
}

.cluster-search-toggle:hover {
background: #0056b3;
}

.cluster-search-toggle i {
margin-right: 5px;
}

/* Sidebar panel */
.cluster-search-panel {
position: fixed;
left: -350px;
top: 0;
width: 350px;
height: 100vh;
background: white;
z-index: 100;
padding: 20px 0 10px 0;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
box-shadow: 2px 0 10px rgba(0,0,0,0.1);
z-index: 1001;
transition: left 0.3s ease;
display: flex;
flex-direction: column;
overflow: hidden;
}

.cluster-search-panel.open {
left: 0;
}

/* Sidebar header */
.cluster-search-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 15px 20px;
border-bottom: 1px solid #dee2e6;
background: #f8f9fa;
}

.cluster-search-header h4 {
margin: 0;
font-size: 18px;
color: #333;
}

.cluster-search-result:hover {
.cluster-search-close {
background: none;
border: none;
font-size: 20px;
color: #666;
cursor: pointer;
padding: 5px;
line-height: 1;
}

.cluster-search-close:hover {
color: #333;
}

/* Sidebar body */
.cluster-search-body {
padding: 20px;
flex: 1;
overflow-y: auto;
}

/* Search results summary */
.search-summary {
background: #e7f3ff;
padding: 10px;
border-radius: 5px;
margin-bottom: 15px;
font-size: 0.9rem;
color: #004085;
}

.cluster-search-result mark {
/* Search results list */
.search-results-list {
display: flex;
flex-direction: column;
gap: 10px;
}

/* Individual search result */
.search-result-item {
background: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 5px;
padding: 12px;
cursor: pointer;
transition: all 0.2s;
}

.search-result-item:hover {
background: #e9ecef;
border-color: #007bff;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}

.search-result-item.active {
background: #e7f3ff;
border-color: #007bff;
box-shadow: inset 0 0 0 1px #007bff;
}

.search-result-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
}

.search-result-header strong {
font-size: 0.95rem;
color: #333;
flex: 1;
margin-right: 10px;
}

.search-result-header .badge {
font-size: 0.75rem;
}

.search-result-cluster {
font-size: 0.8rem;
color: #666;
margin-bottom: 8px;
}

.search-result-snippet {
font-size: 0.8rem;
color: #555;
line-height: 1.4;
}

.search-result-snippet mark {
background-color: #ffeb3b;
padding: 1px 2px;
font-weight: 600;
}

/* Mobile responsive */
@media (max-width: 768px) {
.cluster-search-panel {
width: 100%;
left: -100%;
}

.cluster-search-panel.open {
left: 0;
}

.cluster-search-toggle {
top: 150px;
font-size: 12px;
padding: 10px 12px;
}
}

/* Alert styling in sidebar */
.cluster-search-body .alert {
font-size: 0.85rem;
padding: 8px 12px;
}
`;
document.head.appendChild(style);
Expand Down