diff --git a/package.json b/package.json index c9c152e8d3..ee6a341cf8 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,7 @@ "react-dom": "^16.0.0" }, "dependencies": { + "@types/mark.js": "^8.11.0", "@types/marked": "^0.3.0", "classnames": "^2.2.5", "decko": "^1.2.0", @@ -88,6 +89,7 @@ "json-pointer": "^0.6.0", "json-schema-ref-parser": "^4.0.4", "lunr": "^2.1.5", + "mark.js": "^8.11.1", "marked": "^0.3.12", "mobx": "^3.3.0", "mobx-react": "^4.3.3", diff --git a/src/components/Redoc/Redoc.tsx b/src/components/Redoc/Redoc.tsx index bfdc988a32..f6de6a62de 100644 --- a/src/components/Redoc/Redoc.tsx +++ b/src/components/Redoc/Redoc.tsx @@ -32,7 +32,7 @@ export class Redoc extends React.Component { } render() { - const { store: { spec, menu, options, search } } = this.props; + const { store: { spec, menu, options, search, marker } } = this.props; const store = this.props.store; return ( @@ -42,6 +42,7 @@ export class Redoc extends React.Component { diff --git a/src/components/SearchBox/SearchBox.tsx b/src/components/SearchBox/SearchBox.tsx index fc0255d061..3f25fc7a1e 100644 --- a/src/components/SearchBox/SearchBox.tsx +++ b/src/components/SearchBox/SearchBox.tsx @@ -6,6 +6,8 @@ import { IMenuItem } from '../../services/MenuStore'; import { SearchStore } from '../../services/SearchStore'; import { MenuItem } from '../SideMenu/MenuItem'; import { MenuItemLabel } from '../SideMenu/styled.elements'; +import { MarkerService } from '../../services/MarkerService'; +import { SearchDocument } from '../../services/SearchWorker.worker'; const SearchInput = styled.input.attrs({ className: 'search-input', @@ -77,6 +79,7 @@ const SearchResultsBox = styled.div.attrs({ export interface SearchBoxProps { search: SearchStore; + marker: MarkerService; getItemById: (id: string) => IMenuItem | undefined; onActivate: (item: IMenuItem) => void; } @@ -95,33 +98,52 @@ export class SearchBox extends React.PureComponent { + if (event && event.keyCode === 27) { + this.clear(); + } + }; + + setResults(results: SearchDocument[], term: string) { + this.setState({ + results, + term, + }); + this.props.marker.mark(term); + } + search = (event: React.ChangeEvent) => { const q = event.target.value; if (q.length < 3) { - this.setState({ - term: q, - results: [], - }); + this.clearResults(q); return; } + this.setState({ term: q, }); this.props.search.search(event.target.value).then(res => { - this.setState({ - results: res, - }); + this.setResults(res, q); }); }; - clearIfEsq = event => { - if (event && event.keyCode === 27) { - // escape - this.setState({ term: '', results: [] }); - } - }; - render() { const items: IMenuItem[] = this.state.results.map(res => this.props.getItemById(res.id)); items.sort((a, b) => (a.depth > b.depth ? 1 : a.depth < b.depth ? -1 : 0)); diff --git a/src/services/AppStore.ts b/src/services/AppStore.ts index 50cb59911e..2f184d58b0 100644 --- a/src/services/AppStore.ts +++ b/src/services/AppStore.ts @@ -1,3 +1,5 @@ +import { observe } from 'mobx'; + import { OpenAPISpec } from '../types'; import { loadAndBundleSpec } from '../utils/loadAndBundleSpec'; import { MenuStore } from './MenuStore'; @@ -5,6 +7,7 @@ import { SpecStore } from './models'; import { RedocNormalizedOptions, RedocRawOptions } from './RedocNormalizedOptions'; import { ScrollService } from './ScrollService'; import { SearchStore } from './SearchStore'; +import { MarkerService } from './MarkerService'; interface StoreData { menu: { @@ -44,8 +47,10 @@ export class AppStore { rawOptions: RedocRawOptions; options: RedocNormalizedOptions; search: SearchStore; + marker = new MarkerService(); private scroll: ScrollService; + private disposer; constructor(spec: OpenAPISpec, specUrl?: string, options: RedocRawOptions = {}) { this.rawOptions = options; @@ -55,11 +60,35 @@ export class AppStore { this.menu = new MenuStore(this.spec, this.scroll); this.search = new SearchStore(this.spec); + + this.disposer = observe(this.menu, 'activeItemIdx', change => { + this.updateMarkOnMenu(change.newValue as number); + }); + } + + updateMarkOnMenu(idx: number) { + console.log('update marker'); + const start = Math.max(0, idx); + const end = Math.min(this.menu.flatItems.length, start + 5); + + const elements: Element[] = []; + for (let i = start; i < end; i++) { + let elem = this.menu.getElementAt(i); + if (!elem) continue; + if (this.menu.flatItems[i].type === 'section') { + elem = elem.parentElement!.parentElement; + } + if (elem) elements.push(elem); + } + + this.marker.addOnly(elements); + this.marker.mark(); } dispose() { this.scroll.dispose(); this.menu.dispose(); + this.disposer(); } /** diff --git a/src/services/MarkerService.ts b/src/services/MarkerService.ts new file mode 100644 index 0000000000..8c95697471 --- /dev/null +++ b/src/services/MarkerService.ts @@ -0,0 +1,50 @@ +import * as Mark from 'mark.js'; + +export class MarkerService { + map: Map = new Map(); + + private prevTerm: string = ''; + + add(el: HTMLElement) { + this.map.set(el, new Mark(el)); + } + + delete(el: Element) { + this.map.delete(el); + } + + addOnly(elements: Element[]) { + this.map.forEach((inst, elem) => { + if (elements.indexOf(elem) === -1) { + inst.unmark(); + this.map.delete(elem); + } + }); + + for (let el of elements) { + if (!this.map.has(el)) { + this.map.set(el, new Mark(el as HTMLElement)); + } + } + } + + clearAll() { + this.unmark(); + this.map.clear(); + } + + mark(term?: string) { + console.log('mark', term); + if (!term && !this.prevTerm) return; + this.map.forEach(val => { + val.unmark(); + val.mark(term || this.prevTerm); + }); + this.prevTerm = term || this.prevTerm || ''; + } + + unmark() { + this.map.forEach(val => val.unmark()); + this.prevTerm = ''; + } +} diff --git a/src/services/MenuStore.ts b/src/services/MenuStore.ts index 1f1ce04a12..7070c8b1d4 100644 --- a/src/services/MenuStore.ts +++ b/src/services/MenuStore.ts @@ -37,7 +37,7 @@ export class MenuStore { /** * active item absolute index (when flattened). -1 means nothing is selected */ - activeItemIdx: number = -1; + @observable activeItemIdx: number = -1; /** * whether sidebar with menu is opened or not diff --git a/src/services/index.ts b/src/services/index.ts index 516cf99baa..70493d7dea 100644 --- a/src/services/index.ts +++ b/src/services/index.ts @@ -7,3 +7,7 @@ export * from './SpecStore'; export * from './ClipboardService'; export * from './HistoryService'; export * from './models'; +export * from './RedocNormalizedOptions'; +export * from './MenuBuilder'; +export * from './SearchStore'; +export * from './MarkerService'; diff --git a/yarn.lock b/yarn.lock index f908554f3b..6d74ad6357 100644 --- a/yarn.lock +++ b/yarn.lock @@ -112,6 +112,12 @@ version "2.1.5" resolved "https://registry.yarnpkg.com/@types/lunr/-/lunr-2.1.5.tgz#afb90226a6d2eb472eb1732cef7493a02b0177fd" +"@types/mark.js@^8.11.0": + version "8.11.0" + resolved "https://registry.yarnpkg.com/@types/mark.js/-/mark.js-8.11.0.tgz#1d507352c30f020a35213f80b5131d8ffba194d7" + dependencies: + "@types/jquery" "*" + "@types/marked@^0.3.0": version "0.3.0" resolved "https://registry.yarnpkg.com/@types/marked/-/marked-0.3.0.tgz#583c223dd33385a1dda01aaf77b0cd0411c4b524" @@ -5098,6 +5104,10 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" +mark.js@^8.11.1: + version "8.11.1" + resolved "https://registry.yarnpkg.com/mark.js/-/mark.js-8.11.1.tgz#180f1f9ebef8b0e638e4166ad52db879beb2ffc5" + marked@^0.3.12: version "0.3.17" resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.17.tgz#607f06668b3c6b1246b28f13da76116ac1aa2d2b"