Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
252 changes: 159 additions & 93 deletions src/theme/SearchBar/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useCallback, useMemo, useRef, useState } from 'react';
import React, { useCallback, useMemo, useRef, useState, useEffect } from 'react';
import { DocSearchButton, useDocSearchKeyboardEvents } from '@docsearch/react';
import Head from '@docusaurus/Head';
import { useHistory } from '@docusaurus/router';
Expand All @@ -9,7 +9,6 @@ import {
import useDocusaurusContext from '@docusaurus/useDocusaurusContext';
import { createPortal } from 'react-dom';
import translations from '@theme/SearchTranslations';
import { useEffect } from 'react';
import {useAskAI} from '@site/src/hooks/useAskAI'
import { shouldPreventSearchAction, handleSearchKeyboardConflict } from './utils/aiConflictHandler';
import { initializeSearchAnalytics, createEnhancedSearchClient } from './utils/searchAnalytics';
Expand All @@ -25,17 +24,17 @@ import { DocTypeSelector } from './docTypeSelector';

function DocSearch({ contextualSearch, externalUrlRegex, ...props }) {
const queryIDRef = useRef(null);
const lastQueryRef = useRef('');
const { siteMetadata, i18n: { currentLocale } } = useDocusaurusContext();
const processSearchResultUrl = useSearchResultUrlProcessor();
const contextualSearchFacetFilters = useAlgoliaContextualFacetFilters();
const { isAskAIOpen, currentMode } = useAskAI();
const { isAskAIOpen } = useAskAI();
const history = useHistory();
const searchButtonRef = useRef(null);

// Doc type filtering state
const [selectedDocTypes, setSelectedDocTypes] = useState(null);
const searchParametersRef = useRef(null);

// Use the modal management hook
const {
isOpen,
initialQuery,
Expand All @@ -47,26 +46,63 @@ function DocSearch({ contextualSearch, externalUrlRegex, ...props }) {
importDocSearchModalIfNeeded
} = useDocSearchModal();

// Configure search parameters with doc_type filter
const searchParameters = createSearchParameters(
props,
contextualSearch,
contextualSearchFacetFilters,
selectedDocTypes
);
// Update searchParameters ref instead of creating new object
useEffect(() => {
const newParams = createSearchParameters(
props,
contextualSearch,
contextualSearchFacetFilters,
selectedDocTypes
);

if (!searchParametersRef.current) {
searchParametersRef.current = newParams;
} else {
Object.keys(newParams).forEach(key => {
searchParametersRef.current[key] = newParams[key];
});
}
}, [props, contextualSearch, contextualSearchFacetFilters, selectedDocTypes]);

// Initialize on mount
if (!searchParametersRef.current) {
searchParametersRef.current = createSearchParameters(
props,
contextualSearch,
contextualSearchFacetFilters,
selectedDocTypes
);
}

// Track input changes to capture the query
useEffect(() => {
if (!isOpen) return;

const handleInput = (e) => {
const input = e.target;
if (input.classList.contains('DocSearch-Input')) {
lastQueryRef.current = input.value;
}
};

document.addEventListener('input', handleInput, true);
return () => document.removeEventListener('input', handleInput, true);
}, [isOpen]);

useEffect(() => {
initializeSearchAnalytics(props.appId, props.apiKey);
}, [props.appId, props.apiKey]);

// Create navigator for handling result clicks
const navigator = useMemo(
() => createSearchNavigator(history, externalUrlRegex),
[history, externalUrlRegex]
);

// Transform search items with metadata
const transformItems = useCallback((items, state) => {
if (state?.query) {
lastQueryRef.current = state.query;
}

return transformSearchItems(items, {
transformItems: props.transformItems,
processSearchResultUrl,
Expand All @@ -75,36 +111,74 @@ function DocSearch({ contextualSearch, externalUrlRegex, ...props }) {
});
}, [props.transformItems, processSearchResultUrl, currentLocale]);

const handleDocTypeChange = useCallback((docTypes) => {
setSelectedDocTypes(docTypes);
}, []);
const handleDocTypeChange = useCallback((docTypes) => {
setSelectedDocTypes(docTypes);

// Re-trigger search with updated filters after state update completes
setTimeout(() => {
const input = document.querySelector('.DocSearch-Input');
const query = lastQueryRef.current;

if (input && query) {
// Access React's internal value setter to bypass readonly property
const nativeInputValueSetter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
'value'
).set;

// Clear input to trigger change detection
nativeInputValueSetter.call(input, '');
input.dispatchEvent(new Event('input', { bubbles: true }));

// Restore original query to execute search with new filters
setTimeout(() => {
nativeInputValueSetter.call(input, query);
input.dispatchEvent(new Event('input', { bubbles: true }));
input.focus();
}, 0);
}
}, 100);
}, []);

const resultsFooterComponent = useMemo(
() =>
// eslint-disable-next-line react/no-unstable-nested-components
(footerProps) =>
<SearchResultsFooter {...footerProps} onClose={onClose} />,
[onClose],
() => (footerProps) => <SearchResultsFooter {...footerProps} onClose={onClose} />,
[onClose]
);

const transformSearchClient = useCallback((searchClient) => {
return createEnhancedSearchClient(searchClient, siteMetadata.docusaurusVersion, queryIDRef);
const enhancedClient = createEnhancedSearchClient(
searchClient,
siteMetadata.docusaurusVersion,
queryIDRef
);

const originalSearch = enhancedClient.search.bind(enhancedClient);

let debounceTimeout;
enhancedClient.search = (...args) => {
return new Promise((resolve, reject) => {
clearTimeout(debounceTimeout);
debounceTimeout = setTimeout(() => {
originalSearch(...args)
.then(resolve)
.catch(reject);
}, 200);
});
};

return enhancedClient;
}, [siteMetadata.docusaurusVersion]);

const handleOnOpen = useCallback(() => {
console.log('handleOnOpen called', { isAskAIOpen });

if (shouldPreventSearchAction(isAskAIOpen)) {
console.log('handleOnOpen - preventing search modal');
return;
}

onOpen();
}, [isAskAIOpen, onOpen]);

const handleOnInput = useCallback((event) => {
if (shouldPreventSearchAction(isAskAIOpen)) {
return; // Prevent search input handling
return;
}
onInput(event);
}, [isAskAIOpen, onInput]);
Expand All @@ -118,68 +192,64 @@ function DocSearch({ contextualSearch, externalUrlRegex, ...props }) {
});

return (
<>
<Head>
{/* This hints the browser that the website will load data from Algolia,
and allows it to preconnect to the DocSearch cluster. It makes the first
query faster, especially on mobile. */}
<link
rel="preconnect"
href={`https://${props.appId}-dsn.algolia.net`}
crossOrigin="anonymous"
/>
</Head>

<DocSearchButton
onTouchStart={importDocSearchModalIfNeeded}
onFocus={importDocSearchModalIfNeeded}
onMouseOver={importDocSearchModalIfNeeded}
onClick={onOpen}
ref={searchButtonRef}
translations={translations.button}
<>
<Head>
<link
rel="preconnect"
href={`https://${props.appId}-dsn.algolia.net`}
crossOrigin="anonymous"
/>
</Head>

{isOpen &&
DocSearchModal &&
searchContainer &&
createPortal(
<>
<DocSearchModal
onClose={onClose}
initialScrollY={window.scrollY}
initialQuery={initialQuery}
navigator={navigator}
transformItems={transformItems}
hitComponent={SearchHit}
transformSearchClient={transformSearchClient}
{...(props.searchPagePath && {
resultsFooterComponent,
})}
{...props}
insights={true}
searchParameters={searchParameters}
placeholder={translations.placeholder}
translations={translations.modal}
/>

{/* Selector positioned as overlay */}
<div style={{
position: 'fixed',
top: window.innerWidth < 768 ? '55px' : '120px',
right: window.innerWidth < 768 ? 'calc(50% - 185px)' : 'calc(50% - 255px)',
zIndex: 10000,
backgroundColor: 'var(--docsearch-modal-background)',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
}}>
<DocTypeSelector
selectedDocTypes={selectedDocTypes}
onSelectionChange={handleDocTypeChange}
/>
</div>
</>,
searchContainer,
)}
</>
<DocSearchButton
onTouchStart={importDocSearchModalIfNeeded}
onFocus={importDocSearchModalIfNeeded}
onMouseOver={importDocSearchModalIfNeeded}
onClick={onOpen}
ref={searchButtonRef}
translations={translations.button}
/>

{isOpen &&
DocSearchModal &&
searchContainer &&
createPortal(
<>
<DocSearchModal
onClose={onClose}
initialScrollY={window.scrollY}
initialQuery={initialQuery}
navigator={navigator}
transformItems={transformItems}
hitComponent={SearchHit}
transformSearchClient={transformSearchClient}
{...(props.searchPagePath && {
resultsFooterComponent,
})}
{...props}
insights={true}
searchParameters={searchParametersRef.current}
placeholder={translations.placeholder}
translations={translations.modal}
/>

<div style={{
position: 'fixed',
top: window.innerWidth < 768 ? '55px' : '120px',
right: window.innerWidth < 768 ? 'calc(50% - 185px)' : 'calc(50% - 255px)',
zIndex: 10000,
backgroundColor: 'var(--docsearch-modal-background)',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)'
}}>
<DocTypeSelector
selectedDocTypes={selectedDocTypes}
onSelectionChange={handleDocTypeChange}
/>
</div>
</>,
searchContainer
)}
</>
);
}

Expand All @@ -192,12 +262,8 @@ export default function SearchBar() {
handleSearchKeyboardConflict(event, isAskAIOpen);
};

// Add listener with capture phase to intercept before DocSearch
document.addEventListener('keydown', handleKeyDown, true);

return () => {
document.removeEventListener('keydown', handleKeyDown, true);
};
return () => document.removeEventListener('keydown', handleKeyDown, true);
}, [isAskAIOpen]);

return <DocSearch {...siteConfig.themeConfig.algolia} />;
Expand Down