From 9a02840dec5cf36d271ca0125e3248fe36359e27 Mon Sep 17 00:00:00 2001 From: Przemek Wiech Date: Mon, 11 Mar 2019 23:23:08 +0100 Subject: [PATCH] Updates --- asset-manifest.json | 6 +++--- index.html | 2 +- ... precache-manifest.3ad6da03725342bb5af2b85150189a83.js | 8 ++++---- service-worker.js | 2 +- static/js/main.cf40cb68.chunk.js | 2 ++ static/js/main.cf40cb68.chunk.js.map | 1 + static/js/main.d0a22e0b.chunk.js | 2 -- static/js/main.d0a22e0b.chunk.js.map | 1 - 8 files changed, 12 insertions(+), 12 deletions(-) rename precache-manifest.26d25c0868f6e6db86c2c7b77a877585.js => precache-manifest.3ad6da03725342bb5af2b85150189a83.js (92%) create mode 100644 static/js/main.cf40cb68.chunk.js create mode 100644 static/js/main.cf40cb68.chunk.js.map delete mode 100644 static/js/main.d0a22e0b.chunk.js delete mode 100644 static/js/main.d0a22e0b.chunk.js.map diff --git a/asset-manifest.json b/asset-manifest.json index 3775786..1e96ebd 100644 --- a/asset-manifest.json +++ b/asset-manifest.json @@ -1,7 +1,7 @@ { "main.css": "/topola-viewer/static/css/main.1f0a838c.chunk.css", - "main.js": "/topola-viewer/static/js/main.d0a22e0b.chunk.js", - "main.js.map": "/topola-viewer/static/js/main.d0a22e0b.chunk.js.map", + "main.js": "/topola-viewer/static/js/main.cf40cb68.chunk.js", + "main.js.map": "/topola-viewer/static/js/main.cf40cb68.chunk.js.map", "static/css/1.99ae5474.chunk.css": "/topola-viewer/static/css/1.99ae5474.chunk.css", "static/js/1.f3b55c9e.chunk.js": "/topola-viewer/static/js/1.f3b55c9e.chunk.js", "static/js/1.f3b55c9e.chunk.js.map": "/topola-viewer/static/js/1.f3b55c9e.chunk.js.map", @@ -11,6 +11,6 @@ "static/css/main.1f0a838c.chunk.css.map": "/topola-viewer/static/css/main.1f0a838c.chunk.css.map", "static/css/1.99ae5474.chunk.css.map": "/topola-viewer/static/css/1.99ae5474.chunk.css.map", "index.html": "/topola-viewer/index.html", - "precache-manifest.26d25c0868f6e6db86c2c7b77a877585.js": "/topola-viewer/precache-manifest.26d25c0868f6e6db86c2c7b77a877585.js", + "precache-manifest.3ad6da03725342bb5af2b85150189a83.js": "/topola-viewer/precache-manifest.3ad6da03725342bb5af2b85150189a83.js", "service-worker.js": "/topola-viewer/service-worker.js" } \ No newline at end of file diff --git a/index.html b/index.html index ab27939..63fbbe5 100644 --- a/index.html +++ b/index.html @@ -1 +1 @@ -Topola Genealogy Viewer
\ No newline at end of file +Topola Genealogy Viewer
\ No newline at end of file diff --git a/precache-manifest.26d25c0868f6e6db86c2c7b77a877585.js b/precache-manifest.3ad6da03725342bb5af2b85150189a83.js similarity index 92% rename from precache-manifest.26d25c0868f6e6db86c2c7b77a877585.js rename to precache-manifest.3ad6da03725342bb5af2b85150189a83.js index 51b85ac..4422148 100644 --- a/precache-manifest.26d25c0868f6e6db86c2c7b77a877585.js +++ b/precache-manifest.3ad6da03725342bb5af2b85150189a83.js @@ -4,7 +4,7 @@ self.__precacheManifest = [ "url": "/topola-viewer/static/media/icons.faff9214.woff" }, { - "revision": "d0a22e0b4f1b8fb67184", + "revision": "cf40cb681ffaa3827a6b", "url": "/topola-viewer/static/css/main.1f0a838c.chunk.css" }, { @@ -44,8 +44,8 @@ self.__precacheManifest = [ "url": "/topola-viewer/static/media/icons.0ab54153.woff2" }, { - "revision": "d0a22e0b4f1b8fb67184", - "url": "/topola-viewer/static/js/main.d0a22e0b.chunk.js" + "revision": "cf40cb681ffaa3827a6b", + "url": "/topola-viewer/static/js/main.cf40cb68.chunk.js" }, { "revision": "e8c322de9658cbeb8a774b6624167c2c", @@ -84,7 +84,7 @@ self.__precacheManifest = [ "url": "/topola-viewer/static/css/1.99ae5474.chunk.css" }, { - "revision": "5510cb5608c60ba545605604d94b307e", + "revision": "b0395a41135eb890d4425c234346a894", "url": "/topola-viewer/index.html" } ]; \ No newline at end of file diff --git a/service-worker.js b/service-worker.js index 26787a5..28a2f63 100644 --- a/service-worker.js +++ b/service-worker.js @@ -14,7 +14,7 @@ importScripts("https://storage.googleapis.com/workbox-cdn/releases/3.6.3/workbox-sw.js"); importScripts( - "/topola-viewer/precache-manifest.26d25c0868f6e6db86c2c7b77a877585.js" + "/topola-viewer/precache-manifest.3ad6da03725342bb5af2b85150189a83.js" ); workbox.clientsClaim(); diff --git a/static/js/main.cf40cb68.chunk.js b/static/js/main.cf40cb68.chunk.js new file mode 100644 index 0000000..ea31ae2 --- /dev/null +++ b/static/js/main.cf40cb68.chunk.js @@ -0,0 +1,2 @@ +(window.webpackJsonp=window.webpackJsonp||[]).push([[0],{241:function(e){e.exports={"menu.load_from_url":"Otw\xf3rz URL","menu.load_from_file":"Otw\xf3rz plik","menu.print":"Drukuj","menu.download":"Pobierz","menu.pdf_file":"Plik PDF","menu.png_file":"Plik PNG","menu.svg_file":"Plik SVG","menu.github":"\u0179r\xf3d\u0142a na GitHub","intro.title":"Topola Genealogy","intro.description":"Topola Genealogy pozwala przegl\u0105da\u0107 drzewo genealogiczne w interaktywny spos\xf3b.","intro.instructions":"Kliknij OTW\xd3RZ URL lub OTW\xd3RZ PLIK, aby za\u0142adowa\u0107 plik GEDCOM. Wi\u0119kszo\u015b\u0107 program\xf3w genealogicznych posiada funkcj\u0119 eksportu do pliku GEDCOM.","intro.examples":"Poni\u017cej jest kilka przyk\u0142ad\xf3w znalezionych w Internecie:","intro.from":"\u017ar\xf3d\u0142o:","intro.privacy":"Prywatno\u015b\u0107","intro.privacy_note":'U\u017cywaj\u0105c funkcji "Otw\xf3rz plik", Twoje dane nie s\u0105 nigdzie wysy\u0142ane i pozostaj\u0105 na Twoim komputerze. U\u017cywaj\u0105c funkcji "Otw\xf3rz URL", dane z podanego adresu przesy\u0142ane s\u0105 przez us\u0142ug\u0119 {link} w celu umo\u017cliwienia za\u0142adowania danych z innej domeny (CORS).',"load_from_url.title":"Otw\xf3rz z adresu URL","load_from_url.comment":"Dane z podanego adresu URL zostan\u0105 za\u0142adowane poprzez us\u0142ug\u0119 {link} w celu unikni\u0119cia problem\xf3w z CORS.","load_from_url.cancel":"Anuluj","load_from_url.load":"Otw\xf3rz","gedcom.BAPM":"Chrzest","gedcom.BIRT":"Narodziny","gedcom.BURI":"Pogrzeb","gedcom.CHR":"Chrzest","gedcom.DEAT":"\u015amier\u0107","gedcom.EMAIL":"E-mail","gedcom.OCCU":"Zaw\xf3d","gedcom.TITL":"Tytu\u0142","gedcom.WWW":"Strona WWW"}},242:function(e,t){},281:function(e,t,n){e.exports=n(505)},293:function(e,t){},299:function(e,t){},499:function(e,t,n){},505:function(e,t,n){"use strict";n.r(t);var a=n(152),r=n(239),o=n(240),i=n(0),l=n(61),c=n(241),s=n(15),u=n(30),d=n(31),m=n(33),f=n(32),h=n(34),p=n(41),g=n(17),E=n(244),v=n.n(E),w=n(149),y=n(42);function b(){var e=g.select("#chart").node().parentElement;e.scrollLeft=-g.event.transform.x,e.scrollTop=-g.event.transform.y}function O(){var e=g.select("#chart").node().parentElement,t=e.scrollLeft+e.clientWidth/2,n=e.scrollTop+e.clientHeight/2;g.select(e).call(g.zoom().translateTo,t,n)}function k(e){var t=new FileReader;return t.readAsDataURL(e),new Promise(function(e,n){t.onload=function(t){return e(t.target.result)}})}function C(e){var t=new Image;return t.src=URL.createObjectURL(e),new Promise(function(e,n){t.addEventListener("load",function(){e(t)})})}function j(e){var t=document.createElement("canvas");t.width=2*e.width,t.height=2*e.height;var n=t.getContext("2d"),a=n.fillStyle;return n.fillStyle="white",n.fillRect(0,0,t.width,t.height),n.fillStyle=a,n.drawImage(e,0,0,t.width,t.height),t}var R=function(e){function t(){var e,n;Object(u.a)(this,t);for(var a=arguments.length,r=new Array(a),o=0;o0&&void 0!==arguments[0]?arguments[0]:{initialRender:!1};t.initialRender&&(g.select("#chart").node().innerHTML="",this.chart=Object(y.createChart)({json:this.props.data,chartType:y.HourglassChart,renderer:y.DetailedRenderer,svgSelector:"#chart",indiCallback:function(t){return e.props.onSelection(t)},animate:!0,updateSvgSize:!1,locale:this.context.intl.locale}));var n=this.chart.render({startIndi:this.props.selection.id,baseGeneration:this.props.selection.generation}),a=g.select("#chart"),r=a.node().parentElement;g.select(r).on("scroll",O).call(g.zoom().scaleExtent([1,1]).translateExtent([[0,0],n.size]).on("zoom",b));var o,i,l=r.clientWidth/2-n.origin[0],c=r.clientHeight/2-n.origin[1],s=g.max([0,(r.clientWidth-n.size[0])/2]),u=g.max([0,(r.clientHeight-n.size[1])/2]),d=a.transition().delay(200).duration(500);(t.initialRender?a:d).attr("transform","translate(".concat(s,", ").concat(u,")")).attr("width",n.size[0]).attr("height",n.size[1]),t.initialRender?(r.scrollLeft=-l,r.scrollTop=-c):d.tween("scrollLeft",(i=-l,function(){var e=g.interpolateNumber(r.scrollLeft,i);return function(t){r.scrollLeft=e(t)}})).tween("scrollTop",(o=-c,function(){var e=g.interpolateNumber(r.scrollTop,o);return function(t){r.scrollTop=e(t)}}))}},{key:"componentDidMount",value:function(){this.renderChart({initialRender:!0})}},{key:"componentDidUpdate",value:function(e){this.renderChart({initialRender:this.props.data!==e.data})}},{key:"render",value:function(){return i.createElement("div",{id:"svgContainer"},i.createElement("svg",{id:"chart"}))}},{key:"getSvgContents",value:function(){var e=document.getElementById("chart").cloneNode(!0);return e.removeAttribute("transform"),(new XMLSerializer).serializeToString(e)}},{key:"getSvgContentsWithInlinedImages",value:function(){var e=document.getElementById("chart").cloneNode(!0);return e.removeAttribute("transform"),function(e){var t=Array.from(e.getElementsByTagName("image")).map(function(e){var t=e.href&&e.href.baseVal;return t?fetch(t).then(function(e){return e.blob()}).then(k).then(function(t){e.href.baseVal=t}).catch(function(e){return console.warn("Failed to load image:",e)}):Promise.resolve()});return Promise.all(t)}(e).then(function(){return(new XMLSerializer).serializeToString(e)})}},{key:"print",value:function(){var e=this,t=document.createElement("iframe");t.style.position="absolute",t.style.top="-1000px",t.style.left="-1000px",t.onload=function(){t.contentDocument.open(),t.contentDocument.write(e.getSvgContents()),t.contentDocument.close(),setTimeout(function(){t.contentWindow.focus(),t.contentWindow.print(),t.parentNode.removeChild(t)},500)},document.body.appendChild(t)}},{key:"downloadSvg",value:function(){this.getSvgContentsWithInlinedImages().then(function(e){var t=new Blob([e],{type:"image/svg+xml"});Object(w.saveAs)(t,"topola.svg")})}},{key:"drawOnCanvas",value:function(){return this.getSvgContentsWithInlinedImages().then(function(e){return new Blob([e],{type:"image/svg+xml"})}).then(C).then(j)}},{key:"downloadPng",value:function(){this.drawOnCanvas().then(function(e){return function(e,t){return new Promise(function(n,a){e.toBlob(function(e){e?n(e):a()},t)})}(e,"image/png")}).then(function(e){return Object(w.saveAs)(e,"topola.png")})}},{key:"downloadPdf",value:function(){this.drawOnCanvas().then(function(e){var t=new v.a({orientation:e.width>e.height?"l":"p",unit:"pt",format:[e.width,e.height]});t.addImage(e,"PNG",0,0,e.width,e.height,"NONE"),t.save("topola.pdf")})}}]),t}(i.PureComponent);R.contextTypes={intl:s.d};var S=n(246),z=n.n(S),D=n(247),M=n.n(D),P=["BIRT","BAPM","CHR","DEAT","BURI"],T=["NAME","SEX","FAMC","FAMS","SOUR","NOTE"],L=new Map([["BAPM","Baptism"],["BIRT","Birth"],["BURI","Burial"],["CHR","Christening"],["DEAT","Death"],["EMAIL","E-mail"],["OCCU","Occupation"],["TITL","Title"],["WWW","WWW"]]);function I(e){return i.createElement(s.a,{id:"gedcom.".concat(e),defaultMessage:L.get(e)||e})}function _(e){return i.createElement(i.Fragment,null,e.map(function(e){return i.createElement(i.Fragment,null,i.createElement(M.a,{properties:{target:"_blank"}},e),i.createElement("br",null))}))}function U(e,t){var n=[],a=e.tree.find(function(e){return"DATE"===e.tag});a&&a.data&&n.push(function(e,t){var n=Object(y.getDate)(e),a=n&&n.date;return a?Object(y.formatDate)(a,t):e}(a.data,t));var r=e.tree.find(function(e){return"PLAC"===e.tag});return r&&r.data&&n.push(r.data),e.tree.filter(function(e){return"NOTE"===e.tag}).forEach(function(e){n.push(i.createElement("i",null,e.data))}),n.length?i.createElement(i.Fragment,null,i.createElement("div",{className:"ui sub header"},I(e.tag)),i.createElement("span",null,_(n))):null}function N(e){var t=[];return e.data&&t.push(e.data),t.length?i.createElement("i",null,_(t)):null}function A(e){return i.createElement("h2",{className:"ui header"},e.data.split("/").filter(function(e){return!!e}).map(function(e){return i.createElement(i.Fragment,null,e,i.createElement("br",null))}))}function x(e,t,n){return z()(t,function(t){return e.filter(function(e){return e.tag===t}).map(function(e){return n(e)})}).filter(function(e){return null!==e}).map(function(e){return i.createElement("div",{className:"ui segment"},e)})}function W(e){return e.filter(function(e){return!T.includes(e.tag)&&!P.includes(e.tag)}).map(function(e){return function(e){var t=[];return e.data&&t.push(e.data),e.tree.filter(function(e){return"NOTE"===e.tag}).forEach(function(e){t.push(i.createElement("i",null,e.data))}),t.length?i.createElement(i.Fragment,null,i.createElement("div",{className:"ui sub header"},I(e.tag)),i.createElement("span",null,_(t))):null}(e)}).filter(function(e){return null!==e}).map(function(e){return i.createElement("div",{className:"ui segment"},e)})}var F=function(e){function t(){return Object(u.a)(this,t),Object(m.a)(this,Object(f.a)(t).apply(this,arguments))}return Object(h.a)(t,e),Object(d.a)(t,[{key:"render",value:function(){var e=this,t=this.props.gedcom.indis[this.props.indi].tree;return i.createElement("div",{className:"ui segments",id:"details"},x(t,["NAME"],A),x(t,P,function(t){return U(t,e.context.intl.locale)}),W(t),x(t,["NOTE"],N))}}]),t}(i.Component);F.contextTypes={intl:s.d};var G=n(145);function B(e){return e.substring(1,e.length-1)}function H(e){var t=e.find(function(e){return"HEAD"===e.tag}),n={},a={};return e.forEach(function(e){"INDI"===e.tag?n[B(e.pointer)]=e:"FAM"===e.tag&&(a[B(e.pointer)]=e)}),{head:t,indis:n,fams:a}}function J(e,t){if(!e.children)return e;var n=e.children.sort(function(e){var t=new Map;return e.indis.forEach(function(e){t[e.id]=e}),function(e,n){var a,r,o=(a=e)<(r=n)?-1:a>r?1:0,i=t[e],l=t[n],c=i&&i.birth,s=l&&l.birth,u=c&&(c.date||c.dateRange&&c.dateRange.from),d=s&&(s.date||s.dateRange&&s.dateRange.from);return u&&u.year&&d&&d.year?u.year!==d.year?u.year-d.year:u.month&&d.month?u.month!==d.month?u.month-d.month:u.day&&d.day&&u.day!==d.day?u.month-d.month:o:o:o}}(t));return Object.assign({},e,{children:n})}function V(e){var t=e.fams.map(function(t){return J(t,e)});return Object.assign({},e,{fams:t})}function K(e){if(!e.imageUrl||e.imageUrl.startsWith("http"))return e;var t=Object.assign({},e);return delete t.imageUrl,t}function X(e){var t=e.indis.map(K);return Object.assign({},e,{indis:t})}function Z(e,t,n){return{id:t||e.indis[0].id,generation:n||0}}function q(e,t){var n=function(e){var t=Object(G.parse)(e),n=Object(y.gedcomEntriesToJson)(t);if(!n||!n.indis||!n.indis.length||!n.fams||!n.fams.length)throw new Error("Failed to read GEDCOM file");return{chartData:X(V(n)),gedcom:H(t)}}(e),a=JSON.stringify(n);try{sessionStorage.setItem(t,a)}catch(r){console.warn("Failed to store data in session storage: "+r)}return n}function Y(e,t){try{return Promise.resolve(function(e,t){var n=sessionStorage.getItem(e);if(n)return JSON.parse(n);if(!t)throw new Error("Error loading data. Please upload your file again.");return q(t,e)}(e,t))}catch(n){return Promise.reject(new Error("Failed to read GEDCOM file"))}}var Q=n(521),$=n(514);function ee(e){return i.createElement($.a,{to:{pathname:"/view",search:p.stringify({url:e.url})}},e.text)}function te(){return i.createElement(Q.a,{className:"intro"},i.createElement(Q.a.Content,null,i.createElement(Q.a.Header,null,i.createElement(s.a,{id:"intro.title",defaultMessage:"Topola Genealogy Viewer"}))),i.createElement(Q.a.Content,null,i.createElement("p",null,i.createElement(s.a,{id:"intro.description",defaultMessage:"Topola Genealogy is a genealogy tree viewer that lets you browse the structure of the family."})),i.createElement("p",null,i.createElement(s.a,{id:"intro.instructions",defaultMessage:"Use the LOAD FROM URL or LOAD FROM FILE buttons above to load a GEDCOM file. You can export a GEDCOM file from most of the existing genealogy programs and web sites."})),i.createElement("p",null,i.createElement(s.a,{id:"intro.examples",defaultMessage:"Here are some examples from the web that you can view:"})),i.createElement("ul",null,i.createElement("li",null,i.createElement(ee,{url:"http://genpol.com/module-Downloads-prep_hand_out-lid-32.html",text:"Karol Wojty\u0142a"})," ","(",i.createElement(s.a,{id:"intro.from",defaultMessage:"from"})," ",i.createElement("a",{href:"http://genpol.com/module-Downloads-display-lid-32.html"},"GENPOL"),")"),i.createElement("li",null,i.createElement(ee,{url:"https://webtreeprint.com/tp_downloader.php?path=famous_gedcoms/shakespeare.ged",text:"Shakespeare"})," ","(",i.createElement(s.a,{id:"intro.from",defaultMessage:"from"})," ",i.createElement("a",{href:"https://webtreeprint.com/tp_famous_gedcoms.php"},"webtreeprint.com"),")"),i.createElement("li",null,i.createElement(ee,{url:"http://genealogyoflife.com/tng/gedcom/HarryPotter.ged",text:"Harry Potter"})," ","(",i.createElement(s.a,{id:"intro.from",defaultMessage:"from"})," ",i.createElement("a",{href:"http://famousfamilytrees.blogspot.com/"},"Famous Family Trees"),")")),i.createElement("p",null,i.createElement("b",null,i.createElement(s.a,{id:"intro.privacy",defaultMessage:"Privacy"})),":",i.createElement(s.a,{id:"intro.privacy_note",defaultMessage:'When using the "load from file" option, this site does not send your data anywhere and files loaded from disk do not leave your computer. When using "load from URL", data is passed through the {link} service to deal with an issue with cross-site file loading in the browser (CORS).',values:{link:i.createElement("a",{href:"https://cors-anywhere.herokuapp.com/"},"cors-anywhere")}}))))}var ne=n(522),ae=n(531),re=n(517),oe=n(528),ie=n(530),le=n(525),ce=n(252),se=n.n(ce),ue=n(519),de=n(527),me=n(238),fe=n(518),he=n(516),pe=n(524),ge=n(523),Ee=n(520),ve=function(e){function t(){var e,n;Object(u.a)(this,t);for(var a=arguments.length,r=new Array(a),o=0;o {\n const reader = new FileReader();\n reader.readAsDataURL(blob);\n return new Promise((resolve, reject) => {\n reader.onload = (e) => resolve((e.target as FileReader).result as string);\n });\n}\n\n/**\n * Fetches all images in the SVG and replaces them with inlined images as data\n * URLs. Images are replaced in place. The replacement is done, the returned\n * promise is resolved.\n */\nfunction inlineImages(svg: Element): Promise {\n const images = Array.from(svg.getElementsByTagName('image'));\n const promises = images.map((image) => {\n const href = image.href && image.href.baseVal;\n if (!href) {\n return Promise.resolve();\n }\n return fetch(href)\n .then((response) => response.blob())\n .then(loadAsDataUrl)\n .then((dataUrl) => {\n image.href.baseVal = dataUrl;\n })\n // Log and ignore errors.\n .catch((e) => console.warn('Failed to load image:', e));\n });\n return Promise.all(promises);\n}\n\n/** Loads a blob into an image object. */\nfunction loadImage(blob: Blob): Promise {\n const image = new Image();\n image.src = URL.createObjectURL(blob);\n return new Promise((resolve, reject) => {\n image.addEventListener('load', () => {\n resolve(image);\n });\n });\n}\n\n/** Draw image on a new canvas and return the canvas. */\nfunction drawOnCanvas(image: HTMLImageElement) {\n const canvas = document.createElement('canvas');\n // Scale image for better quality.\n canvas.width = image.width * 2;\n canvas.height = image.height * 2;\n\n const ctx = canvas.getContext('2d')!;\n const oldFill = ctx.fillStyle;\n ctx.fillStyle = 'white';\n ctx.fillRect(0, 0, canvas.width, canvas.height);\n ctx.fillStyle = oldFill;\n\n ctx.drawImage(image, 0, 0, canvas.width, canvas.height);\n return canvas;\n}\n\nfunction canvasToBlob(canvas: HTMLCanvasElement, type: string) {\n return new Promise((resolve, reject) => {\n canvas.toBlob((blob) => {\n if (blob) {\n resolve(blob);\n } else {\n reject();\n }\n }, type);\n });\n}\n\nexport interface ChartProps {\n data: JsonGedcomData;\n selection: IndiInfo;\n onSelection: (indiInfo: IndiInfo) => void;\n}\n\n/** Component showing the genealogy chart and handling transition animations. */\nexport class Chart extends React.PureComponent {\n private chart?: ChartHandle;\n\n /**\n * Renders the chart or performs a transition animation to a new state.\n * If indiInfo is not given, it means that it is the initial render and no\n * animation is performed.\n */\n private renderChart(args: {initialRender: boolean} = {initialRender: false}) {\n if (args.initialRender) {\n (d3.select('#chart').node() as HTMLElement).innerHTML = '';\n this.chart = createChart({\n json: this.props.data,\n chartType: HourglassChart,\n renderer: DetailedRenderer,\n svgSelector: '#chart',\n indiCallback: (info) => this.props.onSelection(info),\n animate: true,\n updateSvgSize: false,\n locale: this.context.intl.locale,\n });\n }\n const chartInfo = this.chart!.render({\n startIndi: this.props.selection.id,\n baseGeneration: this.props.selection.generation,\n });\n const svg = d3.select('#chart');\n const parent = (svg.node() as HTMLElement).parentElement as Element;\n\n d3.select(parent)\n .on('scroll', scrolled)\n .call(\n d3\n .zoom()\n .scaleExtent([1, 1])\n .translateExtent([[0, 0], chartInfo.size])\n .on('zoom', zoomed),\n );\n\n const scrollTopTween = (scrollTop: number) => {\n return () => {\n const i = d3.interpolateNumber(parent.scrollTop, scrollTop);\n return (t: number) => {\n parent.scrollTop = i(t);\n };\n };\n };\n const scrollLeftTween = (scrollLeft: number) => {\n return () => {\n const i = d3.interpolateNumber(parent.scrollLeft, scrollLeft);\n return (t: number) => {\n parent.scrollLeft = i(t);\n };\n };\n };\n\n const dx = parent.clientWidth / 2 - chartInfo.origin[0];\n const dy = parent.clientHeight / 2 - chartInfo.origin[1];\n const offsetX = d3.max([0, (parent.clientWidth - chartInfo.size[0]) / 2]);\n const offsetY = d3.max([0, (parent.clientHeight - chartInfo.size[1]) / 2]);\n const svgTransition = svg\n .transition()\n .delay(200)\n .duration(500);\n const transition = args.initialRender ? svg : svgTransition;\n transition\n .attr('transform', `translate(${offsetX}, ${offsetY})`)\n .attr('width', chartInfo.size[0])\n .attr('height', chartInfo.size[1]);\n if (args.initialRender) {\n parent.scrollLeft = -dx;\n parent.scrollTop = -dy;\n } else {\n svgTransition\n .tween('scrollLeft', scrollLeftTween(-dx))\n .tween('scrollTop', scrollTopTween(-dy));\n }\n }\n\n componentDidMount() {\n this.renderChart({initialRender: true});\n }\n\n componentDidUpdate(prevProps: ChartProps) {\n this.renderChart({initialRender: this.props.data !== prevProps.data});\n }\n\n /** Make intl appear in this.context. */\n static contextTypes = {\n intl: intlShape,\n };\n\n render() {\n return (\n
\n \n
\n );\n }\n\n private getSvgContents() {\n const svg = document.getElementById('chart')!.cloneNode(true) as Element;\n svg.removeAttribute('transform');\n return new XMLSerializer().serializeToString(svg);\n }\n\n private getSvgContentsWithInlinedImages() {\n const svg = document.getElementById('chart')!.cloneNode(true) as Element;\n svg.removeAttribute('transform');\n return inlineImages(svg).then(() =>\n new XMLSerializer().serializeToString(svg),\n );\n }\n\n /** Shows the print dialog to print the currently displayed chart. */\n print() {\n const printWindow = document.createElement('iframe');\n printWindow.style.position = 'absolute';\n printWindow.style.top = '-1000px';\n printWindow.style.left = '-1000px';\n printWindow.onload = () => {\n printWindow.contentDocument!.open();\n printWindow.contentDocument!.write(this.getSvgContents());\n printWindow.contentDocument!.close();\n // Doesn't work on Firefox without the setTimeout.\n setTimeout(() => {\n printWindow.contentWindow!.focus();\n printWindow.contentWindow!.print();\n printWindow.parentNode!.removeChild(printWindow);\n }, 500);\n };\n document.body.appendChild(printWindow);\n }\n\n downloadSvg() {\n this.getSvgContentsWithInlinedImages().then((contents) => {\n const blob = new Blob([contents], {type: 'image/svg+xml'});\n saveAs(blob, 'topola.svg');\n });\n }\n\n drawOnCanvas(): Promise {\n return this.getSvgContentsWithInlinedImages()\n .then((contents) => new Blob([contents], {type: 'image/svg+xml'}))\n .then(loadImage)\n .then(drawOnCanvas);\n }\n\n downloadPng() {\n this.drawOnCanvas()\n .then((canvas) => canvasToBlob(canvas, 'image/png'))\n .then((blob) => saveAs(blob, 'topola.png'));\n }\n\n downloadPdf() {\n this.drawOnCanvas().then((canvas) => {\n const doc = new jsPDF({\n orientation: canvas.width > canvas.height ? 'l' : 'p',\n unit: 'pt',\n format: [canvas.width, canvas.height],\n });\n doc.addImage(canvas, 'PNG', 0, 0, canvas.width, canvas.height, 'NONE');\n doc.save('topola.pdf');\n });\n }\n}\n","import * as React from 'react';\nimport flatMap from 'array.prototype.flatmap';\nimport Linkify from 'react-linkify';\nimport {formatDate, getDate} from 'topola';\nimport {FormattedMessage} from 'react-intl';\nimport {GedcomData} from './gedcom_util';\nimport {GedcomEntry} from 'parse-gedcom';\nimport {intlShape} from 'react-intl';\n\ninterface Props {\n gedcom: GedcomData;\n indi: string;\n}\n\nconst EVENT_TAGS = ['BIRT', 'BAPM', 'CHR', 'DEAT', 'BURI'];\nconst EXCLUDED_TAGS = ['NAME', 'SEX', 'FAMC', 'FAMS', 'SOUR', 'NOTE'];\nconst TAG_DESCRIPTIONS = new Map([\n ['BAPM', 'Baptism'],\n ['BIRT', 'Birth'],\n ['BURI', 'Burial'],\n ['CHR', 'Christening'],\n ['DEAT', 'Death'],\n ['EMAIL', 'E-mail'],\n ['OCCU', 'Occupation'],\n ['TITL', 'Title'],\n ['WWW', 'WWW'],\n]);\n\nfunction translateTag(tag: string) {\n return (\n \n );\n}\n\nfunction translateDate(gedcomDate: string, locale: string) {\n const dateOrRange = getDate(gedcomDate);\n const date = dateOrRange && dateOrRange.date;\n if (!date) {\n return gedcomDate;\n }\n return formatDate(date, locale);\n}\n\nfunction joinLines(lines: (JSX.Element | string)[]) {\n return (\n <>\n {lines.map((line) => (\n <>\n {line}\n
\n \n ))}\n \n );\n}\n\nfunction eventDetails(entry: GedcomEntry, locale: string) {\n const lines = [];\n const date = entry.tree.find((subentry) => subentry.tag === 'DATE');\n if (date && date.data) {\n lines.push(translateDate(date.data, locale));\n }\n const place = entry.tree.find((subentry) => subentry.tag === 'PLAC');\n if (place && place.data) {\n lines.push(place.data);\n }\n entry.tree\n .filter((subentry) => subentry.tag === 'NOTE')\n .forEach((note) => {\n lines.push({note.data});\n });\n if (!lines.length) {\n return null;\n }\n return (\n <>\n
{translateTag(entry.tag)}
\n {joinLines(lines)}\n \n );\n}\n\nfunction dataDetails(entry: GedcomEntry) {\n const lines = [];\n if (entry.data) {\n lines.push(entry.data);\n }\n entry.tree\n .filter((subentry) => subentry.tag === 'NOTE')\n .forEach((note) => {\n lines.push({note.data});\n });\n if (!lines.length) {\n return null;\n }\n return (\n <>\n
{translateTag(entry.tag)}
\n {joinLines(lines)}\n \n );\n}\n\nfunction noteDetails(entry: GedcomEntry) {\n const lines = [];\n if (entry.data) {\n lines.push(entry.data);\n }\n if (!lines.length) {\n return null;\n }\n return {joinLines(lines)};\n}\n\nfunction nameDetails(entry: GedcomEntry) {\n return (\n

\n {entry.data\n .split('/')\n .filter((name) => !!name)\n .map((name) => (\n <>\n {name}\n
\n \n ))}\n

\n );\n}\n\nfunction getDetails(\n entries: GedcomEntry[],\n tags: string[],\n detailsFunction: (entry: GedcomEntry) => JSX.Element | null,\n): JSX.Element[] {\n return flatMap(tags, (tag) =>\n entries\n .filter((entry) => entry.tag === tag)\n .map((entry) => detailsFunction(entry)),\n )\n .filter((element) => element !== null)\n .map((element) =>
{element}
);\n}\n\nfunction getOtherDetails(entries: GedcomEntry[]) {\n return entries\n .filter(\n (entry) =>\n !EXCLUDED_TAGS.includes(entry.tag) && !EVENT_TAGS.includes(entry.tag),\n )\n .map((entry) => dataDetails(entry))\n .filter((element) => element !== null)\n .map((element) =>
{element}
);\n}\n\nexport class Details extends React.Component {\n /** Make intl appear in this.context. */\n static contextTypes = {\n intl: intlShape,\n };\n\n render() {\n const entries = this.props.gedcom.indis[this.props.indi].tree;\n\n return (\n
\n {getDetails(entries, ['NAME'], nameDetails)}\n {getDetails(entries, EVENT_TAGS, (entry) =>\n eventDetails(entry, this.context.intl.locale),\n )}\n {getOtherDetails(entries)}\n {getDetails(entries, ['NOTE'], noteDetails)}\n
\n );\n }\n}\n","import {JsonFam, JsonGedcomData, JsonIndi, gedcomEntriesToJson} from 'topola';\nimport {GedcomEntry, parse as parseGedcom} from 'parse-gedcom';\n\nexport interface GedcomData {\n head: GedcomEntry;\n indis: {[key: string]: GedcomEntry};\n fams: {[key: string]: GedcomEntry};\n}\n\nexport interface TopolaData {\n chartData: JsonGedcomData;\n gedcom: GedcomData;\n}\n\n/**\n * Returns the identifier extracted from a pointer string.\n * E.g. '@I123@' -> 'I123'\n */\nfunction pointerToId(pointer: string): string {\n return pointer.substring(1, pointer.length - 1);\n}\n\nfunction prepareGedcom(entries: GedcomEntry[]): GedcomData {\n const head = entries.find((entry) => entry.tag === 'HEAD')!;\n const indis: {[key: string]: GedcomEntry} = {};\n const fams: {[key: string]: GedcomEntry} = {};\n entries.forEach((entry) => {\n if (entry.tag === 'INDI') {\n indis[pointerToId(entry.pointer)] = entry;\n } else if (entry.tag === 'FAM') {\n fams[pointerToId(entry.pointer)] = entry;\n }\n });\n return {head, indis, fams};\n}\n\nfunction strcmp(a: string, b: string) {\n if (a < b) {\n return -1;\n }\n if (a > b) {\n return 1;\n }\n return 0;\n}\n\n/** Birth date comparator for individuals. */\nfunction birthDatesComparator(gedcom: JsonGedcomData) {\n const idToIndiMap = new Map();\n gedcom.indis.forEach((indi) => {\n idToIndiMap[indi.id] = indi;\n });\n\n return (indiId1: string, indiId2: string) => {\n const idComparison = strcmp(indiId1, indiId2);\n const indi1: JsonIndi = idToIndiMap[indiId1];\n const indi2: JsonIndi = idToIndiMap[indiId2];\n const birth1 = indi1 && indi1.birth;\n const birth2 = indi2 && indi2.birth;\n const date1 =\n birth1 && (birth1.date || (birth1.dateRange && birth1.dateRange.from));\n const date2 =\n birth2 && (birth2.date || (birth2.dateRange && birth2.dateRange.from));\n if (!date1 || !date1.year || !date2 || !date2.year) {\n return idComparison;\n }\n if (date1.year !== date2.year) {\n return date1.year - date2.year;\n }\n if (!date1.month || !date2.month) {\n return idComparison;\n }\n if (date1.month !== date2.month) {\n return date1.month - date2.month;\n }\n if (date1.day && date2.day && date1.day !== date2.day) {\n return date1.month - date2.month;\n }\n return idComparison;\n };\n}\n\n/**\n * Sorts children by birth date in the given family.\n * Does not modify the input objects.\n */\nfunction sortFamilyChildren(fam: JsonFam, gedcom: JsonGedcomData): JsonFam {\n if (!fam.children) {\n return fam;\n }\n const newChildren = fam.children.sort(birthDatesComparator(gedcom));\n return Object.assign({}, fam, {children: newChildren});\n}\n\n/**\n * Sorts children by birth date.\n * Does not modify the input object.\n */\nfunction sortChildren(gedcom: JsonGedcomData): JsonGedcomData {\n const newFams = gedcom.fams.map((fam) => sortFamilyChildren(fam, gedcom));\n return Object.assign({}, gedcom, {fams: newFams});\n}\n\n/**\n * Removes images that are not HTTP links.\n * Does not modify the input object.\n */\nfunction filterImage(indi: JsonIndi): JsonIndi {\n if (!indi.imageUrl || indi.imageUrl.startsWith('http')) {\n return indi;\n }\n const newIndi = Object.assign({}, indi);\n delete newIndi.imageUrl;\n return newIndi;\n}\n\n/**\n * Removes images that are not HTTP links.\n * Does not modify the input object.\n */\nfunction filterImages(gedcom: JsonGedcomData): JsonGedcomData {\n const newIndis = gedcom.indis.map(filterImage);\n return Object.assign({}, gedcom, {indis: newIndis});\n}\n\n/**\n * Converts GEDCOM file into JSON data performing additional transformations:\n * - sort children by birth date\n * - remove images that are not HTTP links.\n */\nexport function convertGedcom(gedcom: string): TopolaData {\n const entries = parseGedcom(gedcom);\n const json = gedcomEntriesToJson(entries);\n if (\n !json ||\n !json.indis ||\n !json.indis.length ||\n !json.fams ||\n !json.fams.length\n ) {\n throw new Error('Failed to read GEDCOM file');\n }\n\n return {\n chartData: filterImages(sortChildren(json)),\n gedcom: prepareGedcom(entries),\n };\n}\n","import {convertGedcom, TopolaData} from './gedcom_util';\nimport {IndiInfo, JsonGedcomData} from 'topola';\n\n/**\n * Returns a valid IndiInfo object, either with the given indi and generation\n * or with an individual taken from the data and generation 0.\n */\nexport function getSelection(\n data: JsonGedcomData,\n indi?: string,\n generation?: number,\n): IndiInfo {\n return {\n id: indi || data.indis[0].id,\n generation: generation || 0,\n };\n}\n\nfunction prepareData(gedcom: string, cacheId: string): TopolaData {\n const data = convertGedcom(gedcom);\n const serializedData = JSON.stringify(data);\n try {\n sessionStorage.setItem(cacheId, serializedData);\n } catch (e) {\n console.warn('Failed to store data in session storage: ' + e);\n }\n return data;\n}\n\n/** Fetches data from the given URL. Uses cors-anywhere if handleCors is true. */\nexport function loadFromUrl(\n url: string,\n handleCors: boolean,\n): Promise {\n const cachedData = sessionStorage.getItem(url);\n if (cachedData) {\n return Promise.resolve(JSON.parse(cachedData));\n }\n const urlToFetch = handleCors\n ? 'https://cors-anywhere.herokuapp.com/' + url\n : url;\n\n return window\n .fetch(urlToFetch)\n .then((response) => {\n if (response.status !== 200) {\n return Promise.reject(new Error(response.statusText));\n }\n return response.text();\n })\n .then((gedcom) => {\n return prepareData(gedcom, url);\n });\n}\n\n/** Loads data from the given GEDCOM file contents. */\nfunction loadGedcomSync(hash: string, gedcom?: string) {\n const cachedData = sessionStorage.getItem(hash);\n if (cachedData) {\n return JSON.parse(cachedData);\n }\n if (!gedcom) {\n throw new Error('Error loading data. Please upload your file again.');\n }\n return prepareData(gedcom, hash);\n}\n\n/** Loads data from the given GEDCOM file contents. */\nexport function loadGedcom(hash: string, gedcom?: string): Promise {\n try {\n return Promise.resolve(loadGedcomSync(hash, gedcom));\n } catch (e) {\n return Promise.reject(new Error('Failed to read GEDCOM file'));\n }\n}\n","import * as queryString from 'query-string';\nimport * as React from 'react';\nimport {Card} from 'semantic-ui-react';\nimport {FormattedMessage} from 'react-intl';\nimport {Link} from 'react-router-dom';\n\n/** Link that loads a GEDCOM file from URL. */\nfunction GedcomLink(props: {url: string; text: string}) {\n return (\n \n {props.text}\n \n );\n}\n\n/** The intro page. */\nexport function Intro() {\n return (\n \n \n \n \n \n \n \n

\n \n

\n

\n \n

\n

\n \n

\n \n

\n \n \n \n :\n cors-anywhere\n ),\n }}\n />\n

\n
\n
\n );\n}\n","import * as queryString from 'query-string';\nimport * as React from 'react';\nimport md5 from 'md5';\nimport {FormattedMessage} from 'react-intl';\nimport {Link} from 'react-router-dom';\nimport {RouteComponentProps} from 'react-router-dom';\nimport {\n Header,\n Button,\n Icon,\n Menu,\n Modal,\n Input,\n Form,\n Dropdown,\n} from 'semantic-ui-react';\n\n/** Menus and dialogs state. */\ninterface State {\n loadUrlDialogOpen: boolean;\n url?: string;\n}\n\ninterface Props {\n showingChart: boolean;\n onPrint: () => void;\n onDownloadPdf: () => void;\n onDownloadPng: () => void;\n onDownloadSvg: () => void;\n}\n\nexport class TopBar extends React.Component<\n RouteComponentProps & Props,\n State\n> {\n state: State = {loadUrlDialogOpen: false};\n inputRef?: Input;\n\n /** Handles the \"Upload file\" button. */\n handleUpload(event: React.SyntheticEvent) {\n const files = (event.target as HTMLInputElement).files;\n if (!files || !files.length) {\n return;\n }\n const reader = new FileReader();\n reader.onload = (evt: ProgressEvent) => {\n const data = (evt.target as FileReader).result;\n const hash = md5(data as string);\n this.props.history.push({\n pathname: '/view',\n search: queryString.stringify({file: hash}),\n state: {data},\n });\n };\n reader.readAsText(files[0]);\n }\n\n /** Opens the \"Load from URL\" dialog. */\n handleLoadFromUrl() {\n this.setState(\n Object.assign({}, this.state, {loadUrlDialogOpen: true}),\n () => this.inputRef!.focus(),\n );\n }\n\n /** Cancels the \"Load from URL\" dialog. */\n handleClose() {\n this.setState(Object.assign({}, this.state, {loadUrlDialogOpen: false}));\n }\n\n /** Upload button clicked in the \"Load from URL\" dialog. */\n handleLoad() {\n this.setState(\n Object.assign({}, this.state, {\n loadUrlDialogOpen: false,\n }),\n );\n if (this.state.url) {\n this.props.history.push({\n pathname: '/view',\n search: queryString.stringify({url: this.state.url}),\n });\n }\n }\n\n /** Called when the URL input is typed into. */\n handleUrlChange(event: React.SyntheticEvent) {\n this.setState(\n Object.assign({}, this.state, {\n url: (event.target as HTMLInputElement).value,\n }),\n );\n }\n\n render() {\n const loadFromUrlModal = (\n this.handleClose()}\n centered={false}\n >\n
\n \n txt}\n />\n
\n \n
this.handleLoad()}>\n this.handleUrlChange(e)}\n ref={(ref) => (this.inputRef = ref!)}\n />\n

