2022-10-01 20:32:01 +00:00
|
|
|
import Fuse from 'fuse.js'
|
2022-10-01 22:35:57 +00:00
|
|
|
import React, { Fragment, useCallback, useEffect, useState } from 'react'
|
2022-10-01 20:32:01 +00:00
|
|
|
import { Search as SearchIcon } from 'react-feather'
|
|
|
|
import { SearchIndex, SearchItem } from '../utils/search'
|
|
|
|
import { Dialog, Transition, Combobox } from '@headlessui/react'
|
|
|
|
import { useRouter } from 'next/router'
|
|
|
|
|
|
|
|
export interface SearchProps {
|
|
|
|
index: SearchIndex
|
|
|
|
}
|
|
|
|
|
|
|
|
interface SearchResultProps {
|
|
|
|
result: Fuse.FuseResult<SearchItem>
|
|
|
|
selected: boolean
|
|
|
|
active: boolean
|
|
|
|
}
|
|
|
|
|
|
|
|
const SearchResult: React.FunctionComponent<SearchResultProps> = (props) => {
|
|
|
|
const excerpt = (s: string) => {
|
2022-10-02 00:00:45 +00:00
|
|
|
if (s.length < 120) {
|
2022-10-01 20:32:01 +00:00
|
|
|
return <>{s}</>
|
|
|
|
} else {
|
2022-10-02 00:00:45 +00:00
|
|
|
return <>{s.substring(0, 120)}…</>
|
2022-10-01 20:32:01 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
const bg_for = (s: string) => {
|
|
|
|
const bg = 'p-1 rounded bg-opacity-20 '
|
|
|
|
if (s === 'var') {
|
|
|
|
return bg + 'bg-blue-500'
|
|
|
|
}
|
|
|
|
if (s === 'config') {
|
|
|
|
return bg + 'bg-green-500'
|
|
|
|
}
|
|
|
|
if (s === 'lua') {
|
|
|
|
return bg + 'bg-red-500'
|
|
|
|
}
|
|
|
|
return bg
|
|
|
|
}
|
|
|
|
const selection = props.active ? 'bg-slate-300 dark:bg-slate-700' : ''
|
|
|
|
|
|
|
|
return (
|
|
|
|
<div className={`m-1 rounded flex flex-col p-2 ${selection}`}>
|
|
|
|
<div className="flex justify-between">
|
|
|
|
<div>
|
|
|
|
<code className="text-lg p-1 mx-1 bg-fuchsia-200 dark:bg-fuchsia-900 font-bold">
|
|
|
|
{props.result.item.name}
|
|
|
|
</code>
|
|
|
|
</div>
|
|
|
|
<div className={bg_for(props.result.item.kind)}>
|
|
|
|
{props.result.item.kind}
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div>
|
|
|
|
<p>{excerpt(props.result.item.desc)}</p>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
const Search: React.FunctionComponent<SearchProps> = (props) => {
|
|
|
|
const router = useRouter()
|
|
|
|
const [searchText, setSearchText] = useState('')
|
|
|
|
const [selected, setSelected] = useState<
|
|
|
|
Fuse.FuseResult<SearchItem> | undefined
|
|
|
|
>(undefined)
|
|
|
|
const [searchResults, setSearchResults] = useState<
|
|
|
|
Fuse.FuseResult<SearchItem>[]
|
|
|
|
>([])
|
|
|
|
const [fuse, setFuse] = useState(() => {
|
|
|
|
const options: Fuse.IFuseOptions<SearchItem> = {}
|
|
|
|
return new Fuse(
|
|
|
|
props.index.list,
|
|
|
|
options,
|
|
|
|
Fuse.parseIndex(props.index.index)
|
|
|
|
)
|
|
|
|
})
|
2022-10-01 22:35:57 +00:00
|
|
|
const [isOpen, setIsOpen] = useState(false)
|
|
|
|
const handleKeyPress = useCallback(
|
|
|
|
(event: KeyboardEvent) => {
|
2022-10-05 02:46:14 +00:00
|
|
|
if (event.key === 'k' && (event.metaKey || event.ctrlKey) && isOpen) {
|
|
|
|
setIsOpen(false)
|
|
|
|
event.preventDefault()
|
|
|
|
} else if (
|
|
|
|
(event.key == '/' ||
|
|
|
|
(event.key === 'k' && (event.metaKey || event.ctrlKey))) &&
|
|
|
|
!isOpen
|
|
|
|
) {
|
2022-10-01 22:35:57 +00:00
|
|
|
setIsOpen(true)
|
|
|
|
event.preventDefault()
|
|
|
|
}
|
|
|
|
},
|
|
|
|
[isOpen]
|
|
|
|
)
|
|
|
|
useEffect(() => {
|
|
|
|
document.addEventListener('keydown', handleKeyPress)
|
|
|
|
|
|
|
|
return () => {
|
|
|
|
document.removeEventListener('keydown', handleKeyPress)
|
|
|
|
}
|
|
|
|
}, [handleKeyPress])
|
2022-10-01 20:32:01 +00:00
|
|
|
|
|
|
|
const setSearch = (value: string) => {
|
|
|
|
setSearchText(value)
|
|
|
|
const searchResult = fuse.search(value)
|
|
|
|
setSearchResults(searchResult)
|
|
|
|
}
|
2022-10-01 22:35:57 +00:00
|
|
|
const onChange = (value?: Fuse.FuseResult<SearchItem>) => {
|
|
|
|
if (value) {
|
|
|
|
if (value.item.kind === 'var') {
|
|
|
|
router.push(`/variables#${value.item.name}`, undefined, {
|
|
|
|
scroll: false,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
if (value.item.kind === 'config') {
|
|
|
|
router.push(`/config_settings#${value.item.name}`, undefined, {
|
|
|
|
scroll: false,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
if (value.item.kind === 'lua') {
|
|
|
|
router.push(`/lua#${value.item.name}`, undefined, { scroll: false })
|
|
|
|
}
|
2022-10-01 22:53:38 +00:00
|
|
|
setIsOpen(false)
|
2022-10-01 20:32:01 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
const closeModal = () => {
|
|
|
|
setIsOpen(false)
|
|
|
|
}
|
|
|
|
|
|
|
|
const openModal = () => {
|
|
|
|
setIsOpen(true)
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<>
|
|
|
|
<div className="flex items-center ml-2">
|
2022-10-05 02:46:14 +00:00
|
|
|
<button onClick={openModal} title="Search (/ or ⌘K)">
|
2022-10-01 20:32:01 +00:00
|
|
|
<SearchIcon size={32} />
|
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
|
|
|
|
<Transition appear show={isOpen} as={Fragment}>
|
|
|
|
<Dialog as="div" className="relative z-10" onClose={closeModal}>
|
|
|
|
<Transition.Child
|
|
|
|
as={Fragment}
|
|
|
|
enter="ease-out duration-300"
|
|
|
|
enterFrom="opacity-0"
|
|
|
|
enterTo="opacity-100"
|
|
|
|
>
|
|
|
|
<div className="fixed inset-0 bg-black bg-opacity-25 backdrop-blur-sm" />
|
|
|
|
</Transition.Child>
|
|
|
|
|
2022-10-01 23:27:06 +00:00
|
|
|
<Transition.Child
|
|
|
|
as={Fragment}
|
|
|
|
enter="ease-out duration-300"
|
|
|
|
enterFrom="opacity-0 scale-95"
|
|
|
|
enterTo="opacity-100 scale-100"
|
|
|
|
>
|
|
|
|
<div className="fixed inset-0">
|
|
|
|
<div className="flex h-screen w-screen items-start justify-center p-16 text-center">
|
2022-10-01 20:32:01 +00:00
|
|
|
<Dialog.Panel className="flex flex-col max-h-full w-full max-w-2xl p-1 bg-gray-200 dark:bg-gray-800 transform rounded-xl text-left align-middle shadow transition-all border border-gray-800 dark:border-white border-opacity-10 dark:border-opacity-10">
|
|
|
|
<Combobox value={selected} nullable onChange={onChange}>
|
|
|
|
<div className="flex">
|
|
|
|
<Combobox.Label className="flex items-center ml-2">
|
|
|
|
<SearchIcon size={32} />
|
|
|
|
</Combobox.Label>
|
|
|
|
<Combobox.Input
|
2022-10-05 02:46:14 +00:00
|
|
|
placeholder="Search docs (/ or ⌘K)"
|
2022-10-01 20:32:01 +00:00
|
|
|
className="mx-1 p-2 w-full bg-gray-200 dark:bg-gray-800 outline-none"
|
|
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
|
|
/>
|
|
|
|
</div>
|
|
|
|
<Combobox.Options className="flex flex-col h-full overflow-auto">
|
|
|
|
{searchResults.length === 0 && searchText !== '' ? (
|
|
|
|
<div className="relative cursor-default select-none py-2 px-4 text-gray-500">
|
|
|
|
No results.
|
|
|
|
</div>
|
|
|
|
) : (
|
|
|
|
searchResults.map((r) => (
|
|
|
|
<Combobox.Option key={r.refIndex} value={r}>
|
|
|
|
{({ selected, active }) => (
|
|
|
|
<SearchResult
|
|
|
|
result={r}
|
|
|
|
selected={selected}
|
|
|
|
active={active}
|
|
|
|
/>
|
|
|
|
)}
|
|
|
|
</Combobox.Option>
|
|
|
|
))
|
|
|
|
)}
|
|
|
|
</Combobox.Options>
|
|
|
|
</Combobox>
|
|
|
|
</Dialog.Panel>
|
2022-10-01 23:27:06 +00:00
|
|
|
</div>
|
2022-10-01 20:32:01 +00:00
|
|
|
</div>
|
2022-10-01 23:27:06 +00:00
|
|
|
</Transition.Child>
|
2022-10-01 20:32:01 +00:00
|
|
|
</Dialog>
|
|
|
|
</Transition>
|
|
|
|
</>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
export default Search
|