Improve DocSearch loading

1. Refactor JavaScript from search.html into a new DocSearch Component Module. The component needs to include: - Asynchronous loading of Algolia docsearch.min.js - Proper Algolia search facets:

  - Nested arrays for AND conditions: In Algolia, filters in the same array are combined with OR, while arrays in an array are combined with AND
  - Space after colon: Algolia's parser expects this specific format
2. Update Search HTML Template: refactor the search.html to use the new component:

3. Register the Component in main.js: update main.js to import and register our new DocSearch component:

Improve SearchInteractions event handler coordination with DocSearch:

- Improve SearchInteractions to dynamically detect the dropdown added by Algolia DocSearch and ensure that blur and focus event handlers are registered after the dropdown loads.

Benefits of This Approach:

Asynchronous Loading: DocSearch.js is now loaded asynchronously, improving page load performance
Search functionality is encapsulated as a component
Clean Separation: HTML template only contains configuration data, not implementation
Event Communication: Components can react to DocSearch initialization via events
SearchInteractions dynamically detects dropdown: Instead of waiting for an event or using setTimeout, we actively observe DOM changes to detect when the dropdown is created.
Nested Observation: A second observer monitors changes to the dropdown itself, allowing code to respond to style changes.
Better Event Handling: The blur handler checks if the related target is inside the search components, preventing unwanted dropdown closing.
Resource Cleanup: The cleanup function disconnects all observers, preventing memory leaks.
Additional Interaction Control: Adds an event listener to the dropdown to prevent unwanted blur events when interacting with search results.
pull/6079/head
Jason Stirnaman 2025-05-13 14:43:20 -05:00
parent d92d253242
commit 4bbf1d7094
4 changed files with 269 additions and 88 deletions

View File

@ -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 +
` <span class=\"search-product-version\">${product} ${version}</span>`;
hit._highlightResult.hierarchy.lvl0.value =
hit._highlightResult.hierarchy.lvl0.value +
` <span class=\"search-product-version\">${product} ${version}</span>`;
});
return hits;
},
algoliaOptions: {
hitsPerPage: 10,
facetFilters: buildFacetFilters(config),
},
autocompleteOptions: {
templates: {
header:
'<div class="search-all-content"><a href="https:\/\/support.influxdata.com" target="_blank">Search all InfluxData content <span class="icon-arrow-up-right"></span></a>',
empty:
'<div class="search-no-results"><p>Not finding what you\'re looking for?</p> <a href="https:\/\/support.influxdata.com" target="_blank">Search all InfluxData content <span class="icon-arrow-up-right"></span></a></div>',
},
},
});
// 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');
};
}

View File

@ -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,

View File

@ -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();
}
};
}

View File

@ -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) }}
<script src="https://cdn.jsdelivr.net/npm/docsearch.js@2/dist/cdn/docsearch.min.js"></script>
<script>
var multiVersion = ['influxdb']
docsearch({
apiKey: '501434b53a46a92a7931aecc7c9672e2',
appId: 'WHM9UWMP6M',
indexName: 'influxdata',
inputSelector: '#algolia-search-input',
// Set debug to true if you want to inspect the dropdown
debug: true,
transformData: function (hits) {
function fmtVersion (version, productKey) {
if (version == null) {
return '';
} else if (version === 'cloud') {
return 'Cloud (TSM)';
} else if (version === 'core') {
return 'Core';
} else if (version === 'enterprise') {
return 'Enterprise';
} else if (version === 'explorer') {
return 'Explorer';
} else if (version === 'cloud-serverless') {
return 'Cloud Serverless';
} else if (version === 'cloud-dedicated') {
return 'Cloud Dedicated';
} else if (version === 'clustered') {
return 'Clustered';
} else if (multiVersion.includes(productKey)) {
return version;
} else {
return '';
}
};
productNames = {
influxdb: 'InfluxDB',
influxdb3: 'InfluxDB 3',
enterprise_influxdb: 'InfluxDB Enterprise',
flux: 'Flux',
telegraf: 'Telegraf',
chronograf: 'Chronograf',
kapacitor: 'Kapacitor',
platform: 'InfluxData Platform',
resources: 'Additional Resources',
};
hits.map(hit => {
pathData = new URL(hit.url).pathname.split('/').filter(n => n);
product = productNames[pathData[0]];
version = fmtVersion(pathData[1], pathData[0]);
hit.product = product;
hit.version = version;
hit.hierarchy.lvl0 =
hit.hierarchy.lvl0 +
` <span class=\"search-product-version\">${product} ${version}</span>`;
hit._highlightResult.hierarchy.lvl0.value =
hit._highlightResult.hierarchy.lvl0.value +
` <span class=\"search-product-version\">${product} ${version}</span>`;
});
return hits;
},
algoliaOptions: {
hitsPerPage: 10,
'facetFilters': [
{{ if or (eq $product "platform") (eq $product "resources") (le (len $productPathData) 1) }}
'latest:true'
{{ else if $includeFlux }}
['searchTag: {{ $product }}-{{ $version }}', 'flux:true', 'resources:{{ $includeResources }}']
{{ else }}
['searchTag: {{ $product }}-{{ $version }}', 'resources:{{ $includeResources }}']
{{ end }}
]
},
autocompleteOptions: {
templates: {
header: '<div class="search-all-content"><a href="https:\/\/support.influxdata.com" target="_blank">Search all InfluxData content <span class="icon-arrow-up-right"></span></a>',
empty: '<div class="search-no-results"><p>Not finding what you\'re looking for?</p> <a href="https:\/\/support.influxdata.com" target="_blank">Search all InfluxData content <span class="icon-arrow-up-right"></span></a></div>'
}
}
});
</script>
<!-- DocSearch Component Container -->
<div
data-component="doc-search"
data-api-key="501434b53a46a92a7931aecc7c9672e2"
data-app-id="WHM9UWMP6M"
data-index-name="influxdata"
data-input-selector="#algolia-search-input"
data-search-tag="{{ $product }}-{{ $version }}"
data-include-flux="{{ $includeFlux }}"
data-include-resources="{{ $includeResources }}"
data-debug="{{ if hugo.IsProduction }}false{{ else }}true{{ end }}"
></div>