mirror of
https://github.com/PeWu/topola-viewer.git
synced 2026-02-18 02:55:48 +00:00
Refactoring: extracted search from top bar code
This commit is contained in:
parent
eb31f13030
commit
aaba839a46
@ -19,12 +19,15 @@ export class MenuItem extends React.Component<
|
|||||||
Props & MenuItemProps & DropdownItemProps
|
Props & MenuItemProps & DropdownItemProps
|
||||||
> {
|
> {
|
||||||
render() {
|
render() {
|
||||||
|
const newProps = {...this.props};
|
||||||
|
// Remove menuType from props to avoid error message in the console.
|
||||||
|
delete newProps.menuType;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{this.props.menuType === MenuType.Menu ? (
|
{this.props.menuType === MenuType.Menu ? (
|
||||||
<Menu.Item {...this.props}>{this.props.children}</Menu.Item>
|
<Menu.Item {...newProps}>{this.props.children}</Menu.Item>
|
||||||
) : (
|
) : (
|
||||||
<Dropdown.Item {...this.props}>{this.props.children}</Dropdown.Item>
|
<Dropdown.Item {...newProps}>{this.props.children}</Dropdown.Item>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
129
src/menu/search.tsx
Normal file
129
src/menu/search.tsx
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import debounce from 'debounce';
|
||||||
|
import {analyticsEvent} from '../util/analytics';
|
||||||
|
import {buildSearchIndex, SearchIndex, SearchResult} from './search_index';
|
||||||
|
import {formatDateOrRange} from '../util/date_util';
|
||||||
|
import {IndiInfo, JsonGedcomData} from 'topola';
|
||||||
|
import {intlShape} from 'react-intl';
|
||||||
|
import {JsonIndi} from 'topola';
|
||||||
|
import {RouteComponentProps} from 'react-router-dom';
|
||||||
|
import {Search, SearchProps, SearchResultProps} from 'semantic-ui-react';
|
||||||
|
|
||||||
|
function getNameLine(result: SearchResult) {
|
||||||
|
const name = [result.indi.firstName, result.indi.lastName].join(' ').trim();
|
||||||
|
if (result.id.length > 8) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{name} <i>({result.id})</i>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
/** Data used for the search index. */
|
||||||
|
data: JsonGedcomData;
|
||||||
|
onSelection: (indiInfo: IndiInfo) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
searchResults: SearchResultProps[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Displays and handles the search box in the top bar. */
|
||||||
|
export class SearchBar extends React.Component<
|
||||||
|
RouteComponentProps & Props,
|
||||||
|
State
|
||||||
|
> {
|
||||||
|
state: State = {
|
||||||
|
searchResults: [],
|
||||||
|
};
|
||||||
|
/** Make intl appear in this.context. */
|
||||||
|
static contextTypes = {
|
||||||
|
intl: intlShape,
|
||||||
|
};
|
||||||
|
|
||||||
|
searchRef?: {setValue(value: string): void};
|
||||||
|
searchIndex?: SearchIndex;
|
||||||
|
|
||||||
|
private getDescriptionLine(indi: JsonIndi) {
|
||||||
|
const birthDate = formatDateOrRange(indi.birth, this.context.intl);
|
||||||
|
const deathDate = formatDateOrRange(indi.death, this.context.intl);
|
||||||
|
if (!deathDate) {
|
||||||
|
return birthDate;
|
||||||
|
}
|
||||||
|
return `${birthDate} – ${deathDate}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Produces an object that is displayed in the Semantic UI Search results. */
|
||||||
|
private displaySearchResult(result: SearchResult) {
|
||||||
|
return {
|
||||||
|
id: result.id,
|
||||||
|
key: result.id,
|
||||||
|
title: getNameLine(result),
|
||||||
|
description: this.getDescriptionLine(result.indi),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** On search input change. */
|
||||||
|
private handleSearch(input: string | undefined) {
|
||||||
|
if (!input) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const results = this.searchIndex!.search(input).map((result) =>
|
||||||
|
this.displaySearchResult(result),
|
||||||
|
);
|
||||||
|
this.setState(Object.assign({}, this.state, {searchResults: results}));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** On search result selected. */
|
||||||
|
private handleResultSelect(id: string) {
|
||||||
|
analyticsEvent('search_result_selected');
|
||||||
|
this.props.onSelection({id, generation: 0});
|
||||||
|
this.searchRef!.setValue('');
|
||||||
|
}
|
||||||
|
|
||||||
|
private initializeSearchIndex() {
|
||||||
|
this.searchIndex = buildSearchIndex(this.props.data);
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.initializeSearchIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: Props) {
|
||||||
|
if (prevProps.data !== this.props.data) {
|
||||||
|
this.initializeSearchIndex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Search
|
||||||
|
onSearchChange={debounce(
|
||||||
|
(_: React.MouseEvent<HTMLElement>, data: SearchProps) =>
|
||||||
|
this.handleSearch(data.value),
|
||||||
|
200,
|
||||||
|
)}
|
||||||
|
onResultSelect={(_, data) => this.handleResultSelect(data.result.id)}
|
||||||
|
results={this.state.searchResults}
|
||||||
|
noResultsMessage={this.context.intl.formatMessage({
|
||||||
|
id: 'menu.search.no_results',
|
||||||
|
defaultMessage: 'No results found',
|
||||||
|
})}
|
||||||
|
placeholder={this.context.intl.formatMessage({
|
||||||
|
id: 'menu.search.placeholder',
|
||||||
|
defaultMessage: 'Search for people',
|
||||||
|
})}
|
||||||
|
selectFirstResult={true}
|
||||||
|
ref={(ref) =>
|
||||||
|
(this.searchRef = (ref as unknown) as {
|
||||||
|
setValue(value: string): void;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
id="search"
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,36 +0,0 @@
|
|||||||
import * as React from 'react';
|
|
||||||
import {formatDateOrRange} from '../util/date_util';
|
|
||||||
import {InjectedIntl} from 'react-intl';
|
|
||||||
import {JsonIndi} from 'topola';
|
|
||||||
import {SearchResult} from './search_index';
|
|
||||||
|
|
||||||
function getNameLine(result: SearchResult) {
|
|
||||||
const name = [result.indi.firstName, result.indi.lastName].join(' ').trim();
|
|
||||||
if (result.id.length > 8) {
|
|
||||||
return name;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{name} <i>({result.id})</i>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDescriptionLine(indi: JsonIndi, intl: InjectedIntl) {
|
|
||||||
const birthDate = formatDateOrRange(indi.birth, intl);
|
|
||||||
const deathDate = formatDateOrRange(indi.death, intl);
|
|
||||||
if (!deathDate) {
|
|
||||||
return birthDate;
|
|
||||||
}
|
|
||||||
return `${birthDate} – ${deathDate}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Produces an object that is displayed in the Semantic UI Search results. */
|
|
||||||
export function displaySearchResult(result: SearchResult, intl: InjectedIntl) {
|
|
||||||
return {
|
|
||||||
id: result.id,
|
|
||||||
key: result.id,
|
|
||||||
title: getNameLine(result),
|
|
||||||
description: getDescriptionLine(result.indi, intl),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@ -1,10 +1,6 @@
|
|||||||
import * as queryString from 'query-string';
|
import * as queryString from 'query-string';
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import debounce from 'debounce';
|
import {FormattedMessage} from 'react-intl';
|
||||||
import {analyticsEvent} from '../util/analytics';
|
|
||||||
import {buildSearchIndex, SearchIndex} from './search_index';
|
|
||||||
import {displaySearchResult} from './search_util';
|
|
||||||
import {FormattedMessage, intlShape} from 'react-intl';
|
|
||||||
import {IndiInfo, JsonGedcomData} from 'topola';
|
import {IndiInfo, JsonGedcomData} from 'topola';
|
||||||
import {Link} from 'react-router-dom';
|
import {Link} from 'react-router-dom';
|
||||||
import {MenuType} from './menu_item';
|
import {MenuType} from './menu_item';
|
||||||
@ -12,26 +8,14 @@ import {RouteComponentProps} from 'react-router-dom';
|
|||||||
import {UploadMenu} from './upload_menu';
|
import {UploadMenu} from './upload_menu';
|
||||||
import {UrlMenu} from './url_menu';
|
import {UrlMenu} from './url_menu';
|
||||||
import {WikiTreeLoginMenu, WikiTreeMenu} from './wikitree_menu';
|
import {WikiTreeLoginMenu, WikiTreeMenu} from './wikitree_menu';
|
||||||
import {
|
import {Icon, Menu, Dropdown, Responsive} from 'semantic-ui-react';
|
||||||
Icon,
|
import {SearchBar} from './search';
|
||||||
Menu,
|
|
||||||
Dropdown,
|
|
||||||
Search,
|
|
||||||
SearchProps,
|
|
||||||
SearchResultProps,
|
|
||||||
Responsive,
|
|
||||||
} from 'semantic-ui-react';
|
|
||||||
|
|
||||||
enum ScreenSize {
|
enum ScreenSize {
|
||||||
LARGE,
|
LARGE,
|
||||||
SMALL,
|
SMALL,
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Menus and dialogs state. */
|
|
||||||
interface State {
|
|
||||||
searchResults: SearchResultProps[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface EventHandlers {
|
interface EventHandlers {
|
||||||
onSelection: (indiInfo: IndiInfo) => void;
|
onSelection: (indiInfo: IndiInfo) => void;
|
||||||
onPrint: () => void;
|
onPrint: () => void;
|
||||||
@ -53,45 +37,7 @@ interface Props {
|
|||||||
showWikiTreeMenus: boolean;
|
showWikiTreeMenus: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TopBar extends React.Component<
|
export class TopBar extends React.Component<RouteComponentProps & Props> {
|
||||||
RouteComponentProps & Props,
|
|
||||||
State
|
|
||||||
> {
|
|
||||||
state: State = {
|
|
||||||
searchResults: [],
|
|
||||||
};
|
|
||||||
/** Make intl appear in this.context. */
|
|
||||||
static contextTypes = {
|
|
||||||
intl: intlShape,
|
|
||||||
};
|
|
||||||
|
|
||||||
searchRef?: {setValue(value: string): void};
|
|
||||||
searchIndex?: SearchIndex;
|
|
||||||
|
|
||||||
/** On search input change. */
|
|
||||||
private handleSearch(input: string | undefined) {
|
|
||||||
if (!input) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const results = this.searchIndex!.search(input).map((result) =>
|
|
||||||
displaySearchResult(result, this.context.intl),
|
|
||||||
);
|
|
||||||
this.setState(Object.assign({}, this.state, {searchResults: results}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** On search result selected. */
|
|
||||||
private handleResultSelect(id: string) {
|
|
||||||
analyticsEvent('search_result_selected');
|
|
||||||
this.props.eventHandlers.onSelection({id, generation: 0});
|
|
||||||
this.searchRef!.setValue('');
|
|
||||||
}
|
|
||||||
|
|
||||||
private initializeSearchIndex() {
|
|
||||||
if (this.props.data) {
|
|
||||||
this.searchIndex = buildSearchIndex(this.props.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private changeView(view: string) {
|
private changeView(view: string) {
|
||||||
const location = this.props.location;
|
const location = this.props.location;
|
||||||
const search = queryString.parse(location.search);
|
const search = queryString.parse(location.search);
|
||||||
@ -102,45 +48,6 @@ export class TopBar extends React.Component<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.initializeSearchIndex();
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate(prevProps: Props) {
|
|
||||||
if (prevProps.data !== this.props.data) {
|
|
||||||
this.initializeSearchIndex();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private search() {
|
|
||||||
return (
|
|
||||||
<Search
|
|
||||||
onSearchChange={debounce(
|
|
||||||
(_: React.MouseEvent<HTMLElement>, data: SearchProps) =>
|
|
||||||
this.handleSearch(data.value),
|
|
||||||
200,
|
|
||||||
)}
|
|
||||||
onResultSelect={(_, data) => this.handleResultSelect(data.result.id)}
|
|
||||||
results={this.state.searchResults}
|
|
||||||
noResultsMessage={this.context.intl.formatMessage({
|
|
||||||
id: 'menu.search.no_results',
|
|
||||||
defaultMessage: 'No results found',
|
|
||||||
})}
|
|
||||||
placeholder={this.context.intl.formatMessage({
|
|
||||||
id: 'menu.search.placeholder',
|
|
||||||
defaultMessage: 'Search for people',
|
|
||||||
})}
|
|
||||||
selectFirstResult={true}
|
|
||||||
ref={(ref) =>
|
|
||||||
(this.searchRef = (ref as unknown) as {
|
|
||||||
setValue(value: string): void;
|
|
||||||
})
|
|
||||||
}
|
|
||||||
id="search"
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private chartMenus(screenSize: ScreenSize) {
|
private chartMenus(screenSize: ScreenSize) {
|
||||||
if (!this.props.showingChart) {
|
if (!this.props.showingChart) {
|
||||||
return null;
|
return null;
|
||||||
@ -232,7 +139,11 @@ export class TopBar extends React.Component<
|
|||||||
>
|
>
|
||||||
<Dropdown.Menu>{chartTypeItems}</Dropdown.Menu>
|
<Dropdown.Menu>{chartTypeItems}</Dropdown.Menu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
{this.search()}
|
<SearchBar
|
||||||
|
data={this.props.data!}
|
||||||
|
onSelection={this.props.eventHandlers.onSelection}
|
||||||
|
{...this.props}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user