From c76c129e43349550be0b147029b051783c95f0e6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 16:35:50 +0000 Subject: [PATCH 1/3] Initial plan From 484d820693835cfe828242fc260dca818837a078 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 16:43:02 +0000 Subject: [PATCH 2/3] Add search functionality to clusters page for searching within tabs Co-authored-by: LukasWallrich <60155545+LukasWallrich@users.noreply.github.com> --- layouts/partials/custom_js.html | 6 + static/js/clusters-search.js | 359 ++++++++++++++++++++++++++++++++ 2 files changed, 365 insertions(+) create mode 100644 layouts/partials/custom_js.html create mode 100644 static/js/clusters-search.js diff --git a/layouts/partials/custom_js.html b/layouts/partials/custom_js.html new file mode 100644 index 0000000000..6483386262 --- /dev/null +++ b/layouts/partials/custom_js.html @@ -0,0 +1,6 @@ +{{/* Custom JavaScript for FORRT site */}} + +{{/* Include clusters search functionality on clusters page */}} +{{ if eq .RelPermalink "/clusters/" }} + +{{ end }} diff --git a/static/js/clusters-search.js b/static/js/clusters-search.js new file mode 100644 index 0000000000..205109829c --- /dev/null +++ b/static/js/clusters-search.js @@ -0,0 +1,359 @@ +/** + * Clusters Page Search Functionality + * Enables searching within Bootstrap tab content that would otherwise be hidden from Ctrl-F + */ +(function() { + 'use strict'; + + // Wait for DOM to be ready + if (document.readyState === 'loading') { + document.addEventListener('DOMContentLoaded', init); + } else { + init(); + } + + function init() { + // Only run on clusters page + if (!window.location.pathname.includes('/clusters')) { + return; + } + + createSearchInterface(); + setupSearchHandlers(); + } + + 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 = ` +
+
+
+
+ +
+ + +
+
+
+
+
+
+ `; + + // Insert after intro section + introSection.parentNode.insertBefore(searchContainer, introSection.nextSibling); + } + + function setupSearchHandlers() { + const searchInput = document.getElementById('clusterSearchInput'); + const searchBtn = document.getElementById('clusterSearchBtn'); + const clearBtn = document.getElementById('clusterClearBtn'); + const resultsDiv = document.getElementById('clusterSearchResults'); + + if (!searchInput || !searchBtn || !clearBtn) return; + + // Search on button click + searchBtn.addEventListener('click', performSearch); + + // Search on Enter key + searchInput.addEventListener('keypress', function(e) { + if (e.key === 'Enter') { + performSearch(); + } + }); + + // Clear search + clearBtn.addEventListener('click', function() { + searchInput.value = ''; + resultsDiv.innerHTML = ''; + clearBtn.style.display = 'none'; + removeAllHighlights(); + collapseAllTabs(); + }); + } + + function performSearch() { + const searchInput = document.getElementById('clusterSearchInput'); + const clearBtn = document.getElementById('clusterClearBtn'); + const resultsDiv = document.getElementById('clusterSearchResults'); + const query = searchInput.value.trim(); + + if (!query || query.length < 2) { + resultsDiv.innerHTML = '
Please enter at least 2 characters to search.
'; + return; + } + + // Remove previous highlights + removeAllHighlights(); + + // Search through all tab content + const results = searchAllTabs(query); + + // Display results + displayResults(results, query); + + // Show clear button + clearBtn.style.display = 'inline-block'; + } + + function searchAllTabs(query) { + const results = []; + const queryLower = query.toLowerCase(); + + // Find all cluster sections + const clusterSections = document.querySelectorAll('section[id^="cluster"]'); + + clusterSections.forEach(function(section) { + const clusterTitle = section.querySelector('h3, h2, .home-section-title'); + const clusterName = clusterTitle ? clusterTitle.textContent.trim() : 'Unknown Cluster'; + + // Find all tab panes in this cluster + const tabPanes = section.querySelectorAll('.tab-pane'); + + tabPanes.forEach(function(tabPane) { + const tabId = tabPane.id; + const content = tabPane.textContent || tabPane.innerText; + const contentLower = content.toLowerCase(); + + // Check if query is in content + if (contentLower.includes(queryLower)) { + // Count occurrences + const matches = countMatches(contentLower, queryLower); + + // Get tab label + const tabLink = section.querySelector(`a[href="#${tabId}"]`); + const tabLabel = tabLink ? tabLink.textContent.trim() : tabId; + + // Get a snippet of context + const snippet = getContextSnippet(content, query); + + results.push({ + cluster: clusterName, + tab: tabLabel, + tabId: tabId, + matches: matches, + snippet: snippet, + section: section, + tabPane: tabPane, + tabLink: tabLink + }); + } + }); + }); + + return results; + } + + function countMatches(text, query) { + const regex = new RegExp(query, 'gi'); + const matches = text.match(regex); + return matches ? matches.length : 0; + } + + function getContextSnippet(text, query) { + const queryLower = query.toLowerCase(); + const textLower = text.toLowerCase(); + const index = textLower.indexOf(queryLower); + + if (index === -1) return ''; + + const start = Math.max(0, index - 50); + const end = Math.min(text.length, index + query.length + 100); + let snippet = text.substring(start, end); + + if (start > 0) snippet = '...' + snippet; + if (end < text.length) snippet = snippet + '...'; + + // Highlight the query in snippet + const regex = new RegExp(`(${escapeRegExp(query)})`, 'gi'); + snippet = snippet.replace(regex, '$1'); + + return snippet; + } + + function escapeRegExp(string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); + } + + function displayResults(results, query) { + const resultsDiv = document.getElementById('clusterSearchResults'); + + if (results.length === 0) { + resultsDiv.innerHTML = `
No results found for "${escapeHtml(query)}".
`; + return; + } + + let html = `
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:
`; + html += '
'; + + results.forEach(function(result) { + html += ` + +
+
${escapeHtml(result.cluster)} → ${escapeHtml(result.tab)}
+ ${result.matches} match${result.matches > 1 ? 'es' : ''} +
+

${result.snippet}

+
+ `; + }); + + html += '
'; + 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 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' }); + } + }); + }); + + // Auto-expand first result + if (results.length > 0) { + activateTab(results[0]); + highlightMatches(results[0].tabPane, query); + } + } + + function escapeHtml(text) { + const div = document.createElement('div'); + div.textContent = text; + return div.innerHTML; + } + + function activateTab(result) { + // Collapse all tabs first + collapseAllTabs(); + + // Activate the target tab + if (result.tabLink) { + result.tabLink.click(); + } + } + + function collapseAllTabs() { + document.querySelectorAll('.tab-pane.show.active').forEach(function(pane) { + pane.classList.remove('show', 'active'); + }); + document.querySelectorAll('.nav-link.active').forEach(function(link) { + link.classList.remove('active'); + }); + } + + function highlightMatches(tabPane, query) { + if (!tabPane) return; + + // Remove existing highlights first + removeHighlightsInElement(tabPane); + + // Use mark.js-like approach to highlight matches + const regex = new RegExp(`(${escapeRegExp(query)})`, 'gi'); + + // Get all text nodes + const walker = document.createTreeWalker( + tabPane, + NodeFilter.SHOW_TEXT, + null, + false + ); + + const textNodes = []; + let node; + while (node = walker.nextNode()) { + // Skip if parent is already a mark + if (node.parentElement.tagName !== 'MARK') { + textNodes.push(node); + } + } + + textNodes.forEach(function(textNode) { + const text = textNode.textContent; + if (regex.test(text)) { + const span = document.createElement('span'); + span.innerHTML = text.replace(regex, '$1'); + textNode.parentNode.replaceChild(span, textNode); + } + }); + } + + function removeAllHighlights() { + document.querySelectorAll('.cluster-highlight').forEach(function(mark) { + const parent = mark.parentNode; + parent.replaceChild(document.createTextNode(mark.textContent), mark); + parent.normalize(); + }); + + // Also remove wrapper spans + document.querySelectorAll('.tab-pane span').forEach(function(span) { + if (span.childNodes.length === 1 && span.childNodes[0].nodeType === Node.TEXT_NODE) { + const text = span.textContent; + if (span.parentNode && !span.classList.length) { + span.parentNode.replaceChild(document.createTextNode(text), span); + } + } + }); + } + + function removeHighlightsInElement(element) { + element.querySelectorAll('.cluster-highlight').forEach(function(mark) { + const parent = mark.parentNode; + parent.replaceChild(document.createTextNode(mark.textContent), mark); + parent.normalize(); + }); + } + + // Add CSS for highlighting + const style = document.createElement('style'); + style.textContent = ` + .cluster-highlight { + background-color: #ffeb3b; + padding: 2px 0; + font-weight: bold; + } + + .cluster-search-container { + position: sticky; + top: 70px; + background: white; + z-index: 100; + padding: 20px 0 10px 0; + box-shadow: 0 2px 4px rgba(0,0,0,0.1); + } + + .cluster-search-result:hover { + cursor: pointer; + } + + .cluster-search-result mark { + background-color: #ffeb3b; + padding: 1px 2px; + } + `; + document.head.appendChild(style); + +})(); From 5abda06455983784fdb03693dfb7883ecdafccd8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 4 Feb 2026 16:44:52 +0000 Subject: [PATCH 3/3] Fix code review issues: improve regex handling and DOM manipulation Co-authored-by: LukasWallrich <60155545+LukasWallrich@users.noreply.github.com> --- static/js/clusters-search.js | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/static/js/clusters-search.js b/static/js/clusters-search.js index 205109829c..291be7ab28 100644 --- a/static/js/clusters-search.js +++ b/static/js/clusters-search.js @@ -284,7 +284,7 @@ const textNodes = []; let node; - while (node = walker.nextNode()) { + while ((node = walker.nextNode())) { // Skip if parent is already a mark if (node.parentElement.tagName !== 'MARK') { textNodes.push(node); @@ -293,10 +293,15 @@ textNodes.forEach(function(textNode) { const text = textNode.textContent; - if (regex.test(text)) { - const span = document.createElement('span'); - span.innerHTML = text.replace(regex, '$1'); - textNode.parentNode.replaceChild(span, textNode); + const testRegex = new RegExp(`(${escapeRegExp(query)})`, 'gi'); + if (testRegex.test(text)) { + const fragment = document.createDocumentFragment(); + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = text.replace(regex, '$1'); + while (tempDiv.firstChild) { + fragment.appendChild(tempDiv.firstChild); + } + textNode.parentNode.replaceChild(fragment, textNode); } }); } @@ -307,16 +312,6 @@ parent.replaceChild(document.createTextNode(mark.textContent), mark); parent.normalize(); }); - - // Also remove wrapper spans - document.querySelectorAll('.tab-pane span').forEach(function(span) { - if (span.childNodes.length === 1 && span.childNodes[0].nodeType === Node.TEXT_NODE) { - const text = span.textContent; - if (span.parentNode && !span.classList.length) { - span.parentNode.replaceChild(document.createTextNode(text), span); - } - } - }); } function removeHighlightsInElement(element) {