\n \n cors-anywhere.herokuapp.com\n \n ),\n }}\n />\n

\n \n
\n \n \n \n \n \n );\n\n const chartMenus = this.props.showingChart ? (\n <>\n this.props.onPrint()}>\n \n \n \n \n \n \n \n }\n className=\"item\"\n >\n \n this.props.onDownloadPdf()}>\n \n \n this.props.onDownloadPng()}>\n \n \n this.props.onDownloadSvg()}>\n \n \n \n \n \n ) : null;\n\n return (\n \n \n \n Topola Genealogy\n \n \n this.handleLoadFromUrl()}>\n \n \n \n this.handleUpload(e)}\n />\n \n {chartMenus}\n \n \n \n {loadFromUrlModal}\n \n );\n }\n}\n","import * as queryString from 'query-string';\nimport * as React from 'react';\nimport {Chart} from './chart';\nimport {Details} from './details';\nimport {getSelection, loadFromUrl, loadGedcom} from './load_data';\nimport {IndiInfo} from 'topola';\nimport {Intro} from './intro';\nimport {Loader, Message, Responsive} from 'semantic-ui-react';\nimport {Redirect, Route, RouteComponentProps, Switch} from 'react-router-dom';\nimport {TopBar} from './top_bar';\nimport {TopolaData} from './gedcom_util';\n\n/** Shows an error message. */\nexport function ErrorMessage(props: {message: string}) {\n return (\n \n Failed to load file\n

