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
parent
d92d253242
commit
4bbf1d7094
|
@ -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');
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
};
|
||||
}
|
|
@ -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>
|
||||
|
|
Loading…
Reference in New Issue