feat: add marker

This commit is contained in:
Roman Hotsiy 2018-02-22 11:26:53 +02:00
parent ecf33d2dca
commit 1ff2bd84cc
No known key found for this signature in database
GPG Key ID: 5CB7B3ACABA57CB0
8 changed files with 134 additions and 16 deletions

View File

@ -80,6 +80,7 @@
"react-dom": "^16.0.0" "react-dom": "^16.0.0"
}, },
"dependencies": { "dependencies": {
"@types/mark.js": "^8.11.0",
"@types/marked": "^0.3.0", "@types/marked": "^0.3.0",
"classnames": "^2.2.5", "classnames": "^2.2.5",
"decko": "^1.2.0", "decko": "^1.2.0",
@ -88,6 +89,7 @@
"json-pointer": "^0.6.0", "json-pointer": "^0.6.0",
"json-schema-ref-parser": "^4.0.4", "json-schema-ref-parser": "^4.0.4",
"lunr": "^2.1.5", "lunr": "^2.1.5",
"mark.js": "^8.11.1",
"marked": "^0.3.12", "marked": "^0.3.12",
"mobx": "^3.3.0", "mobx": "^3.3.0",
"mobx-react": "^4.3.3", "mobx-react": "^4.3.3",

View File

@ -32,7 +32,7 @@ export class Redoc extends React.Component<RedocProps> {
} }
render() { render() {
const { store: { spec, menu, options, search } } = this.props; const { store: { spec, menu, options, search, marker } } = this.props;
const store = this.props.store; const store = this.props.store;
return ( return (
<ThemeProvider theme={options.theme}> <ThemeProvider theme={options.theme}>
@ -42,6 +42,7 @@ export class Redoc extends React.Component<RedocProps> {
<ApiLogo info={spec.info} /> <ApiLogo info={spec.info} />
<SearchBox <SearchBox
search={search} search={search}
marker={marker}
getItemById={menu.getItemById} getItemById={menu.getItemById}
onActivate={menu.activateAndScroll} onActivate={menu.activateAndScroll}
/> />

View File

@ -6,6 +6,8 @@ import { IMenuItem } from '../../services/MenuStore';
import { SearchStore } from '../../services/SearchStore'; import { SearchStore } from '../../services/SearchStore';
import { MenuItem } from '../SideMenu/MenuItem'; import { MenuItem } from '../SideMenu/MenuItem';
import { MenuItemLabel } from '../SideMenu/styled.elements'; import { MenuItemLabel } from '../SideMenu/styled.elements';
import { MarkerService } from '../../services/MarkerService';
import { SearchDocument } from '../../services/SearchWorker.worker';
const SearchInput = styled.input.attrs({ const SearchInput = styled.input.attrs({
className: 'search-input', className: 'search-input',
@ -77,6 +79,7 @@ const SearchResultsBox = styled.div.attrs({
export interface SearchBoxProps { export interface SearchBoxProps {
search: SearchStore; search: SearchStore;
marker: MarkerService;
getItemById: (id: string) => IMenuItem | undefined; getItemById: (id: string) => IMenuItem | undefined;
onActivate: (item: IMenuItem) => void; onActivate: (item: IMenuItem) => void;
} }
@ -95,33 +98,52 @@ export class SearchBox extends React.PureComponent<SearchBoxProps, SearchBoxStat
}; };
} }
clearResults(term: string) {
this.setState({
results: [],
term,
});
this.props.marker.unmark();
}
clear() {
this.setState({
results: [],
term: '',
});
this.props.marker.unmark();
}
clearIfEsq = event => {
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<HTMLInputElement>) => { search = (event: React.ChangeEvent<HTMLInputElement>) => {
const q = event.target.value; const q = event.target.value;
if (q.length < 3) { if (q.length < 3) {
this.setState({ this.clearResults(q);
term: q,
results: [],
});
return; return;
} }
this.setState({ this.setState({
term: q, term: q,
}); });
this.props.search.search(event.target.value).then(res => { this.props.search.search(event.target.value).then(res => {
this.setState({ this.setResults(res, q);
results: res,
});
}); });
}; };
clearIfEsq = event => {
if (event && event.keyCode === 27) {
// escape
this.setState({ term: '', results: [] });
}
};
render() { render() {
const items: IMenuItem[] = this.state.results.map(res => this.props.getItemById(res.id)); 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)); items.sort((a, b) => (a.depth > b.depth ? 1 : a.depth < b.depth ? -1 : 0));

View File

@ -1,3 +1,5 @@
import { observe } from 'mobx';
import { OpenAPISpec } from '../types'; import { OpenAPISpec } from '../types';
import { loadAndBundleSpec } from '../utils/loadAndBundleSpec'; import { loadAndBundleSpec } from '../utils/loadAndBundleSpec';
import { MenuStore } from './MenuStore'; import { MenuStore } from './MenuStore';
@ -5,6 +7,7 @@ import { SpecStore } from './models';
import { RedocNormalizedOptions, RedocRawOptions } from './RedocNormalizedOptions'; import { RedocNormalizedOptions, RedocRawOptions } from './RedocNormalizedOptions';
import { ScrollService } from './ScrollService'; import { ScrollService } from './ScrollService';
import { SearchStore } from './SearchStore'; import { SearchStore } from './SearchStore';
import { MarkerService } from './MarkerService';
interface StoreData { interface StoreData {
menu: { menu: {
@ -44,8 +47,10 @@ export class AppStore {
rawOptions: RedocRawOptions; rawOptions: RedocRawOptions;
options: RedocNormalizedOptions; options: RedocNormalizedOptions;
search: SearchStore; search: SearchStore;
marker = new MarkerService();
private scroll: ScrollService; private scroll: ScrollService;
private disposer;
constructor(spec: OpenAPISpec, specUrl?: string, options: RedocRawOptions = {}) { constructor(spec: OpenAPISpec, specUrl?: string, options: RedocRawOptions = {}) {
this.rawOptions = options; this.rawOptions = options;
@ -55,11 +60,35 @@ export class AppStore {
this.menu = new MenuStore(this.spec, this.scroll); this.menu = new MenuStore(this.spec, this.scroll);
this.search = new SearchStore(this.spec); 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() { dispose() {
this.scroll.dispose(); this.scroll.dispose();
this.menu.dispose(); this.menu.dispose();
this.disposer();
} }
/** /**

View File

@ -0,0 +1,50 @@
import * as Mark from 'mark.js';
export class MarkerService {
map: Map<Element, Mark> = 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 = '';
}
}

View File

@ -37,7 +37,7 @@ export class MenuStore {
/** /**
* active item absolute index (when flattened). -1 means nothing is selected * 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 * whether sidebar with menu is opened or not

View File

@ -7,3 +7,7 @@ export * from './SpecStore';
export * from './ClipboardService'; export * from './ClipboardService';
export * from './HistoryService'; export * from './HistoryService';
export * from './models'; export * from './models';
export * from './RedocNormalizedOptions';
export * from './MenuBuilder';
export * from './SearchStore';
export * from './MarkerService';

View File

@ -112,6 +112,12 @@
version "2.1.5" version "2.1.5"
resolved "https://registry.yarnpkg.com/@types/lunr/-/lunr-2.1.5.tgz#afb90226a6d2eb472eb1732cef7493a02b0177fd" 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": "@types/marked@^0.3.0":
version "0.3.0" version "0.3.0"
resolved "https://registry.yarnpkg.com/@types/marked/-/marked-0.3.0.tgz#583c223dd33385a1dda01aaf77b0cd0411c4b524" resolved "https://registry.yarnpkg.com/@types/marked/-/marked-0.3.0.tgz#583c223dd33385a1dda01aaf77b0cd0411c4b524"
@ -5098,6 +5104,10 @@ map-visit@^1.0.0:
dependencies: dependencies:
object-visit "^1.0.0" 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: marked@^0.3.12:
version "0.3.17" version "0.3.17"
resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.17.tgz#607f06668b3c6b1246b28f13da76116ac1aa2d2b" resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.17.tgz#607f06668b3c6b1246b28f13da76116ac1aa2d2b"