{props.message}

\n
\n );\n}\n\ninterface State {\n /** Loaded data. */\n data?: TopolaData;\n /** Selected individual. */\n selection?: IndiInfo;\n /** Hash of the GEDCOM contents. */\n hash?: string;\n /** Error to display. */\n error?: string;\n /** True if currently loading. */\n loading: boolean;\n /** URL of the data that is loaded or is being loaded. */\n url?: string;\n /** Whether the side panel is shoen. */\n showSidePanel?: boolean;\n}\n\nexport class App extends React.Component {\n state: State = {loading: false};\n chartRef: Chart | null = null;\n\n private isNewData(\n hash: string | undefined,\n url: string | undefined,\n ): boolean {\n return (\n !!(hash && hash !== this.state.hash) || !!(url && this.state.url !== url)\n );\n }\n\n componentDidMount() {\n this.componentDidUpdate();\n }\n\n componentDidUpdate() {\n if (this.props.location.pathname !== '/view') {\n return;\n }\n const gedcom = this.props.location.state && this.props.location.state.data;\n const search = queryString.parse(this.props.location.search);\n const getParam = (name: string) => {\n const value = search[name];\n return typeof value === 'string' ? value : undefined;\n };\n const url = getParam('url');\n const indi = getParam('indi');\n const parsedGen = Number(getParam('gen'));\n const generation = !isNaN(parsedGen) ? parsedGen : undefined;\n const hash = getParam('file');\n const handleCors = getParam('handleCors') !== 'false'; // True by default.\n const showSidePanel = getParam('sidePanel') !== 'false'; // True by default.\n\n if (!url && !hash) {\n this.props.history.replace({pathname: '/'});\n } else if (this.isNewData(hash, url)) {\n const loadedData = hash\n ? loadGedcom(hash, gedcom)\n : loadFromUrl(url!, handleCors);\n loadedData.then(\n (data) => {\n // Set state with data.\n this.setState(\n Object.assign({}, this.state, {\n data,\n hash,\n selection: getSelection(data.chartData, indi, generation),\n error: undefined,\n loading: false,\n url,\n showSidePanel,\n }),\n );\n },\n (error) => {\n // Set error state.\n this.setState(\n Object.assign({}, this.state, {\n error: error.message,\n loading: false,\n }),\n );\n },\n );\n // Set loading state.\n this.setState(\n Object.assign({}, this.state, {\n data: undefined,\n selection: undefined,\n hash,\n error: undefined,\n loading: true,\n url,\n }),\n );\n } else if (this.state.data && this.state.selection) {\n // Update selection if it has changed in the URL.\n const selection = getSelection(\n this.state.data.chartData,\n indi,\n generation,\n );\n if (\n this.state.selection.id !== selection.id ||\n this.state.selection.generation !== selection.generation\n ) {\n this.setState(\n Object.assign({}, this.state, {\n selection,\n }),\n );\n }\n }\n }\n\n /**\n * Called when the user clicks an individual box in the chart.\n * Updates the browser URL.\n */\n private onSelection = (selection: IndiInfo) => {\n const location = this.props.location;\n const search = queryString.parse(location.search);\n search.indi = selection.id;\n search.gen = String(selection.generation);\n location.search = queryString.stringify(search);\n this.props.history.push(location);\n };\n\n private renderMainArea = () => {\n if (this.state.data && this.state.selection) {\n return (\n
\n (this.chartRef = ref)}\n />\n {this.state.showSidePanel ? (\n \n \n \n ) : null}\n
\n );\n }\n if (this.state.error) {\n return ;\n }\n return ;\n };\n\n render() {\n return (\n <>\n (\n this.chartRef && this.chartRef.print()}\n onDownloadPdf={() => this.chartRef && this.chartRef.downloadPdf()}\n onDownloadPng={() => this.chartRef && this.chartRef.downloadPng()}\n onDownloadSvg={() => this.chartRef && this.chartRef.downloadSvg()}\n />\n )}\n />\n \n \n \n \n \n \n );\n }\n}\n","import * as locale_en from 'react-intl/locale-data/en';\nimport * as locale_pl from 'react-intl/locale-data/pl';\nimport * as React from 'react';\nimport * as ReactDOM from 'react-dom';\nimport messages_pl from './translations/pl.json';\nimport {addLocaleData} from 'react-intl';\nimport {App} from './app';\nimport {detect} from 'detect-browser';\nimport {HashRouter as Router, Route} from 'react-router-dom';\nimport {IntlProvider} from 'react-intl';\nimport './index.css';\nimport 'semantic-ui-css/semantic.min.css';\nimport 'canvas-toBlob';\n\naddLocaleData([...locale_en, ...locale_pl]);\n\nconst messages = {\n pl: messages_pl,\n};\nconst language = navigator.language && navigator.language.split(/[-_]/)[0];\n\nconst browser = detect();\n\nif (browser && browser.name === 'ie') {\n ReactDOM.render(\n

\n Topola Genealogy Viewer does not support Internet Explorer. Please try a\n different browser.\n

,\n document.querySelector('#root'),\n );\n} else {\n ReactDOM.render(\n \n \n \n \n ,\n document.querySelector('#root'),\n );\n}\n"],"sourceRoot":""} \ No newline at end of file diff --git a/static/js/main.d0a22e0b.chunk.js b/static/js/main.d0a22e0b.chunk.js deleted file mode 100644 index 954acee..0000000 --- a/static/js/main.d0a22e0b.chunk.js +++ /dev/null @@ -1,2 +0,0 @@ -(window.webpackJsonp=window.webpackJsonp||[]).push([[0],{241:function(e){e.exports={"menu.load_from_url":"Otw\xf3rz URL","menu.load_from_file":"Otw\xf3rz plik","menu.print":"Drukuj","menu.download":"Pobierz","menu.pdf_file":"Plik PDF","menu.png_file":"Plik PNG","menu.svg_file":"Plik SVG","menu.github":"\u0179r\xf3d\u0142a na GitHub","intro.title":"Topola Genealogy","intro.description":"Topola Genealogy pozwala przegl\u0105da\u0107 drzewo genealogiczne w interaktywny spos\xf3b.","intro.instructions":"Kliknij OTW\xd3RZ URL lub OTW\xd3RZ PLIK, aby za\u0142adowa\u0107 plik GEDCOM. Wi\u0119kszo\u015b\u0107 program\xf3w genealogicznych posiada funkcj\u0119 eksportu do pliku GEDCOM.","intro.examples":"Poni\u017cej jest kilka przyk\u0142ad\xf3w znalezionych w Internecie:","intro.from":"\u017ar\xf3d\u0142o:","intro.privacy":"Prywatno\u015b\u0107","intro.privacy_note":'U\u017cywaj\u0105c funkcji "Otw\xf3rz plik", Twoje dane nie s\u0105 nigdzie wysy\u0142ane i pozostaj\u0105 na Twoim komputerze. U\u017cywaj\u0105c funkcji "Otw\xf3rz URL", dane z podanego adresu przesy\u0142ane s\u0105 przez us\u0142ug\u0119 {link} w celu umo\u017cliwienia za\u0142adowania danych z innej domeny (CORS).',"load_from_url.title":"Otw\xf3rz z adresu URL","load_from_url.comment":"Dane z podanego adresu URL zostan\u0105 za\u0142adowane poprzez us\u0142ug\u0119 {link} w celu unikni\u0119cia problem\xf3w z CORS.","load_from_url.cancel":"Anuluj","load_from_url.load":"Otw\xf3rz","gedcom.BAPM":"Chrzest","gedcom.BIRT":"Narodziny","gedcom.BURI":"Pogrzeb","gedcom.CHR":"Chrzest","gedcom.DEAT":"\u015amier\u0107","gedcom.EMAIL":"E-mail","gedcom.OCCU":"Zaw\xf3d","gedcom.TITL":"Tytu\u0142","gedcom.WWW":"Strona WWW"}},242:function(e,t){},281:function(e,t,n){e.exports=n(505)},293:function(e,t){},299:function(e,t){},499:function(e,t,n){},505:function(e,t,n){"use strict";n.r(t);var a=n(152),r=n(239),o=n(240),i=n(0),l=n(61),c=n(241),s=n(15),u=n(30),d=n(31),m=n(33),f=n(32),h=n(34),p=n(41),g=n(17),E=n(244),v=n.n(E),w=n(149),y=n(42);function b(){var e=g.select("#chart").node().parentElement;e.scrollLeft=-g.event.transform.x,e.scrollTop=-g.event.transform.y}function O(){var e=g.select("#chart").node().parentElement,t=e.scrollLeft+e.clientWidth/2,n=e.scrollTop+e.clientHeight/2;g.select(e).call(g.zoom().translateTo,t,n)}var k=function(e){function t(){var e,n;Object(u.a)(this,t);for(var a=arguments.length,r=new Array(a),o=0;o0&&void 0!==arguments[0]?arguments[0]:{initialRender:!1};t.initialRender&&(g.select("#chart").node().innerHTML="",this.chart=Object(y.createChart)({json:this.props.data,chartType:y.HourglassChart,renderer:y.DetailedRenderer,svgSelector:"#chart",indiCallback:function(t){return e.props.onSelection(t)},animate:!0,updateSvgSize:!1,locale:this.context.intl.locale}));var n=this.chart.render({startIndi:this.props.selection.id,baseGeneration:this.props.selection.generation}),a=g.select("#chart"),r=a.node().parentElement;g.select(r).on("scroll",O).call(g.zoom().scaleExtent([1,1]).translateExtent([[0,0],n.size]).on("zoom",b));var o,i,l=r.clientWidth/2-n.origin[0],c=r.clientHeight/2-n.origin[1],s=g.max([0,(r.clientWidth-n.size[0])/2]),u=g.max([0,(r.clientHeight-n.size[1])/2]),d=a.transition().delay(200).duration(500);(t.initialRender?a:d).attr("transform","translate(".concat(s,", ").concat(u,")")).attr("width",n.size[0]).attr("height",n.size[1]),t.initialRender?(r.scrollLeft=-l,r.scrollTop=-c):d.tween("scrollLeft",(i=-l,function(){var e=g.interpolateNumber(r.scrollLeft,i);return function(t){r.scrollLeft=e(t)}})).tween("scrollTop",(o=-c,function(){var e=g.interpolateNumber(r.scrollTop,o);return function(t){r.scrollTop=e(t)}}))}},{key:"componentDidMount",value:function(){this.renderChart({initialRender:!0})}},{key:"componentDidUpdate",value:function(e){this.renderChart({initialRender:this.props.data!==e.data})}},{key:"render",value:function(){return i.createElement("div",{id:"svgContainer"},i.createElement("svg",{id:"chart"}))}},{key:"getSvgContents",value:function(){var e=document.getElementById("chart").cloneNode(!0);return e.removeAttribute("transform"),(new XMLSerializer).serializeToString(e)}},{key:"print",value:function(){var e=this,t=document.createElement("iframe");t.style.position="absolute",t.style.top="-1000px",t.style.left="-1000px",t.onload=function(){t.contentDocument.open(),t.contentDocument.write(e.getSvgContents()),t.contentDocument.close(),setTimeout(function(){t.contentWindow.focus(),t.contentWindow.print(),t.parentNode.removeChild(t)},500)},document.body.appendChild(t)}},{key:"downloadSvg",value:function(){var e=new Blob([this.getSvgContents()],{type:"image/svg+xml"});Object(w.saveAs)(e,"topola.svg")}},{key:"drawOnCanvas",value:function(){var e=document.createElement("canvas"),t=document.getElementById("chart");e.width=2*t.getBBox().width,e.height=2*t.getBBox().height;var n=new Blob([this.getSvgContents()],{type:"image/svg+xml"}),a=new Image;return a.src=URL.createObjectURL(n),new Promise(function(t){a.onload=function(){var n=e.getContext("2d"),r=n.fillStyle;n.fillStyle="white",n.fillRect(0,0,e.width,e.height),n.fillStyle=r,n.drawImage(a,0,0,e.width,e.height),t(e)}})}},{key:"downloadPng",value:function(){var e=function(e){e&&Object(w.saveAs)(e,"topola.png")};this.drawOnCanvas().then(function(t){return t.toBlob(e,"image/png")})}},{key:"downloadPdf",value:function(){this.drawOnCanvas().then(function(e){var t=new v.a({orientation:e.width>e.height?"l":"p",unit:"pt",format:[e.width,e.height]});t.addImage(e,"PNG",0,0,e.width,e.height,"NONE"),t.save("topola.pdf")})}}]),t}(i.PureComponent);k.contextTypes={intl:s.d};var C=n(246),j=n.n(C),R=n(247),S=n.n(R),D=["BIRT","BAPM","CHR","DEAT","BURI"],z=["NAME","SEX","FAMC","FAMS","SOUR","NOTE"],M=new Map([["BAPM","Baptism"],["BIRT","Birth"],["BURI","Burial"],["CHR","Christening"],["DEAT","Death"],["EMAIL","E-mail"],["OCCU","Occupation"],["TITL","Title"],["WWW","WWW"]]);function P(e){return i.createElement(s.a,{id:"gedcom.".concat(e),defaultMessage:M.get(e)||e})}function T(e){return i.createElement(i.Fragment,null,e.map(function(e){return i.createElement(i.Fragment,null,i.createElement(S.a,{properties:{target:"_blank"}},e),i.createElement("br",null))}))}function _(e,t){var n=[],a=e.tree.find(function(e){return"DATE"===e.tag});a&&a.data&&n.push(function(e,t){var n=Object(y.getDate)(e),a=n&&n.date;return a?Object(y.formatDate)(a,t):e}(a.data,t));var r=e.tree.find(function(e){return"PLAC"===e.tag});return r&&r.data&&n.push(r.data),e.tree.filter(function(e){return"NOTE"===e.tag}).forEach(function(e){n.push(i.createElement("i",null,e.data))}),n.length?i.createElement(i.Fragment,null,i.createElement("div",{className:"ui sub header"},P(e.tag)),i.createElement("span",null,T(n))):null}function L(e){var t=[];return e.data&&t.push(e.data),t.length?i.createElement("i",null,T(t)):null}function U(e){return i.createElement("h2",{className:"ui header"},e.data.split("/").filter(function(e){return!!e}).map(function(e){return i.createElement(i.Fragment,null,e,i.createElement("br",null))}))}function N(e,t,n){return j()(t,function(t){return e.filter(function(e){return e.tag===t}).map(function(e){return n(e)})}).filter(function(e){return null!==e}).map(function(e){return i.createElement("div",{className:"ui segment"},e)})}function I(e){return e.filter(function(e){return!z.includes(e.tag)&&!D.includes(e.tag)}).map(function(e){return function(e){var t=[];return e.data&&t.push(e.data),e.tree.filter(function(e){return"NOTE"===e.tag}).forEach(function(e){t.push(i.createElement("i",null,e.data))}),t.length?i.createElement(i.Fragment,null,i.createElement("div",{className:"ui sub header"},P(e.tag)),i.createElement("span",null,T(t))):null}(e)}).filter(function(e){return null!==e}).map(function(e){return i.createElement("div",{className:"ui segment"},e)})}var x=function(e){function t(){return Object(u.a)(this,t),Object(m.a)(this,Object(f.a)(t).apply(this,arguments))}return Object(h.a)(t,e),Object(d.a)(t,[{key:"render",value:function(){var e=this,t=this.props.gedcom.indis[this.props.indi].tree;return i.createElement("div",{className:"ui segments",id:"details"},N(t,["NAME"],U),N(t,D,function(t){return _(t,e.context.intl.locale)}),I(t),N(t,["NOTE"],L))}}]),t}(i.Component);x.contextTypes={intl:s.d};var A=n(145);function W(e){return e.substring(1,e.length-1)}function F(e){var t=e.find(function(e){return"HEAD"===e.tag}),n={},a={};return e.forEach(function(e){"INDI"===e.tag?n[W(e.pointer)]=e:"FAM"===e.tag&&(a[W(e.pointer)]=e)}),{head:t,indis:n,fams:a}}function B(e,t){if(!e.children)return e;var n=e.children.sort(function(e){var t=new Map;return e.indis.forEach(function(e){t[e.id]=e}),function(e,n){var a,r,o=(a=e)<(r=n)?-1:a>r?1:0,i=t[e],l=t[n],c=i&&i.birth,s=l&&l.birth,u=c&&(c.date||c.dateRange&&c.dateRange.from),d=s&&(s.date||s.dateRange&&s.dateRange.from);return u&&u.year&&d&&d.year?u.year!==d.year?u.year-d.year:u.month&&d.month?u.month!==d.month?u.month-d.month:u.day&&d.day&&u.day!==d.day?u.month-d.month:o:o:o}}(t));return Object.assign({},e,{children:n})}function G(e){var t=e.fams.map(function(t){return B(t,e)});return Object.assign({},e,{fams:t})}function H(e){if(!e.imageUrl||e.imageUrl.startsWith("http"))return e;var t=Object.assign({},e);return delete t.imageUrl,t}function J(e){var t=e.indis.map(H);return Object.assign({},e,{indis:t})}function V(e,t,n){return{id:t||e.indis[0].id,generation:n||0}}function K(e,t){var n=function(e){var t=Object(A.parse)(e),n=Object(y.gedcomEntriesToJson)(t);if(!n||!n.indis||!n.indis.length||!n.fams||!n.fams.length)throw new Error("Failed to read GEDCOM file");return{chartData:J(G(n)),gedcom:F(t)}}(e),a=JSON.stringify(n);try{sessionStorage.setItem(t,a)}catch(r){console.warn("Failed to store data in session storage: "+r)}return n}function Z(e,t){try{return Promise.resolve(function(e,t){var n=sessionStorage.getItem(e);if(n)return JSON.parse(n);if(!t)throw new Error("Error loading data. Please upload your file again.");return K(t,e)}(e,t))}catch(n){return Promise.reject(new Error("Failed to read GEDCOM file"))}}var q=n(521),X=n(514);function Y(e){return i.createElement(X.a,{to:{pathname:"/view",search:p.stringify({url:e.url})}},e.text)}function Q(){return i.createElement(q.a,{className:"intro"},i.createElement(q.a.Content,null,i.createElement(q.a.Header,null,i.createElement(s.a,{id:"intro.title",defaultMessage:"Topola Genealogy Viewer"}))),i.createElement(q.a.Content,null,i.createElement("p",null,i.createElement(s.a,{id:"intro.description",defaultMessage:"Topola Genealogy is a genealogy tree viewer that lets you browse the structure of the family."})),i.createElement("p",null,i.createElement(s.a,{id:"intro.instructions",defaultMessage:"Use the LOAD FROM URL or LOAD FROM FILE buttons above to load a GEDCOM file. You can export a GEDCOM file from most of the existing genealogy programs and web sites."})),i.createElement("p",null,i.createElement(s.a,{id:"intro.examples",defaultMessage:"Here are some examples from the web that you can view:"})),i.createElement("ul",null,i.createElement("li",null,i.createElement(Y,{url:"http://genpol.com/module-Downloads-prep_hand_out-lid-32.html",text:"Karol Wojty\u0142a"})," ","(",i.createElement(s.a,{id:"intro.from",defaultMessage:"from"})," ",i.createElement("a",{href:"http://genpol.com/module-Downloads-display-lid-32.html"},"GENPOL"),")"),i.createElement("li",null,i.createElement(Y,{url:"https://webtreeprint.com/tp_downloader.php?path=famous_gedcoms/shakespeare.ged",text:"Shakespeare"})," ","(",i.createElement(s.a,{id:"intro.from",defaultMessage:"from"})," ",i.createElement("a",{href:"https://webtreeprint.com/tp_famous_gedcoms.php"},"webtreeprint.com"),")"),i.createElement("li",null,i.createElement(Y,{url:"http://genealogyoflife.com/tng/gedcom/HarryPotter.ged",text:"Harry Potter"})," ","(",i.createElement(s.a,{id:"intro.from",defaultMessage:"from"})," ",i.createElement("a",{href:"http://famousfamilytrees.blogspot.com/"},"Famous Family Trees"),")")),i.createElement("p",null,i.createElement("b",null,i.createElement(s.a,{id:"intro.privacy",defaultMessage:"Privacy"})),":",i.createElement(s.a,{id:"intro.privacy_note",defaultMessage:'When using the "load from file" option, this site does not send your data anywhere and files loaded from disk do not leave your computer. When using "load from URL", data is passed through the {link} service to deal with an issue with cross-site file loading in the browser (CORS).',values:{link:i.createElement("a",{href:"https://cors-anywhere.herokuapp.com/"},"cors-anywhere")}}))))}var $=n(522),ee=n(531),te=n(517),ne=n(528),ae=n(530),re=n(525),oe=n(252),ie=n.n(oe),le=n(519),ce=n(527),se=n(238),ue=n(518),de=n(516),me=n(524),fe=n(523),he=n(520),pe=function(e){function t(){var e,n;Object(u.a)(this,t);for(var a=arguments.length,r=new Array(a),o=0;o void;\n}\n\n/** Component showing the genealogy chart and handling transition animations. */\nexport class Chart extends React.PureComponent {\n private chart?: ChartHandle;\n\n /**\n * Renders the chart or performs a transition animation to a new state.\n * If indiInfo is not given, it means that it is the initial render and no\n * animation is performed.\n */\n private renderChart(args: {initialRender: boolean} = {initialRender: false}) {\n if (args.initialRender) {\n (d3.select('#chart').node() as HTMLElement).innerHTML = '';\n this.chart = createChart({\n json: this.props.data,\n chartType: HourglassChart,\n renderer: DetailedRenderer,\n svgSelector: '#chart',\n indiCallback: (info) => this.props.onSelection(info),\n animate: true,\n updateSvgSize: false,\n locale: this.context.intl.locale,\n });\n }\n const chartInfo = this.chart!.render({\n startIndi: this.props.selection.id,\n baseGeneration: this.props.selection.generation,\n });\n const svg = d3.select('#chart');\n const parent = (svg.node() as HTMLElement).parentElement as Element;\n\n d3.select(parent)\n .on('scroll', scrolled)\n .call(\n d3\n .zoom()\n .scaleExtent([1, 1])\n .translateExtent([[0, 0], chartInfo.size])\n .on('zoom', zoomed),\n );\n\n const scrollTopTween = (scrollTop: number) => {\n return () => {\n const i = d3.interpolateNumber(parent.scrollTop, scrollTop);\n return (t: number) => {\n parent.scrollTop = i(t);\n };\n };\n };\n const scrollLeftTween = (scrollLeft: number) => {\n return () => {\n const i = d3.interpolateNumber(parent.scrollLeft, scrollLeft);\n return (t: number) => {\n parent.scrollLeft = i(t);\n };\n };\n };\n\n const dx = parent.clientWidth / 2 - chartInfo.origin[0];\n const dy = parent.clientHeight / 2 - chartInfo.origin[1];\n const offsetX = d3.max([0, (parent.clientWidth - chartInfo.size[0]) / 2]);\n const offsetY = d3.max([0, (parent.clientHeight - chartInfo.size[1]) / 2]);\n const svgTransition = svg\n .transition()\n .delay(200)\n .duration(500);\n const transition = args.initialRender ? svg : svgTransition;\n transition\n .attr('transform', `translate(${offsetX}, ${offsetY})`)\n .attr('width', chartInfo.size[0])\n .attr('height', chartInfo.size[1]);\n if (args.initialRender) {\n parent.scrollLeft = -dx;\n parent.scrollTop = -dy;\n } else {\n svgTransition\n .tween('scrollLeft', scrollLeftTween(-dx))\n .tween('scrollTop', scrollTopTween(-dy));\n }\n }\n\n componentDidMount() {\n this.renderChart({initialRender: true});\n }\n\n componentDidUpdate(prevProps: ChartProps) {\n this.renderChart({initialRender: this.props.data !== prevProps.data});\n }\n\n /** Make intl appear in this.context. */\n static contextTypes = {\n intl: intlShape,\n };\n\n render() {\n return (\n
\n \n
\n );\n }\n\n private getSvgContents() {\n const svg = document.getElementById('chart')!.cloneNode(true) as Element;\n svg.removeAttribute('transform');\n return new XMLSerializer().serializeToString(svg);\n }\n\n /** Shows the print dialog to print the currently displayed chart. */\n print() {\n const printWindow = document.createElement('iframe');\n printWindow.style.position = 'absolute';\n printWindow.style.top = '-1000px';\n printWindow.style.left = '-1000px';\n printWindow.onload = () => {\n printWindow.contentDocument!.open();\n printWindow.contentDocument!.write(this.getSvgContents());\n printWindow.contentDocument!.close();\n // Doesn't work on Firefox without the setTimeout.\n setTimeout(() => {\n printWindow.contentWindow!.focus();\n printWindow.contentWindow!.print();\n printWindow.parentNode!.removeChild(printWindow);\n }, 500);\n };\n document.body.appendChild(printWindow);\n }\n\n downloadSvg() {\n const blob = new Blob([this.getSvgContents()], {type: 'image/svg+xml'});\n saveAs(blob, 'topola.svg');\n }\n\n drawOnCanvas(): Promise {\n const canvas = document.createElement('canvas');\n\n // Scale image for better quality.\n const svg = (document.getElementById('chart') as unknown) as SVGSVGElement;\n canvas.width = svg.getBBox().width * 2;\n canvas.height = svg.getBBox().height * 2;\n\n const blob = new Blob([this.getSvgContents()], {type: 'image/svg+xml'});\n const img = new Image();\n img.src = URL.createObjectURL(blob);\n\n return new Promise((resolve) => {\n img.onload = () => {\n const ctx = canvas.getContext('2d')!;\n const oldFill = ctx.fillStyle;\n ctx.fillStyle = 'white';\n ctx.fillRect(0, 0, canvas.width, canvas.height);\n ctx.fillStyle = oldFill;\n\n ctx.drawImage(img, 0, 0, canvas.width, canvas.height);\n resolve(canvas);\n };\n });\n }\n\n downloadPng() {\n const onBlob = (blob: Blob | null) => {\n if (blob) {\n saveAs(blob, 'topola.png');\n }\n };\n this.drawOnCanvas().then((canvas) => canvas.toBlob(onBlob, 'image/png'));\n }\n\n downloadPdf() {\n this.drawOnCanvas().then((canvas) => {\n const doc = new jsPDF({\n orientation: canvas.width > canvas.height ? 'l' : 'p',\n unit: 'pt',\n format: [canvas.width, canvas.height],\n });\n doc.addImage(canvas, 'PNG', 0, 0, canvas.width, canvas.height, 'NONE');\n doc.save('topola.pdf');\n });\n }\n}\n","import * as React from 'react';\nimport flatMap from 'array.prototype.flatmap';\nimport Linkify from 'react-linkify';\nimport {formatDate, getDate} from 'topola';\nimport {FormattedMessage} from 'react-intl';\nimport {GedcomData} from './gedcom_util';\nimport {GedcomEntry} from 'parse-gedcom';\nimport {intlShape} from 'react-intl';\n\ninterface Props {\n gedcom: GedcomData;\n indi: string;\n}\n\nconst EVENT_TAGS = ['BIRT', 'BAPM', 'CHR', 'DEAT', 'BURI'];\nconst EXCLUDED_TAGS = ['NAME', 'SEX', 'FAMC', 'FAMS', 'SOUR', 'NOTE'];\nconst TAG_DESCRIPTIONS = new Map([\n ['BAPM', 'Baptism'],\n ['BIRT', 'Birth'],\n ['BURI', 'Burial'],\n ['CHR', 'Christening'],\n ['DEAT', 'Death'],\n ['EMAIL', 'E-mail'],\n ['OCCU', 'Occupation'],\n ['TITL', 'Title'],\n ['WWW', 'WWW'],\n]);\n\nfunction translateTag(tag: string) {\n return (\n \n );\n}\n\nfunction translateDate(gedcomDate: string, locale: string) {\n const dateOrRange = getDate(gedcomDate);\n const date = dateOrRange && dateOrRange.date;\n if (!date) {\n return gedcomDate;\n }\n return formatDate(date, locale);\n}\n\nfunction joinLines(lines: (JSX.Element | string)[]) {\n return (\n <>\n {lines.map((line) => (\n <>\n {line}\n
\n \n ))}\n \n );\n}\n\nfunction eventDetails(entry: GedcomEntry, locale: string) {\n const lines = [];\n const date = entry.tree.find((subentry) => subentry.tag === 'DATE');\n if (date && date.data) {\n lines.push(translateDate(date.data, locale));\n }\n const place = entry.tree.find((subentry) => subentry.tag === 'PLAC');\n if (place && place.data) {\n lines.push(place.data);\n }\n entry.tree\n .filter((subentry) => subentry.tag === 'NOTE')\n .forEach((note) => {\n lines.push({note.data});\n });\n if (!lines.length) {\n return null;\n }\n return (\n <>\n
{translateTag(entry.tag)}
\n {joinLines(lines)}\n \n );\n}\n\nfunction dataDetails(entry: GedcomEntry) {\n const lines = [];\n if (entry.data) {\n lines.push(entry.data);\n }\n entry.tree\n .filter((subentry) => subentry.tag === 'NOTE')\n .forEach((note) => {\n lines.push({note.data});\n });\n if (!lines.length) {\n return null;\n }\n return (\n <>\n
{translateTag(entry.tag)}
\n {joinLines(lines)}\n \n );\n}\n\nfunction noteDetails(entry: GedcomEntry) {\n const lines = [];\n if (entry.data) {\n lines.push(entry.data);\n }\n if (!lines.length) {\n return null;\n }\n return {joinLines(lines)};\n}\n\nfunction nameDetails(entry: GedcomEntry) {\n return (\n

\n {entry.data\n .split('/')\n .filter((name) => !!name)\n .map((name) => (\n <>\n {name}\n
\n \n ))}\n

\n );\n}\n\nfunction getDetails(\n entries: GedcomEntry[],\n tags: string[],\n detailsFunction: (entry: GedcomEntry) => JSX.Element | null,\n): JSX.Element[] {\n return flatMap(tags, (tag) =>\n entries\n .filter((entry) => entry.tag === tag)\n .map((entry) => detailsFunction(entry)),\n )\n .filter((element) => element !== null)\n .map((element) =>
{element}
);\n}\n\nfunction getOtherDetails(entries: GedcomEntry[]) {\n return entries\n .filter(\n (entry) =>\n !EXCLUDED_TAGS.includes(entry.tag) && !EVENT_TAGS.includes(entry.tag),\n )\n .map((entry) => dataDetails(entry))\n .filter((element) => element !== null)\n .map((element) =>
{element}
);\n}\n\nexport class Details extends React.Component {\n /** Make intl appear in this.context. */\n static contextTypes = {\n intl: intlShape,\n };\n\n render() {\n const entries = this.props.gedcom.indis[this.props.indi].tree;\n\n return (\n
\n {getDetails(entries, ['NAME'], nameDetails)}\n {getDetails(entries, EVENT_TAGS, (entry) =>\n eventDetails(entry, this.context.intl.locale),\n )}\n {getOtherDetails(entries)}\n {getDetails(entries, ['NOTE'], noteDetails)}\n
\n );\n }\n}\n","import {JsonFam, JsonGedcomData, JsonIndi, gedcomEntriesToJson} from 'topola';\nimport {GedcomEntry, parse as parseGedcom} from 'parse-gedcom';\n\nexport interface GedcomData {\n head: GedcomEntry;\n indis: {[key: string]: GedcomEntry};\n fams: {[key: string]: GedcomEntry};\n}\n\nexport interface TopolaData {\n chartData: JsonGedcomData;\n gedcom: GedcomData;\n}\n\n/**\n * Returns the identifier extracted from a pointer string.\n * E.g. '@I123@' -> 'I123'\n */\nfunction pointerToId(pointer: string): string {\n return pointer.substring(1, pointer.length - 1);\n}\n\nfunction prepareGedcom(entries: GedcomEntry[]): GedcomData {\n const head = entries.find((entry) => entry.tag === 'HEAD')!;\n const indis: {[key: string]: GedcomEntry} = {};\n const fams: {[key: string]: GedcomEntry} = {};\n entries.forEach((entry) => {\n if (entry.tag === 'INDI') {\n indis[pointerToId(entry.pointer)] = entry;\n } else if (entry.tag === 'FAM') {\n fams[pointerToId(entry.pointer)] = entry;\n }\n });\n return {head, indis, fams};\n}\n\nfunction strcmp(a: string, b: string) {\n if (a < b) {\n return -1;\n }\n if (a > b) {\n return 1;\n }\n return 0;\n}\n\n/** Birth date comparator for individuals. */\nfunction birthDatesComparator(gedcom: JsonGedcomData) {\n const idToIndiMap = new Map();\n gedcom.indis.forEach((indi) => {\n idToIndiMap[indi.id] = indi;\n });\n\n return (indiId1: string, indiId2: string) => {\n const idComparison = strcmp(indiId1, indiId2);\n const indi1: JsonIndi = idToIndiMap[indiId1];\n const indi2: JsonIndi = idToIndiMap[indiId2];\n const birth1 = indi1 && indi1.birth;\n const birth2 = indi2 && indi2.birth;\n const date1 =\n birth1 && (birth1.date || (birth1.dateRange && birth1.dateRange.from));\n const date2 =\n birth2 && (birth2.date || (birth2.dateRange && birth2.dateRange.from));\n if (!date1 || !date1.year || !date2 || !date2.year) {\n return idComparison;\n }\n if (date1.year !== date2.year) {\n return date1.year - date2.year;\n }\n if (!date1.month || !date2.month) {\n return idComparison;\n }\n if (date1.month !== date2.month) {\n return date1.month - date2.month;\n }\n if (date1.day && date2.day && date1.day !== date2.day) {\n return date1.month - date2.month;\n }\n return idComparison;\n };\n}\n\n/**\n * Sorts children by birth date in the given family.\n * Does not modify the input objects.\n */\nfunction sortFamilyChildren(fam: JsonFam, gedcom: JsonGedcomData): JsonFam {\n if (!fam.children) {\n return fam;\n }\n const newChildren = fam.children.sort(birthDatesComparator(gedcom));\n return Object.assign({}, fam, {children: newChildren});\n}\n\n/**\n * Sorts children by birth date.\n * Does not modify the input object.\n */\nfunction sortChildren(gedcom: JsonGedcomData): JsonGedcomData {\n const newFams = gedcom.fams.map((fam) => sortFamilyChildren(fam, gedcom));\n return Object.assign({}, gedcom, {fams: newFams});\n}\n\n/**\n * Removes images that are not HTTP links.\n * Does not modify the input object.\n */\nfunction filterImage(indi: JsonIndi): JsonIndi {\n if (!indi.imageUrl || indi.imageUrl.startsWith('http')) {\n return indi;\n }\n const newIndi = Object.assign({}, indi);\n delete newIndi.imageUrl;\n return newIndi;\n}\n\n/**\n * Removes images that are not HTTP links.\n * Does not modify the input object.\n */\nfunction filterImages(gedcom: JsonGedcomData): JsonGedcomData {\n const newIndis = gedcom.indis.map(filterImage);\n return Object.assign({}, gedcom, {indis: newIndis});\n}\n\n/**\n * Converts GEDCOM file into JSON data performing additional transformations:\n * - sort children by birth date\n * - remove images that are not HTTP links.\n */\nexport function convertGedcom(gedcom: string): TopolaData {\n const entries = parseGedcom(gedcom);\n const json = gedcomEntriesToJson(entries);\n if (\n !json ||\n !json.indis ||\n !json.indis.length ||\n !json.fams ||\n !json.fams.length\n ) {\n throw new Error('Failed to read GEDCOM file');\n }\n\n return {\n chartData: filterImages(sortChildren(json)),\n gedcom: prepareGedcom(entries),\n };\n}\n","import {convertGedcom, TopolaData} from './gedcom_util';\nimport {IndiInfo, JsonGedcomData} from 'topola';\n\n/**\n * Returns a valid IndiInfo object, either with the given indi and generation\n * or with an individual taken from the data and generation 0.\n */\nexport function getSelection(\n data: JsonGedcomData,\n indi?: string,\n generation?: number,\n): IndiInfo {\n return {\n id: indi || data.indis[0].id,\n generation: generation || 0,\n };\n}\n\nfunction prepareData(gedcom: string, cacheId: string): TopolaData {\n const data = convertGedcom(gedcom);\n const serializedData = JSON.stringify(data);\n try {\n sessionStorage.setItem(cacheId, serializedData);\n } catch (e) {\n console.warn('Failed to store data in session storage: ' + e);\n }\n return data;\n}\n\n/** Fetches data from the given URL. Uses cors-anywhere if handleCors is true. */\nexport function loadFromUrl(\n url: string,\n handleCors: boolean,\n): Promise {\n const cachedData = sessionStorage.getItem(url);\n if (cachedData) {\n return Promise.resolve(JSON.parse(cachedData));\n }\n const urlToFetch = handleCors\n ? 'https://cors-anywhere.herokuapp.com/' + url\n : url;\n\n return window\n .fetch(urlToFetch)\n .then((response) => {\n if (response.status !== 200) {\n return Promise.reject(new Error(response.statusText));\n }\n return response.text();\n })\n .then((gedcom) => {\n return prepareData(gedcom, url);\n });\n}\n\n/** Loads data from the given GEDCOM file contents. */\nfunction loadGedcomSync(hash: string, gedcom?: string) {\n const cachedData = sessionStorage.getItem(hash);\n if (cachedData) {\n return JSON.parse(cachedData);\n }\n if (!gedcom) {\n throw new Error('Error loading data. Please upload your file again.');\n }\n return prepareData(gedcom, hash);\n}\n\n/** Loads data from the given GEDCOM file contents. */\nexport function loadGedcom(hash: string, gedcom?: string): Promise {\n try {\n return Promise.resolve(loadGedcomSync(hash, gedcom));\n } catch (e) {\n return Promise.reject(new Error('Failed to read GEDCOM file'));\n }\n}\n","import * as queryString from 'query-string';\nimport * as React from 'react';\nimport {Card} from 'semantic-ui-react';\nimport {FormattedMessage} from 'react-intl';\nimport {Link} from 'react-router-dom';\n\n/** Link that loads a GEDCOM file from URL. */\nfunction GedcomLink(props: {url: string; text: string}) {\n return (\n \n {props.text}\n \n );\n}\n\n/** The intro page. */\nexport function Intro() {\n return (\n \n \n \n \n \n \n \n

\n \n

\n

\n \n

\n

\n \n

\n \n

\n \n \n \n :\n cors-anywhere\n ),\n }}\n />\n

