mirror of
https://github.com/PeWu/topola-viewer.git
synced 2026-07-17 17:21:48 +00:00
Add configurable place display and shortening (#294)
* Add configurable place display and shortening * Fix prettier formatting stuff * Move place shortening logic to Chart
This commit is contained in:
@@ -754,6 +754,8 @@ export function App() {
|
|||||||
colors={config.color}
|
colors={config.color}
|
||||||
hideIds={config.id}
|
hideIds={config.id}
|
||||||
hideSex={config.sex}
|
hideSex={config.sex}
|
||||||
|
placeDisplay={config.place}
|
||||||
|
placeCount={config.placeCount}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
zoomTransform,
|
zoomTransform,
|
||||||
} from 'd3-zoom';
|
} from 'd3-zoom';
|
||||||
import {saveAs} from 'file-saver';
|
import {saveAs} from 'file-saver';
|
||||||
import {useEffect, useRef} from 'react';
|
import {useEffect, useMemo, useRef} from 'react';
|
||||||
import {IntlShape, useIntl} from 'react-intl';
|
import {IntlShape, useIntl} from 'react-intl';
|
||||||
import {
|
import {
|
||||||
ChartHandle,
|
ChartHandle,
|
||||||
@@ -25,8 +25,9 @@ import {
|
|||||||
RelativesChart,
|
RelativesChart,
|
||||||
ChartColors as TopolaChartColors,
|
ChartColors as TopolaChartColors,
|
||||||
} from 'topola';
|
} from 'topola';
|
||||||
import {ChartColors, Ids, Sex} from './sidepanel/config/config';
|
import {ChartColors, Ids, PlaceDisplay, Sex} from './sidepanel/config/config';
|
||||||
import {Media} from './util/media';
|
import {Media} from './util/media';
|
||||||
|
import {DEFAULT_PLACE_DISPLAY_COUNT, shortenPlace} from './util/place_util';
|
||||||
import {usePrevious} from './util/previous-hook';
|
import {usePrevious} from './util/previous-hook';
|
||||||
|
|
||||||
/** How much to zoom when using the +/- buttons. */
|
/** How much to zoom when using the +/- buttons. */
|
||||||
@@ -325,6 +326,8 @@ export interface ChartProps {
|
|||||||
colors?: ChartColors;
|
colors?: ChartColors;
|
||||||
hideIds?: Ids;
|
hideIds?: Ids;
|
||||||
hideSex?: Sex;
|
hideSex?: Sex;
|
||||||
|
placeDisplay?: PlaceDisplay;
|
||||||
|
placeCount?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ChartWrapper {
|
class ChartWrapper {
|
||||||
@@ -495,13 +498,52 @@ export function Chart(props: ChartProps) {
|
|||||||
const prevProps = usePrevious(props);
|
const prevProps = usePrevious(props);
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const placeDisplay = props.placeDisplay ?? PlaceDisplay.FULL;
|
||||||
|
const placeCount = props.placeCount ?? DEFAULT_PLACE_DISPLAY_COUNT;
|
||||||
|
|
||||||
|
const processedData = useMemo(() => {
|
||||||
|
if (placeDisplay === PlaceDisplay.FULL) {
|
||||||
|
return props.data;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...props.data,
|
||||||
|
indis: props.data.indis.map((indi) => ({
|
||||||
|
...indi,
|
||||||
|
birth: indi.birth
|
||||||
|
? {
|
||||||
|
...indi.birth,
|
||||||
|
place: shortenPlace(indi.birth.place, placeDisplay, placeCount),
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
death: indi.death
|
||||||
|
? {
|
||||||
|
...indi.death,
|
||||||
|
place: shortenPlace(indi.death.place, placeDisplay, placeCount),
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
})),
|
||||||
|
fams: props.data.fams.map((fam) => ({
|
||||||
|
...fam,
|
||||||
|
marriage: fam.marriage
|
||||||
|
? {
|
||||||
|
...fam.marriage,
|
||||||
|
place: shortenPlace(fam.marriage.place, placeDisplay, placeCount),
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
}, [props.data, placeDisplay, placeCount]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const propsWithProcessedData: ChartProps = {...props, data: processedData};
|
||||||
if (prevProps) {
|
if (prevProps) {
|
||||||
const initialRender =
|
const initialRender =
|
||||||
props.chartType !== prevProps?.chartType ||
|
props.chartType !== prevProps?.chartType ||
|
||||||
props.colors !== prevProps?.colors ||
|
props.colors !== prevProps?.colors ||
|
||||||
props.hideIds !== prevProps?.hideIds ||
|
props.hideIds !== prevProps?.hideIds ||
|
||||||
props.hideSex !== prevProps?.hideSex;
|
props.hideSex !== prevProps?.hideSex ||
|
||||||
|
props.placeDisplay !== prevProps?.placeDisplay ||
|
||||||
|
props.placeCount !== prevProps?.placeCount;
|
||||||
const resetPosition =
|
const resetPosition =
|
||||||
props.chartType !== prevProps?.chartType ||
|
props.chartType !== prevProps?.chartType ||
|
||||||
props.data !== prevProps.data ||
|
props.data !== prevProps.data ||
|
||||||
@@ -510,12 +552,12 @@ export function Chart(props: ChartProps) {
|
|||||||
// Therefore, compare id and generation instead.
|
// Therefore, compare id and generation instead.
|
||||||
props.selection.id !== prevProps.selection.id ||
|
props.selection.id !== prevProps.selection.id ||
|
||||||
props.selection.generation !== prevProps.selection.generation;
|
props.selection.generation !== prevProps.selection.generation;
|
||||||
chartWrapper.current.renderChart(props, intl, {
|
chartWrapper.current.renderChart(propsWithProcessedData, intl, {
|
||||||
initialRender,
|
initialRender,
|
||||||
resetPosition,
|
resetPosition,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
chartWrapper.current.renderChart(props, intl, {
|
chartWrapper.current.renderChart(propsWithProcessedData, intl, {
|
||||||
initialRender: true,
|
initialRender: true,
|
||||||
resetPosition: true,
|
resetPosition: true,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import {ParsedQuery} from 'query-string';
|
import {ParsedQuery} from 'query-string';
|
||||||
import {FormattedMessage} from 'react-intl';
|
import {FormattedMessage} from 'react-intl';
|
||||||
import {Checkbox, Form, Header, Item} from 'semantic-ui-react';
|
import {Checkbox, Form, Header, Input, Item} from 'semantic-ui-react';
|
||||||
import {GedcomData} from '../../util/gedcom_util';
|
import {GedcomData} from '../../util/gedcom_util';
|
||||||
|
import {DEFAULT_PLACE_DISPLAY_COUNT, PlaceDisplay} from '../../util/place_util';
|
||||||
import {SourceHead} from '../head/head';
|
import {SourceHead} from '../head/head';
|
||||||
|
|
||||||
|
export {PlaceDisplay};
|
||||||
|
|
||||||
export enum ChartColors {
|
export enum ChartColors {
|
||||||
NO_COLOR,
|
NO_COLOR,
|
||||||
COLOR_BY_SEX,
|
COLOR_BY_SEX,
|
||||||
@@ -24,12 +27,16 @@ export interface Config {
|
|||||||
color: ChartColors;
|
color: ChartColors;
|
||||||
id: Ids;
|
id: Ids;
|
||||||
sex: Sex;
|
sex: Sex;
|
||||||
|
place: PlaceDisplay;
|
||||||
|
placeCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DEFALUT_CONFIG: Config = {
|
export const DEFALUT_CONFIG: Config = {
|
||||||
color: ChartColors.COLOR_BY_GENERATION,
|
color: ChartColors.COLOR_BY_GENERATION,
|
||||||
id: Ids.SHOW,
|
id: Ids.SHOW,
|
||||||
sex: Sex.SHOW,
|
sex: Sex.SHOW,
|
||||||
|
place: PlaceDisplay.FULL,
|
||||||
|
placeCount: DEFAULT_PLACE_DISPLAY_COUNT,
|
||||||
};
|
};
|
||||||
|
|
||||||
const COLOR_ARG = new Map<string, ChartColors>([
|
const COLOR_ARG = new Map<string, ChartColors>([
|
||||||
@@ -54,16 +61,27 @@ const SEX_ARG = new Map<string, Sex>([
|
|||||||
const SEX_ARG_INVERSE = new Map<Sex, string>();
|
const SEX_ARG_INVERSE = new Map<Sex, string>();
|
||||||
SEX_ARG.forEach((v, k) => SEX_ARG_INVERSE.set(v, k));
|
SEX_ARG.forEach((v, k) => SEX_ARG_INVERSE.set(v, k));
|
||||||
|
|
||||||
|
const PLACE_ARG = new Map<string, PlaceDisplay>([
|
||||||
|
['f', PlaceDisplay.FULL],
|
||||||
|
['s', PlaceDisplay.SHORT],
|
||||||
|
['h', PlaceDisplay.HIDE],
|
||||||
|
]);
|
||||||
|
const PLACE_ARG_INVERSE = new Map<PlaceDisplay, string>();
|
||||||
|
PLACE_ARG.forEach((v, k) => PLACE_ARG_INVERSE.set(v, k));
|
||||||
|
|
||||||
export function argsToConfig(args: ParsedQuery<unknown>): Config {
|
export function argsToConfig(args: ParsedQuery<unknown>): Config {
|
||||||
const getParam = (name: string) => {
|
const getParam = (name: string) => {
|
||||||
const value = args[name];
|
const value = args[name];
|
||||||
return typeof value === 'string' ? value : undefined;
|
return typeof value === 'string' ? value : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const placeCount = parseInt(getParam('pn') ?? '', 10);
|
||||||
return {
|
return {
|
||||||
color: COLOR_ARG.get(getParam('c') ?? '') ?? DEFALUT_CONFIG.color,
|
color: COLOR_ARG.get(getParam('c') ?? '') ?? DEFALUT_CONFIG.color,
|
||||||
id: ID_ARG.get(getParam('i') ?? '') ?? DEFALUT_CONFIG.id,
|
id: ID_ARG.get(getParam('i') ?? '') ?? DEFALUT_CONFIG.id,
|
||||||
sex: SEX_ARG.get(getParam('s') ?? '') ?? DEFALUT_CONFIG.sex,
|
sex: SEX_ARG.get(getParam('s') ?? '') ?? DEFALUT_CONFIG.sex,
|
||||||
|
place: PLACE_ARG.get(getParam('p') ?? '') ?? DEFALUT_CONFIG.place,
|
||||||
|
placeCount: placeCount >= 1 ? placeCount : DEFALUT_CONFIG.placeCount,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,6 +99,16 @@ export function configToArgs(config: Config): ParsedQuery {
|
|||||||
if (sex) {
|
if (sex) {
|
||||||
result.s = sex;
|
result.s = sex;
|
||||||
}
|
}
|
||||||
|
const place = PLACE_ARG_INVERSE.get(config.place);
|
||||||
|
if (place && config.place !== PlaceDisplay.FULL) {
|
||||||
|
result.p = place;
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
config.place === PlaceDisplay.SHORT &&
|
||||||
|
config.placeCount !== DEFALUT_CONFIG.placeCount
|
||||||
|
) {
|
||||||
|
result.pn = String(config.placeCount);
|
||||||
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,6 +280,83 @@ export function ConfigPanel(props: {
|
|||||||
</Form.Field>
|
</Form.Field>
|
||||||
</Item.Content>
|
</Item.Content>
|
||||||
</Item>
|
</Item>
|
||||||
|
<Item>
|
||||||
|
<Item.Content>
|
||||||
|
<Header sub>
|
||||||
|
<FormattedMessage id="config.places" defaultMessage="Places" />
|
||||||
|
</Header>
|
||||||
|
<Form.Field className="no-margin">
|
||||||
|
<Checkbox
|
||||||
|
radio
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
tagName="label"
|
||||||
|
id="config.places.FULL"
|
||||||
|
defaultMessage="full"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
name="checkboxRadioGroup"
|
||||||
|
value="full"
|
||||||
|
checked={props.config.place === PlaceDisplay.FULL}
|
||||||
|
onClick={() =>
|
||||||
|
props.onChange({...props.config, place: PlaceDisplay.FULL})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Form.Field>
|
||||||
|
<Form.Field className="no-margin">
|
||||||
|
<Checkbox
|
||||||
|
radio
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
tagName="label"
|
||||||
|
id="config.places.SHORT"
|
||||||
|
defaultMessage="short"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
name="checkboxRadioGroup"
|
||||||
|
value="short"
|
||||||
|
checked={props.config.place === PlaceDisplay.SHORT}
|
||||||
|
onClick={() =>
|
||||||
|
props.onChange({...props.config, place: PlaceDisplay.SHORT})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{props.config.place === PlaceDisplay.SHORT && (
|
||||||
|
<Input
|
||||||
|
type="number"
|
||||||
|
min={1}
|
||||||
|
max={10}
|
||||||
|
size="mini"
|
||||||
|
style={{width: '4em', marginLeft: '1.5em'}}
|
||||||
|
value={props.config.placeCount}
|
||||||
|
onChange={(_e, {value}) => {
|
||||||
|
const n = parseInt(value, 10);
|
||||||
|
if (n >= 1) {
|
||||||
|
props.onChange({...props.config, placeCount: n});
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Form.Field>
|
||||||
|
<Form.Field className="no-margin">
|
||||||
|
<Checkbox
|
||||||
|
radio
|
||||||
|
label={
|
||||||
|
<FormattedMessage
|
||||||
|
tagName="label"
|
||||||
|
id="config.places.HIDE"
|
||||||
|
defaultMessage="hide"
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
name="checkboxRadioGroup"
|
||||||
|
value="hide"
|
||||||
|
checked={props.config.place === PlaceDisplay.HIDE}
|
||||||
|
onClick={() =>
|
||||||
|
props.onChange({...props.config, place: PlaceDisplay.HIDE})
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Form.Field>
|
||||||
|
</Item.Content>
|
||||||
|
</Item>
|
||||||
</Item.Group>
|
</Item.Group>
|
||||||
</Form>
|
</Form>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -129,6 +129,10 @@
|
|||||||
"config.sex": "Пол",
|
"config.sex": "Пол",
|
||||||
"config.sex.HIDE": "Скриване",
|
"config.sex.HIDE": "Скриване",
|
||||||
"config.sex.SHOW": "Показване",
|
"config.sex.SHOW": "Показване",
|
||||||
|
"config.places": "Места",
|
||||||
|
"config.places.FULL": "пълно",
|
||||||
|
"config.places.SHORT": "съкратено",
|
||||||
|
"config.places.HIDE": "скриване",
|
||||||
"head.source": "Източник на данни",
|
"head.source": "Източник на данни",
|
||||||
"name.unknown_name": "Неизвестно име",
|
"name.unknown_name": "Неизвестно име",
|
||||||
"extras.images": "Изображение",
|
"extras.images": "Изображение",
|
||||||
|
|||||||
@@ -129,6 +129,10 @@
|
|||||||
"config.sex": "Pohlaví",
|
"config.sex": "Pohlaví",
|
||||||
"config.sex.HIDE": "skrýt",
|
"config.sex.HIDE": "skrýt",
|
||||||
"config.sex.SHOW": "zobrazit",
|
"config.sex.SHOW": "zobrazit",
|
||||||
|
"config.places": "Místa",
|
||||||
|
"config.places.FULL": "plný",
|
||||||
|
"config.places.SHORT": "zkrácený",
|
||||||
|
"config.places.HIDE": "skrýt",
|
||||||
"head.source": "Zdroj dat",
|
"head.source": "Zdroj dat",
|
||||||
"name.unknown_name": "N.N.",
|
"name.unknown_name": "N.N.",
|
||||||
"extras.images": "Obrázky",
|
"extras.images": "Obrázky",
|
||||||
|
|||||||
@@ -129,6 +129,10 @@
|
|||||||
"config.sex": "Geschlecht",
|
"config.sex": "Geschlecht",
|
||||||
"config.sex.HIDE": "verbergen",
|
"config.sex.HIDE": "verbergen",
|
||||||
"config.sex.SHOW": "anzeigen",
|
"config.sex.SHOW": "anzeigen",
|
||||||
|
"config.places": "Orte",
|
||||||
|
"config.places.FULL": "vollständig",
|
||||||
|
"config.places.SHORT": "gekürzt",
|
||||||
|
"config.places.HIDE": "verbergen",
|
||||||
"head.source": "Datenquelle",
|
"head.source": "Datenquelle",
|
||||||
"name.unknown_name": "N.N.",
|
"name.unknown_name": "N.N.",
|
||||||
"extras.images": "Bilder",
|
"extras.images": "Bilder",
|
||||||
|
|||||||
@@ -129,6 +129,10 @@
|
|||||||
"config.sex": "Sexe",
|
"config.sex": "Sexe",
|
||||||
"config.sex.HIDE": "cacher",
|
"config.sex.HIDE": "cacher",
|
||||||
"config.sex.SHOW": "afficher",
|
"config.sex.SHOW": "afficher",
|
||||||
|
"config.places": "Lieux",
|
||||||
|
"config.places.FULL": "complet",
|
||||||
|
"config.places.SHORT": "abrégé",
|
||||||
|
"config.places.HIDE": "cacher",
|
||||||
"head.source": "Source de données",
|
"head.source": "Source de données",
|
||||||
"name.unknown_name": "?",
|
"name.unknown_name": "?",
|
||||||
"extras.images": "Images",
|
"extras.images": "Images",
|
||||||
|
|||||||
@@ -129,6 +129,10 @@
|
|||||||
"config.sex": "Sesso",
|
"config.sex": "Sesso",
|
||||||
"config.sex.HIDE": "nascondere",
|
"config.sex.HIDE": "nascondere",
|
||||||
"config.sex.SHOW": "visualizzare",
|
"config.sex.SHOW": "visualizzare",
|
||||||
|
"config.places": "Luoghi",
|
||||||
|
"config.places.FULL": "completo",
|
||||||
|
"config.places.SHORT": "abbreviato",
|
||||||
|
"config.places.HIDE": "nascondere",
|
||||||
"head.source": "Origine dati",
|
"head.source": "Origine dati",
|
||||||
"name.unknown_name": "N.N.",
|
"name.unknown_name": "N.N.",
|
||||||
"extras.images": "Immagini",
|
"extras.images": "Immagini",
|
||||||
|
|||||||
@@ -129,6 +129,10 @@
|
|||||||
"config.sex": "Płeć",
|
"config.sex": "Płeć",
|
||||||
"config.sex.HIDE": "ukryj",
|
"config.sex.HIDE": "ukryj",
|
||||||
"config.sex.SHOW": "pokaż",
|
"config.sex.SHOW": "pokaż",
|
||||||
|
"config.places": "Miejsca",
|
||||||
|
"config.places.FULL": "pełne",
|
||||||
|
"config.places.SHORT": "skrócone",
|
||||||
|
"config.places.HIDE": "ukryj",
|
||||||
"head.source": "Źródło danych",
|
"head.source": "Źródło danych",
|
||||||
"name.unknown_name": "N.N.",
|
"name.unknown_name": "N.N.",
|
||||||
"extras.images": "Zdjęcia",
|
"extras.images": "Zdjęcia",
|
||||||
|
|||||||
@@ -129,6 +129,10 @@
|
|||||||
"config.sex": "Пол",
|
"config.sex": "Пол",
|
||||||
"config.sex.HIDE": "Скрыть",
|
"config.sex.HIDE": "Скрыть",
|
||||||
"config.sex.SHOW": "Показать",
|
"config.sex.SHOW": "Показать",
|
||||||
|
"config.places": "Места",
|
||||||
|
"config.places.FULL": "полное",
|
||||||
|
"config.places.SHORT": "сокращённое",
|
||||||
|
"config.places.HIDE": "скрыть",
|
||||||
"head.source": "Источник данных",
|
"head.source": "Источник данных",
|
||||||
"name.unknown_name": "Неизвестно",
|
"name.unknown_name": "Неизвестно",
|
||||||
"extras.images": "Картинки",
|
"extras.images": "Картинки",
|
||||||
|
|||||||
50
src/util/place_util.spec.ts
Normal file
50
src/util/place_util.spec.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import {describe, expect, it} from '@jest/globals';
|
||||||
|
import {PlaceDisplay, shortenPlace} from './place_util';
|
||||||
|
|
||||||
|
const LONG = 'Cyclone, Keating Township, McKean, Pennsylvania, United States';
|
||||||
|
|
||||||
|
describe('shortenPlace', () => {
|
||||||
|
it('returns full place unchanged in FULL mode', () => {
|
||||||
|
expect(shortenPlace(LONG, PlaceDisplay.FULL)).toBe(LONG);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('returns undefined in HIDE mode', () => {
|
||||||
|
expect(shortenPlace(LONG, PlaceDisplay.HIDE)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('defaults to 2 components in SHORT mode', () => {
|
||||||
|
expect(shortenPlace(LONG, PlaceDisplay.SHORT)).toBe(
|
||||||
|
'Cyclone, Keating Township',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps first N components when count is specified', () => {
|
||||||
|
expect(shortenPlace(LONG, PlaceDisplay.SHORT, 1)).toBe('Cyclone');
|
||||||
|
expect(shortenPlace(LONG, PlaceDisplay.SHORT, 3)).toBe(
|
||||||
|
'Cyclone, Keating Township, McKean',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves place unchanged when it has fewer parts than count', () => {
|
||||||
|
expect(shortenPlace('Berlin, Germany', PlaceDisplay.SHORT, 3)).toBe(
|
||||||
|
'Berlin, Germany',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('leaves single-component place unchanged in SHORT mode', () => {
|
||||||
|
expect(shortenPlace('Germany', PlaceDisplay.SHORT)).toBe('Germany');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles undefined place', () => {
|
||||||
|
expect(shortenPlace(undefined, PlaceDisplay.SHORT)).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('handles empty string', () => {
|
||||||
|
expect(shortenPlace('', PlaceDisplay.SHORT)).toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('is idempotent — applying SHORT twice with same count gives same result', () => {
|
||||||
|
const once = shortenPlace(LONG, PlaceDisplay.SHORT, 2);
|
||||||
|
expect(shortenPlace(once, PlaceDisplay.SHORT, 2)).toBe(once);
|
||||||
|
});
|
||||||
|
});
|
||||||
36
src/util/place_util.ts
Normal file
36
src/util/place_util.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
export enum PlaceDisplay {
|
||||||
|
FULL,
|
||||||
|
SHORT,
|
||||||
|
HIDE,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_PLACE_DISPLAY_COUNT = 2;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shortens a GEDCOM place string for display in chart nodes.
|
||||||
|
*
|
||||||
|
* GEDCOM places are comma-separated from most-specific to least-specific,
|
||||||
|
* e.g. "Cyclone, McKean, Pennsylvania, United States".
|
||||||
|
* SHORT keeps the first `count` components (most-specific).
|
||||||
|
* HIDE removes the place entirely.
|
||||||
|
*/
|
||||||
|
export function shortenPlace(
|
||||||
|
place: string | undefined,
|
||||||
|
mode: PlaceDisplay,
|
||||||
|
count: number = DEFAULT_PLACE_DISPLAY_COUNT,
|
||||||
|
): string | undefined {
|
||||||
|
if (!place || mode === PlaceDisplay.FULL) {
|
||||||
|
return place;
|
||||||
|
}
|
||||||
|
if (mode === PlaceDisplay.HIDE) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
const parts = place
|
||||||
|
.split(',')
|
||||||
|
.map((p) => p.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
if (parts.length <= count) {
|
||||||
|
return place;
|
||||||
|
}
|
||||||
|
return parts.slice(0, count).join(', ');
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user