266 lines
6.5 KiB
TypeScript
266 lines
6.5 KiB
TypeScript
import { SitePathsContext } from 'Components/utils/withSitePaths'
|
|
import { parentName } from 'Engine/ruleUtils'
|
|
import { ParsedRule, ParsedRules } from 'Engine/types'
|
|
import { pick, sortBy, take } from 'ramda'
|
|
import React, { useContext, useEffect, useState } from 'react'
|
|
import FuzzyHighlighter, { Highlighter } from 'react-fuzzy-highlighter'
|
|
import { useTranslation } from 'react-i18next'
|
|
import { Link, Redirect, useHistory } from 'react-router-dom'
|
|
import { DottedName } from 'Rules'
|
|
import Worker from 'worker-loader!./SearchBar.worker.js'
|
|
import { capitalise0 } from '../utils'
|
|
import './SearchBar.css'
|
|
|
|
const worker = new Worker()
|
|
|
|
type SearchBarProps = {
|
|
rules: ParsedRules<DottedName>
|
|
showDefaultList: boolean
|
|
finally?: () => void
|
|
}
|
|
|
|
type Option = Pick<ParsedRule<DottedName>, 'dottedName' | 'name' | 'title'>
|
|
type Result = Pick<ParsedRule<DottedName>, 'dottedName'>
|
|
|
|
export default function SearchBar({
|
|
rules,
|
|
showDefaultList,
|
|
finally: finallyCallback
|
|
}: SearchBarProps) {
|
|
const sitePaths = useContext(SitePathsContext)
|
|
const [input, setInput] = useState('')
|
|
const [selectedOption, setSelectedOption] = useState<Option | null>(null)
|
|
const [results, setResults] = useState<Array<Result>>([])
|
|
let [focusElem, setFocusElem] = useState(-1)
|
|
const { i18n } = useTranslation()
|
|
const history = useHistory()
|
|
|
|
const handleKeyDown = e => {
|
|
if (e.key === 'Enter' && results.length > 0) {
|
|
finallyCallback && finallyCallback()
|
|
history.push(
|
|
sitePaths.documentation.rule(
|
|
results[focusElem > 0 ? focusElem : 0].dottedName
|
|
)
|
|
)
|
|
}
|
|
|
|
if (
|
|
e.key === 'ArrowDown' &&
|
|
focusElem < (results.length > 5 ? 4 : results.length - 1)
|
|
) {
|
|
if (focusElem === -1) {
|
|
setFocusElem(0)
|
|
}
|
|
setFocusElem(focusElem + 1)
|
|
} else if (e.key === 'ArrowUp' && focusElem > 0) {
|
|
setFocusElem(focusElem - 1)
|
|
}
|
|
return true
|
|
}
|
|
|
|
useEffect(() => {
|
|
worker.postMessage({
|
|
rules: Object.values(rules).map(
|
|
pick(['title', 'espace', 'description', 'name', 'dottedName'])
|
|
)
|
|
})
|
|
|
|
worker.onmessage = ({ data: results }) => setResults(results)
|
|
}, [rules, results, focusElem])
|
|
|
|
let onMouseOverHandler = () => setFocusElem(-1)
|
|
|
|
const getDottedName = (href: String) => {
|
|
const currentSlug = decodeURI(href.substring(href.lastIndexOf('/') + 1))
|
|
return (
|
|
[
|
|
{
|
|
slugs: ['salarié', 'assimilé-salarié'],
|
|
dottedName: 'contrat salarié'
|
|
},
|
|
{
|
|
slugs: ['indépendant', 'auto-entrepreneur'],
|
|
dottedName: 'dirigeant'
|
|
}
|
|
].find(
|
|
item => item.slugs.find(slug => slug === currentSlug) !== undefined
|
|
)?.dottedName || ''
|
|
)
|
|
}
|
|
|
|
let renderOptions = (rules?: Array<ParsedRule>) => {
|
|
const currentPage = getDottedName(window.location.href)
|
|
let options = (rules && sortBy(rule => rule.dottedName, rules)) || results
|
|
let currentOptions: Array<Option> = []
|
|
let notCurrentOptions: Array<Option> = []
|
|
options.forEach(option => {
|
|
if (option.dottedName.startsWith(currentPage)) {
|
|
currentOptions.push(option)
|
|
} else {
|
|
notCurrentOptions.push(option)
|
|
}
|
|
})
|
|
return (
|
|
<ul>
|
|
{take(5)([...currentOptions, ...notCurrentOptions]).map((option, idx) =>
|
|
renderOption(option, idx)
|
|
)}
|
|
</ul>
|
|
)
|
|
}
|
|
|
|
let renderOption = (option: Option, idx: number) => {
|
|
let { title, dottedName, name } = option
|
|
let espace = parentName(dottedName)
|
|
? parentName(dottedName)
|
|
.split(' . ')
|
|
.map(capitalise0)
|
|
.join(' - ')
|
|
: ''
|
|
return (
|
|
<li
|
|
key={dottedName}
|
|
className={focusElem === idx ? 'active' : `${focusElem}-inactive`}
|
|
css={`
|
|
padding: 0.4rem;
|
|
border-radius: 0.3rem;
|
|
:hover {
|
|
background: var(--color);
|
|
color: var(--textColor);
|
|
}
|
|
:hover a {
|
|
color: var(--textColor);
|
|
}
|
|
`}
|
|
onClick={() => setSelectedOption(option)}
|
|
onMouseOver={() => onMouseOverHandler()}
|
|
>
|
|
<div
|
|
style={{
|
|
fontWeight: 300,
|
|
fontSize: '85%',
|
|
lineHeight: '.9em'
|
|
}}
|
|
>
|
|
<FuzzyHighlighter
|
|
query={input}
|
|
data={[
|
|
{
|
|
title: espace
|
|
}
|
|
]}
|
|
options={{
|
|
includeMatches: true,
|
|
threshold: 0.2,
|
|
minMatchCharLength: 1,
|
|
keys: ['title']
|
|
}}
|
|
>
|
|
{({ results, formattedResults, timing }) => {
|
|
return (
|
|
<>
|
|
{formattedResults.length === 0 && <span>{espace}</span>}
|
|
{formattedResults.map((formattedResult, resultIndex) => {
|
|
if (formattedResult.formatted.title === undefined) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<span key={resultIndex}>
|
|
<Highlighter text={formattedResult.formatted.title} />
|
|
</span>
|
|
)
|
|
})}
|
|
</>
|
|
)
|
|
}}
|
|
</FuzzyHighlighter>
|
|
</div>
|
|
|
|
<FuzzyHighlighter
|
|
query={input}
|
|
data={[{ title: title || capitalise0(name) || '' }]}
|
|
options={{
|
|
includeMatches: true,
|
|
threshold: 0.3,
|
|
keys: ['title', 'name']
|
|
}}
|
|
>
|
|
{({ results, formattedResults, timing }) => {
|
|
return (
|
|
<>
|
|
{formattedResults.length === 0 && (
|
|
<Link to={sitePaths.documentation.rule(dottedName)}>
|
|
{title || capitalise0(name) || ''}
|
|
</Link>
|
|
)}
|
|
{formattedResults.map((formattedResult, resultIndex) => {
|
|
if (formattedResult.formatted.title === undefined) {
|
|
return null
|
|
}
|
|
|
|
return (
|
|
<Link
|
|
to={sitePaths.documentation.rule(dottedName)}
|
|
key={resultIndex}
|
|
>
|
|
<Highlighter text={formattedResult.formatted.title} />
|
|
</Link>
|
|
)
|
|
})}
|
|
</>
|
|
)
|
|
}}
|
|
</FuzzyHighlighter>
|
|
</li>
|
|
)
|
|
}
|
|
|
|
if (selectedOption !== null) {
|
|
finallyCallback && finallyCallback()
|
|
return (
|
|
<Redirect to={sitePaths.documentation.rule(selectedOption.dottedName)} />
|
|
)
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<input
|
|
type="search"
|
|
css={`
|
|
padding: 0.4rem;
|
|
margin: 0.2rem 0;
|
|
width: 100%;
|
|
border: 1px solid var(--lighterTextColor);
|
|
border-radius: 0.3rem;
|
|
color: inherit;
|
|
font-size: inherit;
|
|
transition: border-color 0.1s;
|
|
position: relative;
|
|
|
|
:focus {
|
|
border-color: var(--color);
|
|
}
|
|
`}
|
|
value={input}
|
|
placeholder={i18n.t('Entrez des mots clefs ici')}
|
|
onKeyDown={e => handleKeyDown(e)}
|
|
onChange={e => {
|
|
let input = e.target.value
|
|
setInput(input)
|
|
if (input.length > 0) worker.postMessage({ input })
|
|
}}
|
|
/>
|
|
{input.length > 2 &&
|
|
!results.length &&
|
|
i18n.t('noresults', {
|
|
defaultValue: "Nous n'avons rien trouvé…"
|
|
})}
|
|
{showDefaultList && !input
|
|
? renderOptions(Object.values(rules))
|
|
: renderOptions()}
|
|
</>
|
|
)
|
|
}
|