\n
\n
\n );\n}\n","import * as queryString from 'query-string';\nimport * as React from 'react';\nimport md5 from 'md5';\nimport {FormattedMessage} from 'react-intl';\nimport {Link} from 'react-router-dom';\nimport {RouteComponentProps} from 'react-router-dom';\nimport {\n Header,\n Button,\n Icon,\n Menu,\n Modal,\n Input,\n Form,\n Dropdown,\n} from 'semantic-ui-react';\n\n/** Menus and dialogs state. */\ninterface State {\n loadUrlDialogOpen: boolean;\n url?: string;\n}\n\ninterface Props {\n showingChart: boolean;\n onPrint: () => void;\n onDownloadPdf: () => void;\n onDownloadPng: () => void;\n onDownloadSvg: () => void;\n}\n\nexport class TopBar extends React.Component<\n RouteComponentProps & Props,\n State\n> {\n state: State = {loadUrlDialogOpen: false};\n inputRef?: Input;\n\n /** Handles the \"Upload file\" button. */\n handleUpload(event: React.SyntheticEvent) {\n const files = (event.target as HTMLInputElement).files;\n if (!files || !files.length) {\n return;\n }\n const reader = new FileReader();\n reader.onload = (evt: ProgressEvent) => {\n const data = (evt.target as FileReader).result;\n const hash = md5(data as string);\n this.props.history.push({\n pathname: '/view',\n search: queryString.stringify({file: hash}),\n state: {data},\n });\n };\n reader.readAsText(files[0]);\n }\n\n /** Opens the \"Load from URL\" dialog. */\n handleLoadFromUrl() {\n this.setState(\n Object.assign({}, this.state, {loadUrlDialogOpen: true}),\n () => this.inputRef!.focus(),\n );\n }\n\n /** Cancels the \"Load from URL\" dialog. */\n handleClose() {\n this.setState(Object.assign({}, this.state, {loadUrlDialogOpen: false}));\n }\n\n /** Upload button clicked in the \"Load from URL\" dialog. */\n handleLoad() {\n this.setState(\n Object.assign({}, this.state, {\n loadUrlDialogOpen: false,\n }),\n );\n if (this.state.url) {\n this.props.history.push({\n pathname: '/view',\n search: queryString.stringify({url: this.state.url}),\n });\n }\n }\n\n /** Called when the URL input is typed into. */\n handleUrlChange(event: React.SyntheticEvent) {\n this.setState(\n Object.assign({}, this.state, {\n url: (event.target as HTMLInputElement).value,\n }),\n );\n }\n\n render() {\n const loadFromUrlModal = (\n this.handleClose()}\n centered={false}\n >\n
\n \n txt}\n />\n
\n \n
this.handleLoad()}>\n this.handleUrlChange(e)}\n ref={(ref) => (this.inputRef = ref!)}\n />\n

