mirror of
https://github.com/PeWu/topola-viewer.git
synced 2026-05-06 05:16:16 +00:00
Added search box
This commit is contained in:
@@ -6,10 +6,13 @@
|
|||||||
"array.prototype.flatmap": "^1.2.1",
|
"array.prototype.flatmap": "^1.2.1",
|
||||||
"canvas-toBlob": "^1.0.0",
|
"canvas-toBlob": "^1.0.0",
|
||||||
"d3": "^5.7.0",
|
"d3": "^5.7.0",
|
||||||
|
"debounce": "^1.2.0",
|
||||||
"detect-browser": "^4.1.0",
|
"detect-browser": "^4.1.0",
|
||||||
"file-saver": "^2.0.1",
|
"file-saver": "^2.0.1",
|
||||||
"history": "^4.7.2",
|
"history": "^4.7.2",
|
||||||
|
"javascript-natural-sort": "^0.7.1",
|
||||||
"jspdf": "^1.5.3",
|
"jspdf": "^1.5.3",
|
||||||
|
"lunr": "^2.3.6",
|
||||||
"md5": "^2.2.1",
|
"md5": "^2.2.1",
|
||||||
"parse-gedcom": "^1.0.5",
|
"parse-gedcom": "^1.0.5",
|
||||||
"query-string": "^5.1.1",
|
"query-string": "^5.1.1",
|
||||||
@@ -25,13 +28,15 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/array.prototype.flatmap": "^1.2.0",
|
"@types/array.prototype.flatmap": "^1.2.0",
|
||||||
"@types/d3": "^5.5.0",
|
"@types/d3": "^5.5.0",
|
||||||
|
"@types/debounce": "^1.2.0",
|
||||||
"@types/file-saver": "^2.0.0",
|
"@types/file-saver": "^2.0.0",
|
||||||
"@types/history": "^4.7.2",
|
"@types/history": "^4.7.2",
|
||||||
"@types/jest": "*",
|
"@types/jest": "*",
|
||||||
"@types/jspdf": "^1.2.2",
|
"@types/jspdf": "^1.2.2",
|
||||||
|
"@types/lunr": "^2.3.2",
|
||||||
"@types/md5": "^2.1.33",
|
"@types/md5": "^2.1.33",
|
||||||
"@types/node": "*",
|
"@types/node": "*",
|
||||||
"@types/query-string": "^6.2.0",
|
"@types/query-string": "6.2.0",
|
||||||
"@types/react": "*",
|
"@types/react": "*",
|
||||||
"@types/react-dom": "*",
|
"@types/react-dom": "*",
|
||||||
"@types/react-intl": "^2.3.15",
|
"@types/react-intl": "^2.3.15",
|
||||||
|
|||||||
@@ -190,6 +190,7 @@ export class App extends React.Component<RouteComponentProps, {}> {
|
|||||||
render={(props: RouteComponentProps) => (
|
render={(props: RouteComponentProps) => (
|
||||||
<TopBar
|
<TopBar
|
||||||
{...props}
|
{...props}
|
||||||
|
gedcom={this.state.data && this.state.data.gedcom}
|
||||||
showingChart={
|
showingChart={
|
||||||
!!(
|
!!(
|
||||||
this.props.history.location.pathname === '/view' &&
|
this.props.history.location.pathname === '/view' &&
|
||||||
@@ -197,6 +198,7 @@ export class App extends React.Component<RouteComponentProps, {}> {
|
|||||||
this.state.selection
|
this.state.selection
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
onSelection={this.onSelection}
|
||||||
onPrint={() => {
|
onPrint={() => {
|
||||||
analyticsEvent('print');
|
analyticsEvent('print');
|
||||||
this.chartRef && this.chartRef.print();
|
this.chartRef && this.chartRef.print();
|
||||||
|
|||||||
92
src/date_util.ts
Normal file
92
src/date_util.ts
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
import {Date as TopolaDate, DateRange, getDate} from 'topola';
|
||||||
|
import {InjectedIntl} from 'react-intl';
|
||||||
|
|
||||||
|
const DATE_QUALIFIERS = new Map([
|
||||||
|
['abt', 'about'],
|
||||||
|
['cal', 'calculated'],
|
||||||
|
['est', 'estimated'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
function formatDate(date: TopolaDate, intl: InjectedIntl) {
|
||||||
|
const hasDay = date.day !== undefined;
|
||||||
|
const hasMonth = date.month !== undefined;
|
||||||
|
const hasYear = date.year !== undefined;
|
||||||
|
if (!hasDay && !hasMonth && !hasYear) {
|
||||||
|
return date.text || '';
|
||||||
|
}
|
||||||
|
const dateObject = new Date(
|
||||||
|
hasYear ? date.year! : 0,
|
||||||
|
hasMonth ? date.month! - 1 : 0,
|
||||||
|
hasDay ? date.day! : 1,
|
||||||
|
);
|
||||||
|
|
||||||
|
const qualifier = date.qualifier && date.qualifier.toLowerCase();
|
||||||
|
const translatedQualifier =
|
||||||
|
qualifier &&
|
||||||
|
intl.formatMessage({
|
||||||
|
id: `date.${qualifier}`,
|
||||||
|
defaultMessage: DATE_QUALIFIERS.get(qualifier) || qualifier,
|
||||||
|
});
|
||||||
|
|
||||||
|
const formatOptions = {
|
||||||
|
day: hasDay ? 'numeric' : undefined,
|
||||||
|
month: hasMonth ? 'long' : undefined,
|
||||||
|
year: hasYear ? 'numeric' : undefined,
|
||||||
|
};
|
||||||
|
const translatedDate = new Intl.DateTimeFormat(
|
||||||
|
intl.locale,
|
||||||
|
formatOptions,
|
||||||
|
).format(dateObject);
|
||||||
|
|
||||||
|
return [translatedQualifier, translatedDate].join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDateRage(dateRange: DateRange, intl: InjectedIntl) {
|
||||||
|
const fromDate = dateRange.from;
|
||||||
|
const toDate = dateRange.to;
|
||||||
|
const translatedFromDate = fromDate && formatDate(fromDate, intl);
|
||||||
|
const translatedToDate = toDate && formatDate(toDate, intl);
|
||||||
|
if (translatedFromDate && translatedToDate) {
|
||||||
|
return intl.formatMessage(
|
||||||
|
{
|
||||||
|
id: 'date.between',
|
||||||
|
defaultMessage: 'between {from} and {to}',
|
||||||
|
},
|
||||||
|
{from: translatedFromDate, to: translatedToDate},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (translatedFromDate) {
|
||||||
|
return intl.formatMessage(
|
||||||
|
{
|
||||||
|
id: 'date.after',
|
||||||
|
defaultMessage: 'after {from}',
|
||||||
|
},
|
||||||
|
{from: translatedFromDate},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (translatedToDate) {
|
||||||
|
return intl.formatMessage(
|
||||||
|
{
|
||||||
|
id: 'date.before',
|
||||||
|
defaultMessage: 'before {to}',
|
||||||
|
},
|
||||||
|
{to: translatedToDate},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Formats a date given in GEDCOM format. */
|
||||||
|
export function translateDate(gedcomDate: string, intl: InjectedIntl) {
|
||||||
|
const dateOrRange = getDate(gedcomDate);
|
||||||
|
if (!dateOrRange) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
if (dateOrRange.date) {
|
||||||
|
return formatDate(dateOrRange.date, intl);
|
||||||
|
}
|
||||||
|
if (dateOrRange.dateRange) {
|
||||||
|
return formatDateRage(dateOrRange.dateRange, intl);
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import flatMap from 'array.prototype.flatmap';
|
import flatMap from 'array.prototype.flatmap';
|
||||||
import Linkify from 'react-linkify';
|
import Linkify from 'react-linkify';
|
||||||
import {Date as TopolaDate, DateRange, getDate} from 'topola';
|
|
||||||
import {FormattedMessage, InjectedIntl} from 'react-intl';
|
import {FormattedMessage, InjectedIntl} from 'react-intl';
|
||||||
import {GedcomData} from './gedcom_util';
|
import {GedcomData} from './gedcom_util';
|
||||||
import {GedcomEntry} from 'parse-gedcom';
|
import {GedcomEntry} from 'parse-gedcom';
|
||||||
import {intlShape} from 'react-intl';
|
import {intlShape} from 'react-intl';
|
||||||
|
import {translateDate} from './date_util';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
gedcom: GedcomData;
|
gedcom: GedcomData;
|
||||||
@@ -35,95 +35,6 @@ function translateTag(tag: string) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const DATE_QUALIFIERS = new Map([
|
|
||||||
['abt', 'about'],
|
|
||||||
['cal', 'calculated'],
|
|
||||||
['est', 'estimated'],
|
|
||||||
]);
|
|
||||||
|
|
||||||
function formatDate(date: TopolaDate, intl: InjectedIntl) {
|
|
||||||
const hasDay = date.day !== undefined;
|
|
||||||
const hasMonth = date.month !== undefined;
|
|
||||||
const hasYear = date.year !== undefined;
|
|
||||||
if (!hasDay && !hasMonth && !hasYear) {
|
|
||||||
return date.text || '';
|
|
||||||
}
|
|
||||||
const dateObject = new Date(
|
|
||||||
hasYear ? date.year! : 0,
|
|
||||||
hasMonth ? date.month! - 1 : 0,
|
|
||||||
hasDay ? date.day! : 1,
|
|
||||||
);
|
|
||||||
|
|
||||||
const qualifier = date.qualifier && date.qualifier.toLowerCase();
|
|
||||||
const translatedQualifier =
|
|
||||||
qualifier &&
|
|
||||||
intl.formatMessage({
|
|
||||||
id: `date.${qualifier}`,
|
|
||||||
defaultMessage: DATE_QUALIFIERS.get(qualifier) || qualifier,
|
|
||||||
});
|
|
||||||
|
|
||||||
const formatOptions = {
|
|
||||||
day: hasDay ? 'numeric' : undefined,
|
|
||||||
month: hasMonth ? 'long' : undefined,
|
|
||||||
year: hasYear ? 'numeric' : undefined,
|
|
||||||
};
|
|
||||||
const translatedDate = new Intl.DateTimeFormat(
|
|
||||||
intl.locale,
|
|
||||||
formatOptions,
|
|
||||||
).format(dateObject);
|
|
||||||
|
|
||||||
return [translatedQualifier, translatedDate].join(' ');
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatDateRage(dateRange: DateRange, intl: InjectedIntl) {
|
|
||||||
const fromDate = dateRange.from;
|
|
||||||
const toDate = dateRange.to;
|
|
||||||
const translatedFromDate = fromDate && formatDate(fromDate, intl);
|
|
||||||
const translatedToDate = toDate && formatDate(toDate, intl);
|
|
||||||
if (translatedFromDate && translatedToDate) {
|
|
||||||
return intl.formatMessage(
|
|
||||||
{
|
|
||||||
id: 'date.between',
|
|
||||||
defaultMessage: 'between {from} and {to}',
|
|
||||||
},
|
|
||||||
{from: translatedFromDate, to: translatedToDate},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (translatedFromDate) {
|
|
||||||
return intl.formatMessage(
|
|
||||||
{
|
|
||||||
id: 'date.after',
|
|
||||||
defaultMessage: 'after {from}',
|
|
||||||
},
|
|
||||||
{from: translatedFromDate},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
if (translatedToDate) {
|
|
||||||
return intl.formatMessage(
|
|
||||||
{
|
|
||||||
id: 'date.before',
|
|
||||||
defaultMessage: 'before {to}',
|
|
||||||
},
|
|
||||||
{to: translatedToDate},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function translateDate(gedcomDate: string, intl: InjectedIntl) {
|
|
||||||
const dateOrRange = getDate(gedcomDate);
|
|
||||||
if (!dateOrRange) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
if (dateOrRange.date) {
|
|
||||||
return formatDate(dateOrRange.date, intl);
|
|
||||||
}
|
|
||||||
if (dateOrRange.dateRange) {
|
|
||||||
return formatDateRage(dateOrRange.dateRange, intl);
|
|
||||||
}
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function joinLines(lines: (JSX.Element | string)[]) {
|
function joinLines(lines: (JSX.Element | string)[]) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
4
src/javascript-natural-sort.d.ts
vendored
Normal file
4
src/javascript-natural-sort.d.ts
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
// Data type definitions for the javascript-natural-sort library.
|
||||||
|
declare module 'javascript-natural-sort' {
|
||||||
|
export default function naturalSort(a: string, b: string): number;
|
||||||
|
}
|
||||||
71
src/search_index.ts
Normal file
71
src/search_index.ts
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
import naturalSort from 'javascript-natural-sort';
|
||||||
|
import lunr from 'lunr';
|
||||||
|
import {GedcomData} from './gedcom_util';
|
||||||
|
import {GedcomEntry} from 'parse-gedcom';
|
||||||
|
|
||||||
|
const MAX_RESULTS = 8;
|
||||||
|
|
||||||
|
export interface SearchResult {
|
||||||
|
id: string;
|
||||||
|
indi: GedcomEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchIndex {
|
||||||
|
search(input: string): SearchResult[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Removes accents from letters, e.g. ó->o, ę->e. */
|
||||||
|
function normalize(input: string) {
|
||||||
|
return input
|
||||||
|
.toLocaleLowerCase()
|
||||||
|
.normalize('NFD')
|
||||||
|
.replace(/[\u0300-\u036f]/g, '')
|
||||||
|
.replace(/\u0142/g, 'l'); // Special case: ł is not affected by NFD.
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Comparator to sort by score first, then by id. */
|
||||||
|
function compare(a: lunr.Index.Result, b: lunr.Index.Result) {
|
||||||
|
if (a.score !== b.score) {
|
||||||
|
return a.score - b.score;
|
||||||
|
}
|
||||||
|
return naturalSort(a.ref, b.ref);
|
||||||
|
}
|
||||||
|
|
||||||
|
class LunrSearchIndex implements SearchIndex {
|
||||||
|
private index: lunr.Index;
|
||||||
|
|
||||||
|
public constructor(private gedcom: GedcomData) {
|
||||||
|
this.index = lunr(function() {
|
||||||
|
this.ref('id');
|
||||||
|
this.field('id');
|
||||||
|
this.field('name');
|
||||||
|
this.field('normalizedName');
|
||||||
|
|
||||||
|
for (let id in gedcom.indis) {
|
||||||
|
const name = gedcom.indis[id].tree
|
||||||
|
.filter((entry) => entry.tag === 'NAME')
|
||||||
|
.map((entry) => entry.data)
|
||||||
|
.join(' ');
|
||||||
|
this.add({id, name, normalizedName: normalize(name)});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public search(input: string) {
|
||||||
|
const query = input
|
||||||
|
.split(' ')
|
||||||
|
.filter((s) => !!s)
|
||||||
|
.map((s) => `+${s}*`)
|
||||||
|
.join(' ');
|
||||||
|
const results = this.index.search(query);
|
||||||
|
return results
|
||||||
|
.sort(compare)
|
||||||
|
.slice(0, MAX_RESULTS)
|
||||||
|
.map((result) => ({id: result.ref, indi: this.gedcom.indis[result.ref]}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Builds a search index from data. */
|
||||||
|
export function buildSearchIndex(gedcom: GedcomData): SearchIndex {
|
||||||
|
return new LunrSearchIndex(gedcom);
|
||||||
|
}
|
||||||
48
src/search_util.tsx
Normal file
48
src/search_util.tsx
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import * as React from 'react';
|
||||||
|
import {GedcomEntry} from 'parse-gedcom';
|
||||||
|
import {InjectedIntl} from 'react-intl';
|
||||||
|
import {SearchResult} from './search_index';
|
||||||
|
import {translateDate} from './date_util';
|
||||||
|
|
||||||
|
function getNameLine(result: SearchResult) {
|
||||||
|
const nameTag = result.indi.tree.find((entry) => entry.tag === 'NAME');
|
||||||
|
const name =
|
||||||
|
nameTag &&
|
||||||
|
nameTag.data
|
||||||
|
.split('/')
|
||||||
|
.filter((s) => !!s)
|
||||||
|
.join(' ');
|
||||||
|
if (result.id.length > 8) {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{name} <i>({result.id})</i>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDate(indi: GedcomEntry, tag: string, intl: InjectedIntl) {
|
||||||
|
const eventEntry = indi.tree.find((entry) => entry.tag === tag);
|
||||||
|
const dateEntry =
|
||||||
|
eventEntry && eventEntry.tree.find((entry) => entry.tag === 'DATE');
|
||||||
|
return (dateEntry && translateDate(dateEntry.data, intl)) || '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function getDescriptionLine(indi: GedcomEntry, intl: InjectedIntl) {
|
||||||
|
const birthDate = getDate(indi, 'BIRT', intl);
|
||||||
|
const deathDate = getDate(indi, 'DEAT', 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,
|
||||||
|
title: getNameLine(result),
|
||||||
|
description: getDescriptionLine(result.indi, intl),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
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 md5 from 'md5';
|
import md5 from 'md5';
|
||||||
import {analyticsEvent} from './analytics';
|
import {analyticsEvent} from './analytics';
|
||||||
import {FormattedMessage} from 'react-intl';
|
import {buildSearchIndex, SearchIndex, SearchResult} from './search_index';
|
||||||
|
import {displaySearchResult} from './search_util';
|
||||||
|
import {FormattedMessage, intlShape} from 'react-intl';
|
||||||
|
import {GedcomData} from './gedcom_util';
|
||||||
|
import {IndiInfo} from 'topola';
|
||||||
import {Link} from 'react-router-dom';
|
import {Link} from 'react-router-dom';
|
||||||
import {RouteComponentProps} from 'react-router-dom';
|
import {RouteComponentProps} from 'react-router-dom';
|
||||||
import {
|
import {
|
||||||
@@ -14,16 +19,21 @@ import {
|
|||||||
Input,
|
Input,
|
||||||
Form,
|
Form,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
|
Search,
|
||||||
|
SearchProps,
|
||||||
} from 'semantic-ui-react';
|
} from 'semantic-ui-react';
|
||||||
|
|
||||||
/** Menus and dialogs state. */
|
/** Menus and dialogs state. */
|
||||||
interface State {
|
interface State {
|
||||||
loadUrlDialogOpen: boolean;
|
loadUrlDialogOpen: boolean;
|
||||||
url?: string;
|
url?: string;
|
||||||
|
searchResults: SearchResult[];
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
showingChart: boolean;
|
showingChart: boolean;
|
||||||
|
gedcom?: GedcomData;
|
||||||
|
onSelection: (indiInfo: IndiInfo) => void;
|
||||||
onPrint: () => void;
|
onPrint: () => void;
|
||||||
onDownloadPdf: () => void;
|
onDownloadPdf: () => void;
|
||||||
onDownloadPng: () => void;
|
onDownloadPng: () => void;
|
||||||
@@ -49,8 +59,13 @@ export class TopBar extends React.Component<
|
|||||||
RouteComponentProps & Props,
|
RouteComponentProps & Props,
|
||||||
State
|
State
|
||||||
> {
|
> {
|
||||||
state: State = {loadUrlDialogOpen: false};
|
state: State = {
|
||||||
|
loadUrlDialogOpen: false,
|
||||||
|
searchResults: [],
|
||||||
|
};
|
||||||
inputRef?: Input;
|
inputRef?: Input;
|
||||||
|
searchRef?: {setValue(value: string): void};
|
||||||
|
searchIndex?: SearchIndex;
|
||||||
|
|
||||||
/** Handles the "Upload file" button. */
|
/** Handles the "Upload file" button. */
|
||||||
async handleUpload(event: React.SyntheticEvent<HTMLInputElement>) {
|
async handleUpload(event: React.SyntheticEvent<HTMLInputElement>) {
|
||||||
@@ -135,6 +150,44 @@ export class TopBar extends React.Component<
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** On search input change. */
|
||||||
|
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. */
|
||||||
|
handleResultSelect(id: string) {
|
||||||
|
this.props.onSelection({id, generation: 0});
|
||||||
|
this.searchRef!.setValue('');
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeSearchIndex() {
|
||||||
|
if (this.props.gedcom) {
|
||||||
|
this.searchIndex = buildSearchIndex(this.props.gedcom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidMount() {
|
||||||
|
this.initializeSearchIndex();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentDidUpdate(prevProps: Props) {
|
||||||
|
if (prevProps.gedcom !== this.props.gedcom) {
|
||||||
|
this.initializeSearchIndex();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Make intl appear in this.context. */
|
||||||
|
static contextTypes = {
|
||||||
|
intl: intlShape,
|
||||||
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const loadFromUrlModal = (
|
const loadFromUrlModal = (
|
||||||
<Modal
|
<Modal
|
||||||
@@ -216,6 +269,29 @@ export class TopBar extends React.Component<
|
|||||||
</Dropdown.Item>
|
</Dropdown.Item>
|
||||||
</Dropdown.Menu>
|
</Dropdown.Menu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
<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;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,8 @@
|
|||||||
"menu.png_file": "Plik PNG",
|
"menu.png_file": "Plik PNG",
|
||||||
"menu.svg_file": "Plik SVG",
|
"menu.svg_file": "Plik SVG",
|
||||||
"menu.github": "Źródła na GitHub",
|
"menu.github": "Źródła na GitHub",
|
||||||
|
"menu.search.placeholder": "Szukaj osoby",
|
||||||
|
"menu.search.no_results": "Brak wyników",
|
||||||
"intro.title": "Topola Genealogy",
|
"intro.title": "Topola Genealogy",
|
||||||
"intro.description": "Topola Genealogy pozwala przeglądać drzewo genealogiczne w interaktywny sposób.",
|
"intro.description": "Topola Genealogy pozwala przeglądać drzewo genealogiczne w interaktywny sposób.",
|
||||||
"intro.instructions": "Kliknij OTWÓRZ URL lub OTWÓRZ PLIK, aby załadować plik GEDCOM. Większość programów genealogicznych posiada funkcję eksportu do pliku GEDCOM.",
|
"intro.instructions": "Kliknij OTWÓRZ URL lub OTWÓRZ PLIK, aby załadować plik GEDCOM. Większość programów genealogicznych posiada funkcję eksportu do pliku GEDCOM.",
|
||||||
|
|||||||
Reference in New Issue
Block a user