diff --git a/assets/js/components/doc-search.js b/assets/js/components/doc-search.js new file mode 100644 index 000000000..4722a7393 --- /dev/null +++ b/assets/js/components/doc-search.js @@ -0,0 +1,173 @@ +/** + * DocSearch component for InfluxData documentation + * Handles asynchronous loading and initialization of Algolia DocSearch + */ + +export default function DocSearch({ component }) { + // Store configuration from component data attributes + const config = { + apiKey: component.getAttribute('data-api-key'), + appId: component.getAttribute('data-app-id'), + indexName: component.getAttribute('data-index-name'), + inputSelector: component.getAttribute('data-input-selector'), + searchTag: component.getAttribute('data-search-tag'), + includeFlux: component.getAttribute('data-include-flux') === 'true', + includeResources: + component.getAttribute('data-include-resources') === 'true', + debug: component.getAttribute('data-debug') === 'true', + }; + + // Initialize global object to track DocSearch state + window.InfluxDocs = window.InfluxDocs || {}; + window.InfluxDocs.search = { + initialized: false, + options: config, + }; + + // Load DocSearch asynchronously + function loadDocSearch() { + console.log('Loading DocSearch script...'); + const script = document.createElement('script'); + script.src = + 'https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.js'; + script.async = true; + script.onload = initializeDocSearch; + document.body.appendChild(script); + } + + // Initialize DocSearch after script loads + function initializeDocSearch() { + console.log('Initializing DocSearch...'); + const multiVersion = ['influxdb']; + + // Use object-based lookups instead of conditionals for version and product names + // These can be replaced with data from productData in the future + + // Version display name mappings + const versionDisplayNames = { + cloud: 'Cloud (TSM)', + core: 'Core', + enterprise: 'Enterprise', + 'cloud-serverless': 'Cloud Serverless', + 'cloud-dedicated': 'Cloud Dedicated', + clustered: 'Clustered', + explorer: 'Explorer', + }; + + // Product display name mappings + const productDisplayNames = { + influxdb: 'InfluxDB', + influxdb3: 'InfluxDB 3', + explorer: 'InfluxDB 3 Explorer', + enterprise_influxdb: 'InfluxDB Enterprise', + flux: 'Flux', + telegraf: 'Telegraf', + chronograf: 'Chronograf', + kapacitor: 'Kapacitor', + platform: 'InfluxData Platform', + resources: 'Additional Resources', + }; + + // Initialize DocSearch with configuration + window.docsearch({ + apiKey: config.apiKey, + appId: config.appId, + indexName: config.indexName, + inputSelector: config.inputSelector, + debug: config.debug, + transformData: function (hits) { + // Format version using object lookup instead of if-else chain + function fmtVersion(version, productKey) { + if (version == null) { + return ''; + } else if (versionDisplayNames[version]) { + return versionDisplayNames[version]; + } else if (multiVersion.includes(productKey)) { + return version; + } else { + return ''; + } + } + + hits.map((hit) => { + const pathData = new URL(hit.url).pathname + .split('/') + .filter((n) => n); + const product = productDisplayNames[pathData[0]] || pathData[0]; + const version = fmtVersion(pathData[1], pathData[0]); + + hit.product = product; + hit.version = version; + hit.hierarchy.lvl0 = + hit.hierarchy.lvl0 + + ` ${product} ${version}`; + hit._highlightResult.hierarchy.lvl0.value = + hit._highlightResult.hierarchy.lvl0.value + + ` ${product} ${version}`; + }); + return hits; + }, + algoliaOptions: { + hitsPerPage: 10, + facetFilters: buildFacetFilters(config), + }, + autocompleteOptions: { + templates: { + header: + '
Search all InfluxData content ', + empty: + '

Not finding what you\'re looking for?

Search all InfluxData content
', + }, + }, + }); + + // Mark DocSearch as initialized + window.InfluxDocs.search.initialized = true; + + // Dispatch event for other components to know DocSearch is ready + window.dispatchEvent(new CustomEvent('docsearch-initialized')); + } + + /** + * Helper function to build facet filters based on config + * - Uses nested arrays for AND conditions + * - Includes space after colon in filter expressions + */ + function buildFacetFilters(config) { + if (!config.searchTag) { + return ['latest:true']; + } else if (config.includeFlux) { + // Return a nested array to match original template structure + // Note the space after each colon + return [ + [ + 'searchTag: ' + config.searchTag, + 'flux:true', + 'resources: ' + config.includeResources, + ], + ]; + } else { + // Return a nested array to match original template structure + // Note the space after each colon + return [ + [ + 'searchTag: ' + config.searchTag, + 'resources: ' + config.includeResources, + ], + ]; + } + } + + // Load DocSearch when page is idle or after a slight delay + if ('requestIdleCallback' in window) { + requestIdleCallback(loadDocSearch); + } else { + setTimeout(loadDocSearch, 500); + } + + // Return cleanup function + return function cleanup() { + // Clean up any event listeners if needed + console.log('DocSearch component cleanup'); + }; +} diff --git a/assets/js/main.js b/assets/js/main.js index 3578952dc..126189fff 100644 --- a/assets/js/main.js +++ b/assets/js/main.js @@ -34,6 +34,7 @@ import AskAITrigger from './ask-ai-trigger.js'; import CodePlaceholder from './code-placeholders.js'; import { CustomTimeTrigger } from './custom-timestamps.js'; import Diagram from './components/diagram.js'; +import DocSearch from './components/doc-search.js'; import FeatureCallout from './feature-callouts.js'; import FluxGroupKeysDemo from './flux-group-keys.js'; import FluxInfluxDBVersionsTrigger from './flux-influxdb-versions.js'; @@ -62,6 +63,7 @@ const componentRegistry = { 'code-placeholder': CodePlaceholder, 'custom-time-trigger': CustomTimeTrigger, 'diagram': Diagram, + 'doc-search': DocSearch, 'feature-callout': FeatureCallout, 'flux-group-keys-demo': FluxGroupKeysDemo, 'flux-influxdb-versions-trigger': FluxInfluxDBVersionsTrigger, diff --git a/assets/js/utils/search-interactions.js b/assets/js/utils/search-interactions.js index 83d217382..d83433667 100644 --- a/assets/js/utils/search-interactions.js +++ b/assets/js/utils/search-interactions.js @@ -1,22 +1,97 @@ +/** + * Manages search interactions for DocSearch integration + * Uses MutationObserver to watch for dropdown creation + */ export default function SearchInteractions({ searchInput }) { const contentWrapper = document.querySelector('.content-wrapper'); - const dropdownMenu = document.querySelector('.ds-dropdown-menu'); - + let observer = null; + let dropdownObserver = null; + let dropdownMenu = null; + // Fade content wrapper when focusing on search input - searchInput.addEventListener('focus', () => { - // Using CSS transitions instead of jQuery's fadeTo for better performance + function handleFocus() { contentWrapper.style.opacity = '0.35'; contentWrapper.style.transition = 'opacity 300ms'; - }); + } // Hide search dropdown when leaving search input - searchInput.addEventListener('blur', () => { + function handleBlur(event) { + // Only process blur if not clicking within dropdown + const relatedTarget = event.relatedTarget; + if (relatedTarget && ( + relatedTarget.closest('.algolia-autocomplete') || + relatedTarget.closest('.ds-dropdown-menu'))) { + return; + } + contentWrapper.style.opacity = '1'; contentWrapper.style.transition = 'opacity 200ms'; - // Hide dropdown menu + // Hide dropdown if it exists if (dropdownMenu) { dropdownMenu.style.display = 'none'; } + } + + // Add event listeners + searchInput.addEventListener('focus', handleFocus); + searchInput.addEventListener('blur', handleBlur); + + // Use MutationObserver to detect when dropdown is added to the DOM + observer = new MutationObserver((mutations) => { + for (const mutation of mutations) { + if (mutation.type === 'childList') { + const newDropdown = document.querySelector('.ds-dropdown-menu:not([data-monitored])'); + if (newDropdown) { + console.log('DocSearch dropdown detected'); + + // Save reference to dropdown + dropdownMenu = newDropdown; + newDropdown.setAttribute('data-monitored', 'true'); + + // Monitor dropdown removal/display changes + dropdownObserver = new MutationObserver((dropdownMutations) => { + for (const dropdownMutation of dropdownMutations) { + if (dropdownMutation.type === 'attributes' && + dropdownMutation.attributeName === 'style') { + console.log('Dropdown style changed:', dropdownMenu.style.display); + } + } + }); + + // Observe changes to dropdown attributes (like style) + dropdownObserver.observe(dropdownMenu, { + attributes: true, + attributeFilter: ['style'] + }); + + // Add event listeners to keep dropdown open when interacted with + dropdownMenu.addEventListener('mousedown', (e) => { + // Prevent blur on searchInput when clicking in dropdown + e.preventDefault(); + }); + } + } + } }); + + // Start observing the document body for dropdown creation + observer.observe(document.body, { + childList: true, + subtree: true + }); + + // Return cleanup function + return function cleanup() { + searchInput.removeEventListener('focus', handleFocus); + searchInput.removeEventListener('blur', handleBlur); + + if (observer) { + observer.disconnect(); + } + + if (dropdownObserver) { + dropdownObserver.disconnect(); + } + }; } \ No newline at end of file diff --git a/layouts/partials/footer/search.html b/layouts/partials/footer/search.html index c0405957f..f61459af5 100644 --- a/layouts/partials/footer/search.html +++ b/layouts/partials/footer/search.html @@ -7,84 +7,15 @@ {{ $includeFlux := and (in $fluxSupported $product) (in $influxdbFluxSupport $version) }} {{ $includeResources := not (in (slice "cloud-serverless" "cloud-dedicated" "clustered" "core" "enterprise" "explorer") $version) }} - - \ No newline at end of file + +