@ -2,7 +2,7 @@ import { useState, useEffect, useCallback, Fragment } from "react";
import { useTranslation } from "next-i18next" ;
import { useTranslation } from "next-i18next" ;
import { FiSearch } from "react-icons/fi" ;
import { FiSearch } from "react-icons/fi" ;
import { SiDuckduckgo , SiMicrosoftbing , SiGoogle , SiBaidu , SiBrave } from "react-icons/si" ;
import { SiDuckduckgo , SiMicrosoftbing , SiGoogle , SiBaidu , SiBrave } from "react-icons/si" ;
import { Listbox , Transition } from "@headlessui/react" ;
import { Listbox , Transition , Combobox } from "@headlessui/react" ;
import classNames from "classnames" ;
import classNames from "classnames" ;
import ContainerForm from "../widget/container_form" ;
import ContainerForm from "../widget/container_form" ;
@ -12,26 +12,31 @@ export const searchProviders = {
google : {
google : {
name : "Google" ,
name : "Google" ,
url : "https://www.google.com/search?q=" ,
url : "https://www.google.com/search?q=" ,
suggestionUrl : "https://www.google.com/complete/search?client=chrome&q=" ,
icon : SiGoogle ,
icon : SiGoogle ,
} ,
} ,
duckduckgo : {
duckduckgo : {
name : "DuckDuckGo" ,
name : "DuckDuckGo" ,
url : "https://duckduckgo.com/?q=" ,
url : "https://duckduckgo.com/?q=" ,
suggestionUrl : "https://duckduckgo.com/ac/?type=list&q=" ,
icon : SiDuckduckgo ,
icon : SiDuckduckgo ,
} ,
} ,
bing : {
bing : {
name : "Bing" ,
name : "Bing" ,
url : "https://www.bing.com/search?q=" ,
url : "https://www.bing.com/search?q=" ,
suggestionUrl : "https://api.bing.com/osjson.aspx?query=" ,
icon : SiMicrosoftbing ,
icon : SiMicrosoftbing ,
} ,
} ,
baidu : {
baidu : {
name : "Baidu" ,
name : "Baidu" ,
url : "https://www.baidu.com/s?wd=" ,
url : "https://www.baidu.com/s?wd=" ,
suggestionUrl : "http://suggestion.baidu.com/su?&action=opensearch&ie=utf-8&wd=" ,
icon : SiBaidu ,
icon : SiBaidu ,
} ,
} ,
brave : {
brave : {
name : "Brave" ,
name : "Brave" ,
url : "https://search.brave.com/search?q=" ,
url : "https://search.brave.com/search?q=" ,
suggestionUrl : "https://search.brave.com/api/suggest?&rich=false&q=" ,
icon : SiBrave ,
icon : SiBrave ,
} ,
} ,
custom : {
custom : {
@ -72,6 +77,7 @@ export default function Search({ options }) {
const [ selectedProvider , setSelectedProvider ] = useState (
const [ selectedProvider , setSelectedProvider ] = useState (
searchProviders [ availableProviderIds [ 0 ] ? ? searchProviders . google ] ,
searchProviders [ availableProviderIds [ 0 ] ? ? searchProviders . google ] ,
) ;
) ;
const [ searchSuggestions , setSearchSuggestions ] = useState ( [ ] ) ;
useEffect ( ( ) => {
useEffect ( ( ) => {
const storedProvider = getStoredProvider ( ) ;
const storedProvider = getStoredProvider ( ) ;
@ -82,9 +88,40 @@ export default function Search({ options }) {
}
}
} , [ availableProviderIds ] ) ;
} , [ availableProviderIds ] ) ;
useEffect ( ( ) => {
const abortController = new AbortController ( ) ;
if (
options . showSearchSuggestions &&
( selectedProvider . suggestionUrl || options . suggestionUrl ) && / / c u s t o m p r o v i d e r s p a s s u r l v i a o p t i o n s
query . trim ( ) !== searchSuggestions [ 0 ]
) {
fetch ( ` /api/search/searchSuggestion?query= ${ encodeURIComponent ( query ) } &providerName= ${ selectedProvider . name } ` , {
signal : abortController . signal ,
} )
. then ( async ( searchSuggestionResult ) => {
const newSearchSuggestions = await searchSuggestionResult . json ( ) ;
if ( newSearchSuggestions ) {
if ( newSearchSuggestions [ 1 ] . length > 4 ) {
newSearchSuggestions [ 1 ] = newSearchSuggestions [ 1 ] . splice ( 0 , 4 ) ;
}
setSearchSuggestions ( newSearchSuggestions ) ;
}
} )
. catch ( ( ) => {
/ / I f t h e r e i s a n e r r o r , j u s t i g n o r e i t . T h e r e j u s t w i l l b e n o s e a r c h s u g g e s t i o n s .
} ) ;
}
return ( ) => {
abortController . abort ( ) ;
} ;
} , [ selectedProvider , options , query , searchSuggestions ] ) ;
const submitCallback = useCallback (
const submitCallback = useCallback (
( event ) => {
( valu e) => {
const q = encodeURIComponent ( query ) ;
const q = encodeURIComponent ( value ) ;
const { url } = selectedProvider ;
const { url } = selectedProvider ;
if ( url ) {
if ( url ) {
window . open ( ` ${ url } ${ q } ` , options . target || "_blank" ) ;
window . open ( ` ${ url } ${ q } ` , options . target || "_blank" ) ;
@ -92,11 +129,9 @@ export default function Search({ options }) {
window . open ( ` ${ options . url } ${ q } ` , options . target || "_blank" ) ;
window . open ( ` ${ options . url } ${ q } ` , options . target || "_blank" ) ;
}
}
event . preventDefault ( ) ;
event . target . reset ( ) ;
setQuery ( "" ) ;
setQuery ( "" ) ;
} ,
} ,
[ options. target , options . url , query , selectedProvider ] ,
[ selectedProvider, options . url , options . target ] ,
) ;
) ;
if ( ! availableProviderIds ) {
if ( ! availableProviderIds ) {
@ -109,84 +144,111 @@ export default function Search({ options }) {
} ;
} ;
return (
return (
< ContainerForm options = { options } callback= { submitCallback } additionalClassNames= "grow information-widget-search" >
< ContainerForm options = { options } additionalClassNames= "grow information-widget-search" >
< Raw >
< Raw >
< div className = "flex-col relative h-8 my-4 min-w-fit ">
< div className = "flex-col relative h-8 my-4 min-w-fit z-20 ">
< div className = "flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none w-full text-theme-800 dark:text-white" / >
< div className = "flex absolute inset-y-0 left-0 items-center pl-3 pointer-events-none w-full text-theme-800 dark:text-white" / >
< input
< Combobox value = { query } onChange = { submitCallback } >
type = "text"
< Combobox.Input
className = "
type = "text"
overflow - hidden w - full h - full rounded - md
className = "
text - xs text - theme - 900 dark : text - white
overflow - hidden w - full h - full rounded - md
placeholder - theme - 900 dark : placeholder - white / 80
text - xs text - theme - 900 dark : text - white
bg - white / 50 dark : bg - white / 10
placeholder - theme - 900 dark : placeholder - white / 80
focus : ring - theme - 500 dark : focus : ring - white / 50
bg - white / 50 dark : bg - white / 10
focus : border - theme - 500 dark : focus : border - white / 50
focus : ring - theme - 500 dark : focus : ring - white / 50
border border - theme - 300 dark : border - theme - 200 / 50 "
focus : border - theme - 500 dark : focus : border - white / 50
placeholder = { t ( "search.placeholder" ) }
border border - theme - 300 dark : border - theme - 200 / 50 "
onChange = { ( s ) => setQuery ( s . currentTarget . value ) }
placeholder = { t ( "search.placeholder" ) }
required
onChange = { ( event ) => setQuery ( event . target . value ) }
autoCapitalize = "off"
required
autoCorrect = "off"
autoCapitalize = "off"
autoComplete = "off"
autoCorrect = "off"
/ / e s l i n t - d i s a b l e - n e x t - l i n e j s x - a 1 1 y / n o - a u t o f o c u s
autoComplete = "off"
autoFocus = { options . focus }
/ / e s l i n t - d i s a b l e - n e x t - l i n e j s x - a 1 1 y / n o - a u t o f o c u s
/ >
autoFocus = { options . focus }
< Listbox
/ >
as = "div"
< Listbox
value = { selectedProvider }
as = "div"
onChange = { onChangeProvider }
value = { selectedProvider }
className = "relative text-left"
onChange = { onChangeProvider }
disabled = { availableProviderIds ? . length === 1 }
className = "relative text-left"
>
disabled = { availableProviderIds ? . length === 1 }
< div >
< Listbox.Button
className = "
absolute right - 0.5 bottom - 0.5 rounded - r - md px - 4 py - 2 border - 1
text - white font - medium text - sm
bg - theme - 600 / 40 dark : bg - white / 10
focus : ring - theme - 500 dark : focus : ring - white / 50 "
>
< selectedProvider.icon className = "text-white w-3 h-3" / >
< span className = "sr-only" > { t ( "search.search" ) } < / span >
< / Listbox.Button >
< / div >
< Transition
as = { Fragment }
enter = "transition ease-out duration-100"
enterFrom = "transform opacity-0 scale-95"
enterTo = "transform opacity-100 scale-100"
leave = "transition ease-in duration-75"
leaveFrom = "transform opacity-100 scale-100"
leaveTo = "transform opacity-0 scale-95"
>
>
< Listbox.Options
< div >
className = " absolute right - 0 z - 10 mt - 1 origin - top - right rounded - md
< Listbox.Button
bg - theme - 100 dark : bg - theme - 600 shadow - lg
className = "
ring - 1 ring - black ring - opacity - 5 focus : outline - none "
absolute right - 0.5 bottom - 0.5 rounded - r - md px - 4 py - 2 border - 1
text - white font - medium text - sm
bg - theme - 600 / 40 dark : bg - white / 10
focus : ring - theme - 500 dark : focus : ring - white / 50 "
>
< selectedProvider.icon className = "text-white w-3 h-3" / >
< span className = "sr-only" > { t ( "search.search" ) } < / span >
< / Listbox.Button >
< / div >
< Transition
as = { Fragment }
enter = "transition ease-out duration-100"
enterFrom = "transform opacity-0 scale-95"
enterTo = "transform opacity-100 scale-100"
leave = "transition ease-in duration-75"
leaveFrom = "transform opacity-100 scale-100"
leaveTo = "transform opacity-0 scale-95"
>
>
< div className = "flex flex-col" >
< Listbox.Options
{ availableProviderIds . map ( ( providerId ) => {
className = " absolute right - 0 z - 10 mt - 1 origin - top - right rounded - md
const p = searchProviders [ providerId ] ;
bg - theme - 100 dark : bg - theme - 600 shadow - lg
return (
ring - 1 ring - black ring - opacity - 5 focus : outline - none "
< Listbox.Option key = { providerId } value = { p } as = { Fragment } >
>
{ ( { active } ) => (
< div className = "flex flex-col" >
< li
{ availableProviderIds . map ( ( providerId ) => {
className = { classNames (
const p = searchProviders [ providerId ] ;
"rounded-md cursor-pointer" ,
return (
active ? "bg-theme-600/10 dark:bg-white/10 dark:text-gray-900" : "dark:text-gray-100" ,
< Listbox.Option key = { providerId } value = { p } as = { Fragment } >
) }
{ ( { active } ) => (
>
< li
< p.icon className = "h-4 w-4 mx-4 my-2" / >
className = { classNames (
< / li >
"rounded-md cursor-pointer" ,
) }
active ? "bg-theme-600/10 dark:bg-white/10 dark:text-gray-900" : "dark:text-gray-100" ,
< / Listbox.Option >
) }
) ;
>
} ) }
< p.icon className = "h-4 w-4 mx-4 my-2" / >
< / li >
) }
< / Listbox.Option >
) ;
} ) }
< / div >
< / Listbox.Options >
< / Transition >
< / Listbox >
{ searchSuggestions [ 1 ] ? . length > 0 && (
< Combobox.Options className = "mt-1 rounded-md bg-theme-50 dark:bg-theme-800 border border-theme-300 dark:border-theme-200/30 cursor-pointer shadow-lg" >
< div className = "p-1 bg-white/50 dark:bg-white/10 text-theme-900/90 dark:text-white/90 text-xs" >
< Combobox.Option key = { query } value = { query } / >
{ searchSuggestions [ 1 ] . map ( ( suggestion ) => (
< Combobox.Option key = { suggestion } value = { suggestion } className = "flex w-full" >
{ ( { active } ) => (
< div
className = { classNames (
"px-2 py-1 rounded-md w-full flex-nowrap" ,
active ? "bg-theme-300/20 dark:bg-white/10" : "" ,
) }
>
< span className = "whitespace-pre" > { suggestion . indexOf ( query ) === 0 ? query : "" } < / span >
< span className = "mr-4 whitespace-pre opacity-50" >
{ suggestion . indexOf ( query ) === 0 ? suggestion . substring ( query . length ) : suggestion }
< / span >
< / div >
) }
< / Combobox.Option >
) ) }
< / div >
< / div >
< / Listbox.Options >
< / Combo box.Options>
< / Transition >
)}
< / Listbox >
< / Combo box>
< / div >
< / div >
< / Raw >
< / Raw >
< / ContainerForm >
< / ContainerForm >