mirror of
https://github.com/PeWu/topola-viewer.git
synced 2026-05-27 07:36:18 +00:00
Added 'embedded mode' for using Topola Viewer inside other apps.
This commit is contained in:
134
src/app.tsx
134
src/app.tsx
@@ -21,6 +21,29 @@ export function ErrorMessage(props: {message: string}) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Message types used in embedded mode.
|
||||||
|
* When the parent is ready to receive messages, it sends PARENT_READY.
|
||||||
|
* When the child (this app) is ready to receive messages, it sends READY.
|
||||||
|
* When the child receives PARENT_READY, it sends READY.
|
||||||
|
* When the parent receives READY, it sends data in a GEDCOM message.
|
||||||
|
*/
|
||||||
|
enum EmbeddedMessageType {
|
||||||
|
GEDCOM = 'gedcom',
|
||||||
|
READY = 'ready',
|
||||||
|
PARENT_READY = 'parent_ready',
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Message sent to parent or received from parent in embedded mode. */
|
||||||
|
interface EmbeddedMessage {
|
||||||
|
message: EmbeddedMessageType;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GedcomMessage extends EmbeddedMessage {
|
||||||
|
message: EmbeddedMessageType.GEDCOM;
|
||||||
|
gedcom?: string;
|
||||||
|
}
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
/** Loaded data. */
|
/** Loaded data. */
|
||||||
data?: TopolaData;
|
data?: TopolaData;
|
||||||
@@ -36,10 +59,12 @@ interface State {
|
|||||||
url?: string;
|
url?: string;
|
||||||
/** Whether the side panel is shoen. */
|
/** Whether the side panel is shoen. */
|
||||||
showSidePanel?: boolean;
|
showSidePanel?: boolean;
|
||||||
|
/** Whether the app is in embedded mode, i.e. embedded in an iframe. */
|
||||||
|
embedded: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class App extends React.Component<RouteComponentProps, {}> {
|
export class App extends React.Component<RouteComponentProps, {}> {
|
||||||
state: State = {loading: false};
|
state: State = {loading: false, embedded: false};
|
||||||
chartRef: Chart | null = null;
|
chartRef: Chart | null = null;
|
||||||
|
|
||||||
private isNewData(
|
private isNewData(
|
||||||
@@ -54,6 +79,62 @@ export class App extends React.Component<RouteComponentProps, {}> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Sets the state with a new individual selection. */
|
||||||
|
private updateSelection(selection: IndiInfo) {
|
||||||
|
if (
|
||||||
|
!this.state.selection ||
|
||||||
|
this.state.selection.id !== selection.id ||
|
||||||
|
this.state.selection!.generation !== selection.generation
|
||||||
|
) {
|
||||||
|
this.setState(
|
||||||
|
Object.assign({}, this.state, {
|
||||||
|
selection,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets error message after data load failure. */
|
||||||
|
private setError(error: string) {
|
||||||
|
this.setState(
|
||||||
|
Object.assign({}, this.state, {
|
||||||
|
error: error,
|
||||||
|
loading: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async onMessage(message: EmbeddedMessage) {
|
||||||
|
if (message.message === EmbeddedMessageType.PARENT_READY) {
|
||||||
|
// Parent didn't receive the first 'ready' message, so we need to send it again.
|
||||||
|
window.parent.postMessage({message: EmbeddedMessageType.READY}, '*');
|
||||||
|
} else if (message.message === EmbeddedMessageType.GEDCOM) {
|
||||||
|
const gedcom = (message as GedcomMessage).gedcom;
|
||||||
|
if (!gedcom) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const data = await loadGedcom('', gedcom);
|
||||||
|
const software = getSoftware(data.gedcom.head);
|
||||||
|
analyticsEvent('embedded_file_loaded', {
|
||||||
|
event_label: software,
|
||||||
|
});
|
||||||
|
// Set state with data.
|
||||||
|
this.setState(
|
||||||
|
Object.assign({}, this.state, {
|
||||||
|
data,
|
||||||
|
selection: getSelection(data.chartData),
|
||||||
|
error: undefined,
|
||||||
|
loading: false,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
analyticsEvent('embedded_file_error');
|
||||||
|
this.setError(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.componentDidUpdate();
|
this.componentDidUpdate();
|
||||||
}
|
}
|
||||||
@@ -62,21 +143,39 @@ export class App extends React.Component<RouteComponentProps, {}> {
|
|||||||
if (this.props.location.pathname !== '/view') {
|
if (this.props.location.pathname !== '/view') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const gedcom = this.props.location.state && this.props.location.state.data;
|
|
||||||
const images =
|
|
||||||
this.props.location.state && this.props.location.state.images;
|
|
||||||
const search = queryString.parse(this.props.location.search);
|
const search = queryString.parse(this.props.location.search);
|
||||||
const getParam = (name: string) => {
|
const getParam = (name: string) => {
|
||||||
const value = search[name];
|
const value = search[name];
|
||||||
return typeof value === 'string' ? value : undefined;
|
return typeof value === 'string' ? value : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const showSidePanel = getParam('sidePanel') !== 'false'; // True by default.
|
||||||
|
const embedded = getParam('embedded') === 'true'; // False by default.
|
||||||
|
|
||||||
|
if (embedded && !this.state.embedded) {
|
||||||
|
this.setState(
|
||||||
|
Object.assign({}, this.state, {embedded: true, showSidePanel}),
|
||||||
|
);
|
||||||
|
// Notify the parent window that we are ready.
|
||||||
|
window.parent.postMessage('ready', '*');
|
||||||
|
window.addEventListener('message', (data) => this.onMessage(data.data));
|
||||||
|
}
|
||||||
|
if (embedded) {
|
||||||
|
// If the app is embedded, do not run the normal loading code.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const url = getParam('url');
|
const url = getParam('url');
|
||||||
const indi = getParam('indi');
|
const indi = getParam('indi');
|
||||||
const parsedGen = Number(getParam('gen'));
|
const parsedGen = Number(getParam('gen'));
|
||||||
const generation = !isNaN(parsedGen) ? parsedGen : undefined;
|
const generation = !isNaN(parsedGen) ? parsedGen : undefined;
|
||||||
const hash = getParam('file');
|
const hash = getParam('file');
|
||||||
const handleCors = getParam('handleCors') !== 'false'; // True by default.
|
const handleCors = getParam('handleCors') !== 'false'; // True by default.
|
||||||
const showSidePanel = getParam('sidePanel') !== 'false'; // True by default.
|
|
||||||
|
const gedcom = this.props.location.state && this.props.location.state.data;
|
||||||
|
const images =
|
||||||
|
this.props.location.state && this.props.location.state.images;
|
||||||
|
|
||||||
if (!url && !hash) {
|
if (!url && !hash) {
|
||||||
this.props.history.replace({pathname: '/'});
|
this.props.history.replace({pathname: '/'});
|
||||||
@@ -117,13 +216,7 @@ export class App extends React.Component<RouteComponentProps, {}> {
|
|||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
analyticsEvent(hash ? 'upload_file_error' : 'url_file_error');
|
analyticsEvent(hash ? 'upload_file_error' : 'url_file_error');
|
||||||
// Set error state.
|
this.setError(error.message);
|
||||||
this.setState(
|
|
||||||
Object.assign({}, this.state, {
|
|
||||||
error: error.message,
|
|
||||||
loading: false,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
} else if (this.state.data && this.state.selection) {
|
} else if (this.state.data && this.state.selection) {
|
||||||
// Update selection if it has changed in the URL.
|
// Update selection if it has changed in the URL.
|
||||||
@@ -132,16 +225,7 @@ export class App extends React.Component<RouteComponentProps, {}> {
|
|||||||
indi,
|
indi,
|
||||||
generation,
|
generation,
|
||||||
);
|
);
|
||||||
if (
|
this.updateSelection(selection);
|
||||||
this.state.selection.id !== selection.id ||
|
|
||||||
this.state.selection.generation !== selection.generation
|
|
||||||
) {
|
|
||||||
this.setState(
|
|
||||||
Object.assign({}, this.state, {
|
|
||||||
selection,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,6 +235,11 @@ export class App extends React.Component<RouteComponentProps, {}> {
|
|||||||
*/
|
*/
|
||||||
private onSelection = (selection: IndiInfo) => {
|
private onSelection = (selection: IndiInfo) => {
|
||||||
analyticsEvent('selection_changed');
|
analyticsEvent('selection_changed');
|
||||||
|
if (this.state.embedded) {
|
||||||
|
// In embedded mode the URL doesn't change.
|
||||||
|
this.updateSelection(selection);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const location = this.props.location;
|
const location = this.props.location;
|
||||||
const search = queryString.parse(location.search);
|
const search = queryString.parse(location.search);
|
||||||
search.indi = selection.id;
|
search.indi = selection.id;
|
||||||
@@ -201,6 +290,7 @@ export class App extends React.Component<RouteComponentProps, {}> {
|
|||||||
this.state.selection
|
this.state.selection
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
embedded={this.state.embedded}
|
||||||
onSelection={this.onSelection}
|
onSelection={this.onSelection}
|
||||||
onPrint={() => {
|
onPrint={() => {
|
||||||
analyticsEvent('print');
|
analyticsEvent('print');
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ interface State {
|
|||||||
interface Props {
|
interface Props {
|
||||||
showingChart: boolean;
|
showingChart: boolean;
|
||||||
gedcom?: GedcomData;
|
gedcom?: GedcomData;
|
||||||
|
embedded: boolean;
|
||||||
onSelection: (indiInfo: IndiInfo) => void;
|
onSelection: (indiInfo: IndiInfo) => void;
|
||||||
onPrint: () => void;
|
onPrint: () => void;
|
||||||
onDownloadPdf: () => void;
|
onDownloadPdf: () => void;
|
||||||
@@ -305,8 +306,8 @@ export class TopBar extends React.Component<
|
|||||||
</>
|
</>
|
||||||
) : null;
|
) : null;
|
||||||
|
|
||||||
return (
|
const fileMenus = this.props.embedded ? null : (
|
||||||
<Menu attached="top" inverted color="blue" size="large">
|
<>
|
||||||
<Link to="/">
|
<Link to="/">
|
||||||
<Menu.Item>
|
<Menu.Item>
|
||||||
<b>Topola Genealogy</b>
|
<b>Topola Genealogy</b>
|
||||||
@@ -336,7 +337,25 @@ export class TopBar extends React.Component<
|
|||||||
/>
|
/>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
</label>
|
</label>
|
||||||
{chartMenus}
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
const sourceLink = this.props.embedded ? (
|
||||||
|
<>
|
||||||
|
<Menu.Item
|
||||||
|
as="a"
|
||||||
|
href="https://pewu.github.com/topola-viewer"
|
||||||
|
position="right"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
<FormattedMessage
|
||||||
|
id="menu.powered_by"
|
||||||
|
defaultMessage="Powered by Topola"
|
||||||
|
/>
|
||||||
|
</Menu.Item>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
<Menu.Item
|
<Menu.Item
|
||||||
as="a"
|
as="a"
|
||||||
href="https://github.com/PeWu/topola-viewer"
|
href="https://github.com/PeWu/topola-viewer"
|
||||||
@@ -348,6 +367,14 @@ export class TopBar extends React.Component<
|
|||||||
defaultMessage="Source on GitHub"
|
defaultMessage="Source on GitHub"
|
||||||
/>
|
/>
|
||||||
</Menu.Item>
|
</Menu.Item>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Menu attached="top" inverted color="blue" size="large">
|
||||||
|
{fileMenus}
|
||||||
|
{chartMenus}
|
||||||
|
{sourceLink}
|
||||||
{loadFromUrlModal}
|
{loadFromUrlModal}
|
||||||
</Menu>
|
</Menu>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
"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.powered_by": "Topola Genealogy",
|
||||||
"menu.search.placeholder": "Szukaj osoby",
|
"menu.search.placeholder": "Szukaj osoby",
|
||||||
"menu.search.no_results": "Brak wyników",
|
"menu.search.no_results": "Brak wyników",
|
||||||
"intro.title": "Topola Genealogy",
|
"intro.title": "Topola Genealogy",
|
||||||
|
|||||||
Reference in New Issue
Block a user