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 CodePlaceholder from './code-placeholders.js';
|
||||||
import { CustomTimeTrigger } from './custom-timestamps.js';
|
import { CustomTimeTrigger } from './custom-timestamps.js';
|
||||||
import Diagram from './components/diagram.js';
|
import Diagram from './components/diagram.js';
|
||||||
|
import DocSearch from './components/doc-search.js';
|
||||||
import FeatureCallout from './feature-callouts.js';
|
import FeatureCallout from './feature-callouts.js';
|
||||||
import FluxGroupKeysDemo from './flux-group-keys.js';
|
import FluxGroupKeysDemo from './flux-group-keys.js';
|
||||||
import FluxInfluxDBVersionsTrigger from './flux-influxdb-versions.js';
|
import FluxInfluxDBVersionsTrigger from './flux-influxdb-versions.js';
|
||||||
|
@ -62,6 +63,7 @@ const componentRegistry = {
|
||||||
'code-placeholder': CodePlaceholder,
|
'code-placeholder': CodePlaceholder,
|
||||||
'custom-time-trigger': CustomTimeTrigger,
|
'custom-time-trigger': CustomTimeTrigger,
|
||||||
'diagram': Diagram,
|
'diagram': Diagram,
|
||||||
|
'doc-search': DocSearch,
|
||||||
'feature-callout': FeatureCallout,
|
'feature-callout': FeatureCallout,
|
||||||
'flux-group-keys-demo': FluxGroupKeysDemo,
|
'flux-group-keys-demo': FluxGroupKeysDemo,
|
||||||
'flux-influxdb-versions-trigger': FluxInfluxDBVersionsTrigger,
|
'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 }) {
|
export default function SearchInteractions({ searchInput }) {
|
||||||
const contentWrapper = document.querySelector('.content-wrapper');
|
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
|
// Fade content wrapper when focusing on search input
|
||||||
searchInput.addEventListener('focus', () => {
|
function handleFocus() {
|
||||||
// Using CSS transitions instead of jQuery's fadeTo for better performance
|
|
||||||
contentWrapper.style.opacity = '0.35';
|
contentWrapper.style.opacity = '0.35';
|
||||||
contentWrapper.style.transition = 'opacity 300ms';
|
contentWrapper.style.transition = 'opacity 300ms';
|
||||||
});
|
}
|
||||||
|
|
||||||
// Hide search dropdown when leaving search input
|
// 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.opacity = '1';
|
||||||
contentWrapper.style.transition = 'opacity 200ms';
|
contentWrapper.style.transition = 'opacity 200ms';
|
||||||
|
|
||||||
// Hide dropdown menu
|
// Hide dropdown if it exists
|
||||||
if (dropdownMenu) {
|
if (dropdownMenu) {
|
||||||
dropdownMenu.style.display = 'none';
|
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) }}
|
{{ $includeFlux := and (in $fluxSupported $product) (in $influxdbFluxSupport $version) }}
|
||||||
{{ $includeResources := not (in (slice "cloud-serverless" "cloud-dedicated" "clustered" "core" "enterprise" "explorer") $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>
|
<!-- DocSearch Component Container -->
|
||||||
<script>
|
<div
|
||||||
var multiVersion = ['influxdb']
|
data-component="doc-search"
|
||||||
docsearch({
|
data-api-key="501434b53a46a92a7931aecc7c9672e2"
|
||||||
apiKey: '501434b53a46a92a7931aecc7c9672e2',
|
data-app-id="WHM9UWMP6M"
|
||||||
appId: 'WHM9UWMP6M',
|
data-index-name="influxdata"
|
||||||
indexName: 'influxdata',
|
data-input-selector="#algolia-search-input"
|
||||||
inputSelector: '#algolia-search-input',
|
data-search-tag="{{ $product }}-{{ $version }}"
|
||||||
// Set debug to true if you want to inspect the dropdown
|
data-include-flux="{{ $includeFlux }}"
|
||||||
debug: true,
|
data-include-resources="{{ $includeResources }}"
|
||||||
transformData: function (hits) {
|
data-debug="{{ if hugo.IsProduction }}false{{ else }}true{{ end }}"
|
||||||
function fmtVersion (version, productKey) {
|
></div>
|
||||||
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>
|
|
||||||
|
|
Loading…
Reference in New Issue