\n \n cors-anywhere.herokuapp.com\n \n ),\n }}\n />\n

\n \n
\n \n \n \n \n \n );\n\n const chartMenus = this.props.showingChart ? (\n <>\n this.props.onPrint()}>\n \n \n \n \n \n \n \n }\n className=\"item\"\n >\n \n this.props.onDownloadPdf()}>\n \n \n this.props.onDownloadPng()}>\n \n \n this.props.onDownloadSvg()}>\n \n \n \n \n \n ) : null;\n\n return (\n \n \n \n Topola Genealogy\n \n \n this.handleLoadFromUrl()}>\n \n \n \n this.handleUpload(e)}\n />\n \n {chartMenus}\n \n \n \n {loadFromUrlModal}\n \n );\n }\n}\n","import * as queryString from 'query-string';\nimport * as React from 'react';\nimport {Chart} from './chart';\nimport {Details} from './details';\nimport {getSelection, loadFromUrl, loadGedcom} from './load_data';\nimport {IndiInfo} from 'topola';\nimport {Intro} from './intro';\nimport {Loader, Message, Responsive} from 'semantic-ui-react';\nimport {Redirect, Route, RouteComponentProps, Switch} from 'react-router-dom';\nimport {TopBar} from './top_bar';\nimport {TopolaData} from './gedcom_util';\n\n/** Shows an error message. */\nexport function ErrorMessage(props: {message: string}) {\n return (\n \n Failed to load file\n

