Skip to content

Commit 1ff2bd8

Browse files
committed
feat: add marker
1 parent ecf33d2 commit 1ff2bd8

File tree

8 files changed

+134
-16
lines changed

8 files changed

+134
-16
lines changed

package.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@
8080
"react-dom": "^16.0.0"
8181
},
8282
"dependencies": {
83+
"@types/mark.js": "^8.11.0",
8384
"@types/marked": "^0.3.0",
8485
"classnames": "^2.2.5",
8586
"decko": "^1.2.0",
@@ -88,6 +89,7 @@
8889
"json-pointer": "^0.6.0",
8990
"json-schema-ref-parser": "^4.0.4",
9091
"lunr": "^2.1.5",
92+
"mark.js": "^8.11.1",
9193
"marked": "^0.3.12",
9294
"mobx": "^3.3.0",
9395
"mobx-react": "^4.3.3",

src/components/Redoc/Redoc.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ export class Redoc extends React.Component<RedocProps> {
3232
}
3333

3434
render() {
35-
const { store: { spec, menu, options, search } } = this.props;
35+
const { store: { spec, menu, options, search, marker } } = this.props;
3636
const store = this.props.store;
3737
return (
3838
<ThemeProvider theme={options.theme}>
@@ -42,6 +42,7 @@ export class Redoc extends React.Component<RedocProps> {
4242
<ApiLogo info={spec.info} />
4343
<SearchBox
4444
search={search}
45+
marker={marker}
4546
getItemById={menu.getItemById}
4647
onActivate={menu.activateAndScroll}
4748
/>

src/components/SearchBox/SearchBox.tsx

Lines changed: 36 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { IMenuItem } from '../../services/MenuStore';
66
import { SearchStore } from '../../services/SearchStore';
77
import { MenuItem } from '../SideMenu/MenuItem';
88
import { MenuItemLabel } from '../SideMenu/styled.elements';
9+
import { MarkerService } from '../../services/MarkerService';
10+
import { SearchDocument } from '../../services/SearchWorker.worker';
911

1012
const SearchInput = styled.input.attrs({
1113
className: 'search-input',
@@ -77,6 +79,7 @@ const SearchResultsBox = styled.div.attrs({
7779

7880
export interface SearchBoxProps {
7981
search: SearchStore;
82+
marker: MarkerService;
8083
getItemById: (id: string) => IMenuItem | undefined;
8184
onActivate: (item: IMenuItem) => void;
8285
}
@@ -95,33 +98,52 @@ export class SearchBox extends React.PureComponent<SearchBoxProps, SearchBoxStat
9598
};
9699
}
97100

101+
clearResults(term: string) {
102+
this.setState({
103+
results: [],
104+
term,
105+
});
106+
this.props.marker.unmark();
107+
}
108+
109+
clear() {
110+
this.setState({
111+
results: [],
112+
term: '',
113+
});
114+
this.props.marker.unmark();
115+
}
116+
117+
clearIfEsq = event => {
118+
if (event && event.keyCode === 27) {
119+
this.clear();
120+
}
121+
};
122+
123+
setResults(results: SearchDocument[], term: string) {
124+
this.setState({
125+
results,
126+
term,
127+
});
128+
this.props.marker.mark(term);
129+
}
130+
98131
search = (event: React.ChangeEvent<HTMLInputElement>) => {
99132
const q = event.target.value;
100133
if (q.length < 3) {
101-
this.setState({
102-
term: q,
103-
results: [],
104-
});
134+
this.clearResults(q);
105135
return;
106136
}
137+
107138
this.setState({
108139
term: q,
109140
});
110141

111142
this.props.search.search(event.target.value).then(res => {
112-
this.setState({
113-
results: res,
114-
});
143+
this.setResults(res, q);
115144
});
116145
};
117146

118-
clearIfEsq = event => {
119-
if (event && event.keyCode === 27) {
120-
// escape
121-
this.setState({ term: '', results: [] });
122-
}
123-
};
124-
125147
render() {
126148
const items: IMenuItem[] = this.state.results.map(res => this.props.getItemById(res.id));
127149
items.sort((a, b) => (a.depth > b.depth ? 1 : a.depth < b.depth ? -1 : 0));

src/services/AppStore.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
1+
import { observe } from 'mobx';
2+
13
import { OpenAPISpec } from '../types';
24
import { loadAndBundleSpec } from '../utils/loadAndBundleSpec';
35
import { MenuStore } from './MenuStore';
46
import { SpecStore } from './models';
57
import { RedocNormalizedOptions, RedocRawOptions } from './RedocNormalizedOptions';
68
import { ScrollService } from './ScrollService';
79
import { SearchStore } from './SearchStore';
10+
import { MarkerService } from './MarkerService';
811

912
interface StoreData {
1013
menu: {
@@ -44,8 +47,10 @@ export class AppStore {
4447
rawOptions: RedocRawOptions;
4548
options: RedocNormalizedOptions;
4649
search: SearchStore;
50+
marker = new MarkerService();
4751

4852
private scroll: ScrollService;
53+
private disposer;
4954

5055
constructor(spec: OpenAPISpec, specUrl?: string, options: RedocRawOptions = {}) {
5156
this.rawOptions = options;
@@ -55,11 +60,35 @@ export class AppStore {
5560
this.menu = new MenuStore(this.spec, this.scroll);
5661

5762
this.search = new SearchStore(this.spec);
63+
64+
this.disposer = observe(this.menu, 'activeItemIdx', change => {
65+
this.updateMarkOnMenu(change.newValue as number);
66+
});
67+
}
68+
69+
updateMarkOnMenu(idx: number) {
70+
console.log('update marker');
71+
const start = Math.max(0, idx);
72+
const end = Math.min(this.menu.flatItems.length, start + 5);
73+
74+
const elements: Element[] = [];
75+
for (let i = start; i < end; i++) {
76+
let elem = this.menu.getElementAt(i);
77+
if (!elem) continue;
78+
if (this.menu.flatItems[i].type === 'section') {
79+
elem = elem.parentElement!.parentElement;
80+
}
81+
if (elem) elements.push(elem);
82+
}
83+
84+
this.marker.addOnly(elements);
85+
this.marker.mark();
5886
}
5987

6088
dispose() {
6189
this.scroll.dispose();
6290
this.menu.dispose();
91+
this.disposer();
6392
}
6493

6594
/**

src/services/MarkerService.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import * as Mark from 'mark.js';
2+
3+
export class MarkerService {
4+
map: Map<Element, Mark> = new Map();
5+
6+
private prevTerm: string = '';
7+
8+
add(el: HTMLElement) {
9+
this.map.set(el, new Mark(el));
10+
}
11+
12+
delete(el: Element) {
13+
this.map.delete(el);
14+
}
15+
16+
addOnly(elements: Element[]) {
17+
this.map.forEach((inst, elem) => {
18+
if (elements.indexOf(elem) === -1) {
19+
inst.unmark();
20+
this.map.delete(elem);
21+
}
22+
});
23+
24+
for (let el of elements) {
25+
if (!this.map.has(el)) {
26+
this.map.set(el, new Mark(el as HTMLElement));
27+
}
28+
}
29+
}
30+
31+
clearAll() {
32+
this.unmark();
33+
this.map.clear();
34+
}
35+
36+
mark(term?: string) {
37+
console.log('mark', term);
38+
if (!term && !this.prevTerm) return;
39+
this.map.forEach(val => {
40+
val.unmark();
41+
val.mark(term || this.prevTerm);
42+
});
43+
this.prevTerm = term || this.prevTerm || '';
44+
}
45+
46+
unmark() {
47+
this.map.forEach(val => val.unmark());
48+
this.prevTerm = '';
49+
}
50+
}

src/services/MenuStore.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export class MenuStore {
3737
/**
3838
* active item absolute index (when flattened). -1 means nothing is selected
3939
*/
40-
activeItemIdx: number = -1;
40+
@observable activeItemIdx: number = -1;
4141

4242
/**
4343
* whether sidebar with menu is opened or not

src/services/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,3 +7,7 @@ export * from './SpecStore';
77
export * from './ClipboardService';
88
export * from './HistoryService';
99
export * from './models';
10+
export * from './RedocNormalizedOptions';
11+
export * from './MenuBuilder';
12+
export * from './SearchStore';
13+
export * from './MarkerService';

yarn.lock

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,12 @@
112112
version "2.1.5"
113113
resolved "https://registry.yarnpkg.com/@types/lunr/-/lunr-2.1.5.tgz#afb90226a6d2eb472eb1732cef7493a02b0177fd"
114114

115+
"@types/mark.js@^8.11.0":
116+
version "8.11.0"
117+
resolved "https://registry.yarnpkg.com/@types/mark.js/-/mark.js-8.11.0.tgz#1d507352c30f020a35213f80b5131d8ffba194d7"
118+
dependencies:
119+
"@types/jquery" "*"
120+
115121
"@types/marked@^0.3.0":
116122
version "0.3.0"
117123
resolved "https://registry.yarnpkg.com/@types/marked/-/marked-0.3.0.tgz#583c223dd33385a1dda01aaf77b0cd0411c4b524"
@@ -5098,6 +5104,10 @@ map-visit@^1.0.0:
50985104
dependencies:
50995105
object-visit "^1.0.0"
51005106

5107+
mark.js@^8.11.1:
5108+
version "8.11.1"
5109+
resolved "https://registry.yarnpkg.com/mark.js/-/mark.js-8.11.1.tgz#180f1f9ebef8b0e638e4166ad52db879beb2ffc5"
5110+
51015111
marked@^0.3.12:
51025112
version "0.3.17"
51035113
resolved "https://registry.yarnpkg.com/marked/-/marked-0.3.17.tgz#607f06668b3c6b1246b28f13da76116ac1aa2d2b"

0 commit comments

Comments
 (0)