File "global-search.js"
Full Path: /home/fresvfqn/waterdamagerestorationandrepairsmithtown.com/wp-content/plugins/surerank/src/apps/admin-components/global-search.js
File size: 9.3 KB
MIME-type: text/x-java
Charset: utf-8
import { __ } from '@wordpress/i18n';
import { SearchBox } from '@bsf/force-ui';
import {
useNavigate,
useLocation,
useRouterState,
} from '@tanstack/react-router';
import { useState, useMemo, useEffect } from '@wordpress/element';
import { File } from 'lucide-react';
import { scrollToElement as scrollToElementFn } from '@Functions/utils';
const flattenNavLinks = ( navLinks ) => {
const flattened = [];
const extractPageContent = ( content ) => {
if ( ! content ) {
return [];
}
const processContentItem = ( item, parentLabel = '' ) => {
// Skip non-searchable items explicitly marked as not searchable
if ( item?.searchable === false ) {
return null;
}
// For custom components: collect searchKeywords but don't display as separate items
// If it's a custom component without label and no keywords, skip it
if (
item.type === 'custom' &&
! item.label &&
! item.searchKeywords?.length
) {
return null;
}
// Create a content item with all relevant information
const contentItem = {
label: item.label || parentLabel || '',
description: item.description || '',
type: 'content',
id: item.id || '',
storeKey: item.storeKey || '',
contentType: item.type,
dataType: item.dataType || '',
searchKeywords: item.searchKeywords || [],
useParentLabel: ! item.label && !! parentLabel,
};
// Add options if they exist (for radio, select, etc.)
if ( item.options ) {
contentItem.options = item.options
.map( ( opt ) => opt.label )
.join( ', ' );
}
// Add tooltip if it exists
if ( item.tooltip ) {
contentItem.tooltip = item.tooltip;
}
return contentItem;
};
const processContentArray = ( contentArray, parentLabel = '' ) => {
return contentArray.reduce( ( acc, item ) => {
// If item has its own content array, process it recursively
// Pass down the label if available for nested items without labels
if ( item.content ) {
acc.push(
...processContentArray(
item.content,
item.label || parentLabel
)
);
}
// Process the current item
const processedItem = processContentItem( item, parentLabel );
if ( processedItem ) {
acc.push( processedItem );
}
return acc;
}, [] );
};
return content.reduce( ( acc, section ) => {
if ( section.content ) {
acc.push( ...processContentArray( section.content ) );
}
return acc;
}, [] );
};
navLinks.forEach( ( section ) => {
section.links.forEach( ( link ) => {
// Add main link
const linkItem = {
path: link.path,
label: link.label,
icon: link.icon,
section: section.section,
type: 'link',
};
// Add page content if exists
if ( link.pageContent ) {
linkItem.content = extractPageContent( link.pageContent );
}
flattened.push( linkItem );
// Add submenu links if they exist
if ( link.submenu ) {
link.submenu.forEach( ( subLink ) => {
const subLinkItem = {
path: subLink.path,
label: `${ link.label } > ${ subLink.label }`,
icon: link.icon,
section: section.section,
type: 'link',
};
// Add page content for submenu items if exists
if ( subLink.pageContent ) {
subLinkItem.content = extractPageContent(
subLink.pageContent
);
}
flattened.push( subLinkItem );
} );
}
} );
} );
return flattened;
};
const GlobalSearch = ( { navLinks = [] } ) => {
const navigate = useNavigate();
const location = useLocation();
const {
location: {
state: { scrollToElement = undefined },
},
} = useRouterState();
const [ open, setOpen ] = useState( false );
const [ searchResults, setSearchResults ] = useState( [] );
const flattenedLinks = useMemo(
() => flattenNavLinks( navLinks ),
[ navLinks ]
);
const handleChange = ( value ) => {
const searchTerm = value.toLowerCase();
if ( ! searchTerm ) {
setSearchResults( [] );
return;
}
const results = flattenedLinks.reduce( ( acc, item ) => {
// Track if we've already added this item to prevent duplicates
let itemAdded = false;
// Check if any content items match
if ( item.content ) {
// First gather all matching content
const matchingContent = item.content.filter(
( contentItem ) => {
// Match by standard fields
const matchesStandardFields =
contentItem.label
.toLowerCase()
.includes( searchTerm ) ||
contentItem.description
.toLowerCase()
.includes( searchTerm ) ||
contentItem.tooltip
?.toLowerCase()
.includes( searchTerm ) ||
contentItem.options
?.toLowerCase()
.includes( searchTerm );
// Match by keywords
const matchesKeywords =
contentItem.searchKeywords?.some( ( keyword ) =>
keyword.toLowerCase().includes( searchTerm )
);
return matchesStandardFields || matchesKeywords;
}
);
// Add matching items to results
if ( matchingContent.length > 0 ) {
// Check for custom components with only search keywords
const hasCustomWithKeywords = matchingContent.some(
( contentItem ) =>
contentItem.contentType === 'custom' &&
! contentItem.label &&
contentItem.searchKeywords?.some( ( keyword ) =>
keyword.toLowerCase().includes( searchTerm )
)
);
// If we have custom components with only keywords, add the parent section once
if ( hasCustomWithKeywords ) {
acc.push( {
path: item.path,
label: item.label,
section: item.section,
icon: item.icon,
type: 'link',
} );
itemAdded = true;
}
// If we didn't add the parent item for custom components,
// add individual matching items
if ( ! itemAdded ) {
matchingContent.forEach( ( contentItem ) => {
if (
contentItem.label ||
contentItem.contentType !== 'custom'
) {
acc.push( {
...contentItem,
parentPath: item.path,
parentLabel: item.label,
section: item.section,
icon: item.icon,
} );
}
} );
}
}
}
// Check if main item matches - only if we haven't added it already due to custom components
if (
! itemAdded &&
( item.label.toLowerCase().includes( searchTerm ) ||
item.section.toLowerCase().includes( searchTerm ) )
) {
acc.push( item );
}
return acc;
}, [] );
setSearchResults( results );
};
const handleItemClick = ( item ) => {
let path = '';
switch ( item.type ) {
case 'link':
path = item.path;
break;
case 'content':
path = item.parentPath;
break;
default:
path = item.parentPath;
break;
}
const containsHttp = path.includes( 'http' );
if ( containsHttp ) {
// Open external links.
const searchParam = item?.id ? `?scrollToElement=${ item.id }` : '';
window.open( path + searchParam, '_self', 'noopener,noreferrer' );
return;
}
// If the item is a content type, store the element identifier for scrolling after navigation
if ( item.type === 'content' ) {
const elementId = item.id || item.storeKey;
// Navigate with state containing the element ID to scroll to
navigate( {
to: path,
// Use the state to store the element ID to scroll to.
state: { scrollToElement: elementId },
} );
} else {
navigate( { to: path } );
}
};
useEffect( () => {
// Get scrollToElement from query params.
const { scrollToElement: scrollToElementParamValue, ...restParams } =
location.search;
if ( scrollToElement || scrollToElementParamValue ) {
scrollToElementFn( scrollToElement || scrollToElementParamValue );
}
// Remove scrollToElement from query params.
if ( scrollToElementParamValue ) {
navigate( {
to: location.pathname,
search: restParams,
replace: true,
} );
}
}, [ scrollToElement, location.search ] );
return (
<div>
<SearchBox
variant="secondary"
size="sm"
open={ open }
setOpen={ setOpen }
className="w-full md:w-72 z-50"
filter={ false }
>
<SearchBox.Input
onChange={ handleChange }
placeholder={ __( 'Search…', 'surerank' ) }
/>
<SearchBox.Content className="!max-h-96">
<SearchBox.List className="p-1.5">
{ searchResults.map( ( item, index ) => (
<SearchBox.Item
key={
`${ item.path }-${ index }` ||
`${ item.parentPath }-${ index }`
}
icon={
item.icon ? (
<item.icon className="size-4" />
) : (
<File className="size-4" />
)
}
onClick={ () => handleItemClick( item ) }
className="items-start [&>:nth-child(2)]:pt-0"
>
<div className="flex flex-col">
<span className="text-sm font-medium">
{ item.type === 'content' ? (
<>
<span className="text-text-tertiary">
{ item.parentLabel } ›{ ' ' }
</span>
{ item.useParentLabel
? __(
'Settings',
'surerank'
)
: item.label }
</>
) : (
item.label
) }
</span>
</div>
</SearchBox.Item>
) ) }
{ searchResults.length === 0 && <SearchBox.Empty /> }
</SearchBox.List>
</SearchBox.Content>
</SearchBox>
</div>
);
};
export default GlobalSearch;