{props.message}

\n
\n );\n}\n\ninterface State {\n /** Loaded data. */\n data?: TopolaData;\n /** Selected individual. */\n selection?: IndiInfo;\n /** Hash of the GEDCOM contents. */\n hash?: string;\n /** Error to display. */\n error?: string;\n /** True if currently loading. */\n loading: boolean;\n /** URL of the data that is loaded or is being loaded. */\n url?: string;\n /** Whether the side panel is shoen. */\n showSidePanel?: boolean;\n}\n\nexport class App extends React.Component {\n state: State = {loading: false};\n chartRef: Chart | null = null;\n\n private isNewData(\n hash: string | undefined,\n url: string | undefined,\n ): boolean {\n return (\n !!(hash && hash !== this.state.hash) || !!(url && this.state.url !== url)\n );\n }\n\n componentDidMount() {\n this.componentDidUpdate();\n }\n\n componentDidUpdate() {\n if (this.props.location.pathname !== '/view') {\n return;\n }\n const gedcom = this.props.location.state && this.props.location.state.data;\n const search = queryString.parse(this.props.location.search);\n const getParam = (name: string) => {\n const value = search[name];\n return typeof value === 'string' ? value : undefined;\n };\n const url = getParam('url');\n const indi = getParam('indi');\n const parsedGen = Number(getParam('gen'));\n const generation = !isNaN(parsedGen) ? parsedGen : undefined;\n const hash = getParam('file');\n const handleCors = getParam('handleCors') !== 'false'; // True by default.\n const showSidePanel = getParam('sidePanel') !== 'false'; // True by default.\n\n if (!url && !hash) {\n this.props.history.replace({pathname: '/'});\n } else if (this.isNewData(hash, url)) {\n const loadedData = hash\n ? loadGedcom(hash, gedcom)\n : loadFromUrl(url!, handleCors);\n loadedData.then(\n (data) => {\n // Set state with data.\n this.setState(\n Object.assign({}, this.state, {\n data,\n hash,\n selection: getSelection(data.chartData, indi, generation),\n error: undefined,\n loading: false,\n url,\n showSidePanel,\n }),\n );\n },\n (error) => {\n // Set error state.\n this.setState(\n Object.assign({}, this.state, {\n error: error.message,\n loading: false,\n }),\n );\n },\n );\n // Set loading state.\n this.setState(\n Object.assign({}, this.state, {\n data: undefined,\n selection: undefined,\n hash,\n error: undefined,\n loading: true,\n url,\n }),\n );\n } else if (this.state.data && this.state.selection) {\n // Update selection if it has changed in the URL.\n const selection = getSelection(\n this.state.data.chartData,\n indi,\n generation,\n );\n if (\n this.state.selection.id !== selection.id ||\n this.state.selection.generation !== selection.generation\n ) {\n this.setState(\n Object.assign({}, this.state, {\n selection,\n }),\n );\n }\n }\n }\n\n /**\n * Called when the user clicks an individual box in the chart.\n * Updates the browser URL.\n */\n private onSelection = (selection: IndiInfo) => {\n const location = this.props.location;\n const search = queryString.parse(location.search);\n search.indi = selection.id;\n search.gen = String(selection.generation);\n location.search = queryString.stringify(search);\n this.props.history.push(location);\n };\n\n private renderMainArea = () => {\n if (this.state.data && this.state.selection) {\n return (\n
\n (this.chartRef = ref)}\n />\n {this.state.showSidePanel ? (\n \n \n \n ) : null}\n
\n );\n }\n if (this.state.error) {\n return ;\n }\n return ;\n };\n\n render() {\n return (\n <>\n (\n this.chartRef && this.chartRef.print()}\n onDownloadPdf={() => this.chartRef && this.chartRef.downloadPdf()}\n onDownloadPng={() => this.chartRef && this.chartRef.downloadPng()}\n onDownloadSvg={() => this.chartRef && this.chartRef.downloadSvg()}\n />\n )}\n />\n \n \n \n \n \n \n );\n }\n}\n","import * as locale_en from 'react-intl/locale-data/en';\nimport * as locale_pl from 'react-intl/locale-data/pl';\nimport * as React from 'react';\nimport * as ReactDOM from 'react-dom';\nimport messages_pl from './translations/pl.json';\nimport {addLocaleData} from 'react-intl';\nimport {App} from './app';\nimport {detect} from 'detect-browser';\nimport {HashRouter as Router, Route} from 'react-router-dom';\nimport {IntlProvider} from 'react-intl';\nimport './index.css';\nimport 'semantic-ui-css/semantic.min.css';\nimport 'canvas-toBlob';\n\naddLocaleData([...locale_en, ...locale_pl]);\n\nconst messages = {\n pl: messages_pl,\n};\nconst language = navigator.language && navigator.language.split(/[-_]/)[0];\n\nconst browser = detect();\n\nif (browser && browser.name === 'ie') {\n ReactDOM.render(\n

\n Topola Genealogy Viewer does not support Internet Explorer. Please try a\n different browser.\n

,\n document.querySelector('#root'),\n );\n} else {\n ReactDOM.render(\n \n \n \n \n ,\n document.querySelector('#root'),\n );\n}\n"],"sourceRoot":""} \ No newline at end of file