UNPKG

gitdocs

Version:

Easy to use, SEO-friendly, beautiful documentation that lives in your git repo.

226 lines (194 loc) 5.53 kB
import React, { Component } from 'react' import enhanceClickOutside from 'react-click-outside' import { Link } from 'react-router-dom' import axios from 'axios' import Highlight from 'react-highlight-words' import { Search as SearchIcon, ChevronRight } from 'react-feather' import { createDB } from './db' import strip from './strip' import history from '../history' import { ellipsify } from '../utils' import { Wrapper, Input, Results, Result, Center } from './styles' const UP = 'ArrowUp' const DOWN = 'ArrowDown' const ENTER = 'Enter' const ESCAPE = 'Escape' class Search extends Component { constructor (props) { super(props) // Basic state for querying and loading this.state = { query: '', loading: false, selectedIndex: 0, results: [], } // Index docs for search results this.loadResults() } componentDidUpdate (prevProps, prevState) { if (this.state.selectedIndex !== prevState.selectedIndex) { this.ensureActiveItemVisible() } } ensureActiveItemVisible () { if (!this.activeItem) return false const distanceFromTop = this.activeItem.offsetTop const height = this.activeItem.offsetHeight const scrollTop = this.results.scrollTop const clientHeight = this.results.clientHeight if (distanceFromTop === 0) { return this.results.scrollTop = 0 } if (distanceFromTop < scrollTop) { return this.results.scrollTop = distanceFromTop } if ((distanceFromTop + height) > (scrollTop + clientHeight)) { return this.results.scrollTop = distanceFromTop - height } } async loadResults () { // Initialize search instance and set indices const resp = await axios.get('/db.json') this.db = createDB({ ref: 'url', indices: ['title', 'content'], items: resp.data, }) } handleChange = e => { const { value } = e.target this.setState({ query: value, loading: value.length !== 0, selectedIndex: 0, }, async () => { const results = await this.fetchResults(value) this.setState({ results, loading: false }) this.results.scrollTop = 0 }) } handleKeyUp = e => { const { key } = e if (e.key === ESCAPE) { return this.clearSearch() } // Only listen for key up, down, and enter if ( !key === UP && !key === DOWN && !key === ENTER ) return false // Get the selected index if it exists const { selectedIndex = 0, results } = this.state if (key === ENTER) { const selected = results[selectedIndex] if (selected) history.push(selected.url) this.clearSearch() } // Next selected index let nextIndex = selectedIndex if (key === UP) { if (selectedIndex === 0) { nextIndex = results.length - 1 } else if (selectedIndex < 0) { nextIndex = results.length - 1 } else { nextIndex = selectedIndex - 1 } } if (key === DOWN) { if (selectedIndex === results.length - 1) { nextIndex = 0 } else { nextIndex = selectedIndex + 1 } } this.setState({ selectedIndex: nextIndex }) } handleClickOutside () { this.clearSearch() } fetchResults (query) { return new Promise((resolve, reject) => { const results = this.db .search(query) .slice(0, 10) resolve(results) }) } clearSearch = () => { this.setState({ loading: false, query: '', results: [], selectedIndex: 0, }) } renderBreadCrumb (result) { return result.breadcrumbs .slice(1, result.breadcrumbs.length) .map(({ title }, i) => ( <span key={`${title}-${i}`}> {i !== 0 && <ChevronRight size={14} style={{ display: 'inline-block', padding: '0 .25rem', position: 'relative', top: 2, }} />} {title} </span> )) } renderResults () { const { query, loading, selectedIndex, results } = this.state if (!query.length) return null // Map over search results and create links const items = results.map((r, i) => <Result key={r.url} selected={i === selectedIndex} innerRef={ref => i === selectedIndex ? this.activeItem = ref : null} onClick={this.clearSearch} > <Link to={r.url}> <h5>{this.renderBreadCrumb(r)}</h5> <p> <Highlight highlightClassName="highlight" searchWords={query.length > 2 ? query.split(' ') : []} autoEscape textToHighlight={strip(ellipsify(r.content, 200))} /> </p> <span className="url">{r.url}</span> </Link> </Result> ) return ( <Results innerRef={ref => this.results = ref}> {items.length !== 0 && !loading && items} {items.length === 0 && !loading && <Center>No Results Found matching {`"${query}"`}...</Center>} {loading && <span>Loading...</span>} </Results> ) } render () { return ( <Wrapper> <SearchIcon color="#BCBFC1" size={20} /> <Input onChange={this.handleChange} onKeyUp={this.handleKeyUp} value={this.state.query} placeholder="Search documentation..." /> {this.renderResults()} </Wrapper> ) } } export default enhanceClickOutside(Search)