From f4d3f7e8be4ff403c791f6eafd5fd3e99910d4cd Mon Sep 17 00:00:00 2001 From: Alexander Pilz Date: Thu, 30 Oct 2025 09:44:35 +0100 Subject: [PATCH 1/4] Fix for missing parents in main element. --- src/layout/handlers.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/layout/handlers.ts b/src/layout/handlers.ts index 60808e10..d2435953 100644 --- a/src/layout/handlers.ts +++ b/src/layout/handlers.ts @@ -62,7 +62,13 @@ export function setupSiblings({ const p2 = main.data.rels.parents[1] const siblings = findSiblings(main) - if (siblings.length > 0 && !main.parents) throw new Error('no parents') + if (siblings.length > 0 && !main.parents) { + if (main.data.parents) { + main.parents = main.data.parents; + } else { + throw new Error('no parents'); + } + } const siblings_added = addSiblingsToTree(main) positionSiblings(main) From 79c8b55181701e191a75aac6fd5cfef1a054e7f9 Mon Sep 17 00:00:00 2001 From: Johannes Raggam Date: Mon, 3 Nov 2025 00:05:08 +0100 Subject: [PATCH 2/4] fix: view-handlers cardToMiddle. Fix calculation of cardToMiddle. Without this fix the y-position was calculated incorrectly for scales below 1. --- src/handlers/view-handlers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/handlers/view-handlers.ts b/src/handlers/view-handlers.ts index dffecfc1..bd1e7e8b 100644 --- a/src/handlers/view-handlers.ts +++ b/src/handlers/view-handlers.ts @@ -45,7 +45,7 @@ type CardToMiddleProps = { transition_time?: number } export function cardToMiddle({datum, svg, svg_dim, scale, transition_time}: CardToMiddleProps) { - const k = scale || 1, x = svg_dim.width/2-datum.x*k, y = svg_dim.height/2-datum.y, + const k = scale || 1, x = svg_dim.width/2-datum.x*k, y = svg_dim.height/2-datum.y*k, t = {k, x: x/k, y: y/k} positionTree({t, svg, transition_time}) } From 206161465b1209fd00fb113e759d287d39197d13 Mon Sep 17 00:00:00 2001 From: Johannes Raggam Date: Wed, 29 Oct 2025 19:37:52 +0100 Subject: [PATCH 3/4] TMP: allow dist directory for testing. --- .gitignore | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 01f3b1a9..df9ed194 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ -/dist/ +#/dist/ /node_modules/ /.idea/ /.vscode/ /tests/debugging/ cypress/screenshots -docs/api \ No newline at end of file +docs/api From db9d2557590cbe635e8b04692cc8d48a8aa15830 Mon Sep 17 00:00:00 2001 From: Johannes Raggam Date: Mon, 22 Dec 2025 14:12:40 +0100 Subject: [PATCH 4/4] Compiled. --- dist/family-chart.esm.js | 5680 ++++++++++++++++ dist/family-chart.js | 5726 +++++++++++++++++ dist/family-chart.min.js | 2 + dist/styles/family-chart.css | 880 +++ dist/types/core/add-relative.d.ts | 38 + dist/types/core/cards/card-html.d.ts | 51 + dist/types/core/cards/card-svg.d.ts | 24 + dist/types/core/cards/utils.d.ts | 1 + dist/types/core/chart.d.ts | 259 + dist/types/core/edit.d.ts | 132 + dist/types/core/form-creator.d.ts | 2 + dist/types/core/remove-relative.d.ts | 18 + dist/types/elements.d.ts | 6 + dist/types/exports.d.ts | 30 + dist/types/features/autocomplete.d.ts | 27 + .../card-component/card-component.d.ts | 3 + .../features/card-component/handlers.d.ts | 11 + .../duplicates-ancestry.d.ts | 1 + .../duplicates-toggle/duplicates-progeny.d.ts | 2 + .../duplicates-toggle-renderer.d.ts | 1 + dist/types/features/history.d.ts | 20 + dist/types/features/info-popup.d.ts | 12 + .../features/kinships/calculate-kinships.d.ts | 17 + .../types/features/kinships/kinship-info.d.ts | 3 + .../features/kinships/kinships-data.d.ts | 6 + dist/types/features/link-spouse-text.d.ts | 10 + dist/types/features/modal.d.ts | 17 + dist/types/handlers.d.ts | 10 + .../handlers/check-person-connection.d.ts | 3 + dist/types/handlers/general.d.ts | 8 + dist/types/handlers/view-handlers.d.ts | 46 + dist/types/index.d.ts | 3 + dist/types/layout/calculate-tree.d.ts | 54 + dist/types/layout/create-links.d.ts | 13 + dist/types/layout/handlers.d.ts | 23 + dist/types/layout/path-to-main.d.ts | 13 + dist/types/renderers/card-html.d.ts | 20 + dist/types/renderers/card-svg/card-svg.d.ts | 24 + dist/types/renderers/card-svg/defs.d.ts | 3 + dist/types/renderers/card-svg/elements.d.ts | 25 + dist/types/renderers/card-svg/methods.d.ts | 5 + dist/types/renderers/card-svg/templates.d.ts | 73 + dist/types/renderers/create-form-html.d.ts | 3 + dist/types/renderers/create-form.d.ts | 3 + dist/types/renderers/html.d.ts | 10 + dist/types/renderers/icons.d.ts | 36 + dist/types/renderers/svg.d.ts | 2 + dist/types/renderers/view-cards-html.d.ts | 3 + dist/types/renderers/view-cards-svg.d.ts | 3 + dist/types/renderers/view-links.d.ts | 3 + dist/types/renderers/view.d.ts | 11 + dist/types/store/add-existing-rel.d.ts | 3 + dist/types/store/add-relative.d.ts | 5 + dist/types/store/edit.d.ts | 11 + dist/types/store/format-data.d.ts | 12 + dist/types/store/new-person.d.ts | 42 + dist/types/store/store.d.ts | 2 + dist/types/types/card.d.ts | 13 + dist/types/types/data.d.ts | 14 + dist/types/types/form.d.ts | 104 + dist/types/types/index.d.ts | 10 + dist/types/types/store.d.ts | 61 + dist/types/types/treeData.d.ts | 62 + dist/types/types/view.d.ts | 7 + 64 files changed, 13722 insertions(+) create mode 100644 dist/family-chart.esm.js create mode 100644 dist/family-chart.js create mode 100644 dist/family-chart.min.js create mode 100644 dist/styles/family-chart.css create mode 100644 dist/types/core/add-relative.d.ts create mode 100644 dist/types/core/cards/card-html.d.ts create mode 100644 dist/types/core/cards/card-svg.d.ts create mode 100644 dist/types/core/cards/utils.d.ts create mode 100644 dist/types/core/chart.d.ts create mode 100644 dist/types/core/edit.d.ts create mode 100644 dist/types/core/form-creator.d.ts create mode 100644 dist/types/core/remove-relative.d.ts create mode 100644 dist/types/elements.d.ts create mode 100644 dist/types/exports.d.ts create mode 100644 dist/types/features/autocomplete.d.ts create mode 100644 dist/types/features/card-component/card-component.d.ts create mode 100644 dist/types/features/card-component/handlers.d.ts create mode 100644 dist/types/features/duplicates-toggle/duplicates-ancestry.d.ts create mode 100644 dist/types/features/duplicates-toggle/duplicates-progeny.d.ts create mode 100644 dist/types/features/duplicates-toggle/duplicates-toggle-renderer.d.ts create mode 100644 dist/types/features/history.d.ts create mode 100644 dist/types/features/info-popup.d.ts create mode 100644 dist/types/features/kinships/calculate-kinships.d.ts create mode 100644 dist/types/features/kinships/kinship-info.d.ts create mode 100644 dist/types/features/kinships/kinships-data.d.ts create mode 100644 dist/types/features/link-spouse-text.d.ts create mode 100644 dist/types/features/modal.d.ts create mode 100644 dist/types/handlers.d.ts create mode 100644 dist/types/handlers/check-person-connection.d.ts create mode 100644 dist/types/handlers/general.d.ts create mode 100644 dist/types/handlers/view-handlers.d.ts create mode 100644 dist/types/index.d.ts create mode 100644 dist/types/layout/calculate-tree.d.ts create mode 100644 dist/types/layout/create-links.d.ts create mode 100644 dist/types/layout/handlers.d.ts create mode 100644 dist/types/layout/path-to-main.d.ts create mode 100644 dist/types/renderers/card-html.d.ts create mode 100644 dist/types/renderers/card-svg/card-svg.d.ts create mode 100644 dist/types/renderers/card-svg/defs.d.ts create mode 100644 dist/types/renderers/card-svg/elements.d.ts create mode 100644 dist/types/renderers/card-svg/methods.d.ts create mode 100644 dist/types/renderers/card-svg/templates.d.ts create mode 100644 dist/types/renderers/create-form-html.d.ts create mode 100644 dist/types/renderers/create-form.d.ts create mode 100644 dist/types/renderers/html.d.ts create mode 100644 dist/types/renderers/icons.d.ts create mode 100644 dist/types/renderers/svg.d.ts create mode 100644 dist/types/renderers/view-cards-html.d.ts create mode 100644 dist/types/renderers/view-cards-svg.d.ts create mode 100644 dist/types/renderers/view-links.d.ts create mode 100644 dist/types/renderers/view.d.ts create mode 100644 dist/types/store/add-existing-rel.d.ts create mode 100644 dist/types/store/add-relative.d.ts create mode 100644 dist/types/store/edit.d.ts create mode 100644 dist/types/store/format-data.d.ts create mode 100644 dist/types/store/new-person.d.ts create mode 100644 dist/types/store/store.d.ts create mode 100644 dist/types/types/card.d.ts create mode 100644 dist/types/types/data.d.ts create mode 100644 dist/types/types/form.d.ts create mode 100644 dist/types/types/index.d.ts create mode 100644 dist/types/types/store.d.ts create mode 100644 dist/types/types/treeData.d.ts create mode 100644 dist/types/types/view.d.ts diff --git a/dist/family-chart.esm.js b/dist/family-chart.esm.js new file mode 100644 index 00000000..28e3c6b9 --- /dev/null +++ b/dist/family-chart.esm.js @@ -0,0 +1,5680 @@ +// https://donatso.github.io/family-chart/ v0.9.0 Copyright 2025 donatso +import * as d3 from 'd3'; + +function sortChildrenWithSpouses(children, datum, data) { + if (!datum.rels.children) + return; + const spouses = datum.rels.spouses || []; + return children.sort((a, b) => { + const a_p2 = otherParent(a, datum, data); + const b_p2 = otherParent(b, datum, data); + const a_i = a_p2 ? spouses.indexOf(a_p2.id) : -1; + const b_i = b_p2 ? spouses.indexOf(b_p2.id) : -1; + if (datum.data.gender === "M") + return a_i - b_i; + else + return b_i - a_i; + }); +} +function sortAddNewChildren(children) { + return children.sort((a, b) => { + const a_new = a._new_rel_data; + const b_new = b._new_rel_data; + if (a_new && !b_new) + return 1; + if (!a_new && b_new) + return -1; + return 0; + }); +} +function otherParent(d, p1, data) { + return data.find(d0 => (d0.id !== p1.id) && (d.rels.parents.includes(d0.id))); +} +function calculateEnterAndExitPositions(d, entering, exiting) { + d.exiting = exiting; + if (entering) { + if (d.depth === 0 && !d.spouse) { + d._x = d.x; + d._y = d.y; + } + else if (d.spouse) { + d._x = d.spouse.x; + d._y = d.spouse.y; + } + else if (d.is_ancestry) { + if (!d.parent) + throw new Error('no parent'); + d._x = d.parent.x; + d._y = d.parent.y; + } + else { + d._x = d.psx; + d._y = d.psy; + } + } + else if (exiting) { + const x = d.x > 0 ? 1 : -1, y = d.y > 0 ? 1 : -1; + { + d._x = d.x + 400 * x; + d._y = d.y + 400 * y; + } + } +} +function setupSiblings({ tree, data_stash, node_separation, sortChildrenFunction }) { + const main = tree.find(d => d.data.main); + if (!main) + throw new Error('no main'); + const p1 = main.data.rels.parents[0]; + const p2 = main.data.rels.parents[1]; + const siblings = findSiblings(main); + if (siblings.length > 0 && !main.parents) { + if (main.data.parents) { + main.parents = main.data.parents; + } + else { + throw new Error('no parents'); + } + } + const siblings_added = addSiblingsToTree(main); + positionSiblings(main); + function findSiblings(main) { + return data_stash.filter(d => { + if (d.id === main.data.id) + return false; + if (p1 && d.rels.parents.includes(p1)) + return true; + if (p2 && d.rels.parents.includes(p2)) + return true; + return false; + }); + } + function addSiblingsToTree(main) { + const siblings_added = []; + for (let i = 0; i < siblings.length; i++) { + const sib = { + data: siblings[i], + sibling: true, + x: 0.0, // to be calculated in positionSiblings + y: main.y, + depth: main.depth - 1, + parents: [] + }; + const p1 = main.parents.find(d => d.data.id === sib.data.rels.parents[0]); + const p2 = main.parents.find(d => d.data.id === sib.data.rels.parents[1]); + if (p1) + sib.parents.push(p1); + if (p2) + sib.parents.push(p2); + tree.push(sib); + siblings_added.push(sib); + } + return siblings_added; + } + function positionSiblings(main) { + var _a, _b; + const sorted_siblings = [main, ...siblings_added]; + if (sortChildrenFunction) + sorted_siblings.sort((a, b) => sortChildrenFunction(a.data, b.data)); // first sort by custom function if provided + sorted_siblings.sort((a, b) => { + const a_p1 = main.parents.find(d => d.data.id === a.data.rels.parents[0]); + const a_p2 = main.parents.find(d => d.data.id === a.data.rels.parents[1]); + const b_p1 = main.parents.find(d => d.data.id === b.data.rels.parents[0]); + const b_p2 = main.parents.find(d => d.data.id === b.data.rels.parents[1]); + if (!a_p2 && b_p2) + return -1; + if (a_p2 && !b_p2) + return 1; + if (!a_p1 && b_p1) + return 1; + if (a_p1 && !b_p1) + return -1; + // If both have same parents or both missing same parent, maintain original order + return 0; + }); + const main_x = main.x; + const spouses_x = (main.spouses || []).map(d => d.x); + const x_range = d3.extent([main_x, ...spouses_x]); + const main_sorted_index = sorted_siblings.findIndex(d => d.data.id === main.data.id); + for (let i = 0; i < sorted_siblings.length; i++) { + if (i === main_sorted_index) + continue; + const sib = sorted_siblings[i]; + if (i < main_sorted_index) { + sib.x = ((_a = x_range[0]) !== null && _a !== void 0 ? _a : 0) - node_separation * (main_sorted_index - i); + } + else { + sib.x = ((_b = x_range[1]) !== null && _b !== void 0 ? _b : 0) + node_separation * (i - main_sorted_index); + } + } + } +} +function handlePrivateCards({ tree, data_stash, private_cards_config }) { + const private_persons = {}; + const condition = private_cards_config.condition; + if (!condition) + return console.error('private_cards_config.condition is not set'); + tree.forEach(d => { + if (d.data._new_rel_data) + return; + const is_private = isPrivate(d.data.id); + if (is_private) + d.is_private = is_private; + return; + }); + function isPrivate(d_id) { + const parents_and_spouses_checked = []; + let is_private = false; + checkParentsAndSpouses(d_id); + private_persons[d_id] = is_private; + return is_private; + function checkParentsAndSpouses(d_id) { + if (is_private) + return; + if (private_persons.hasOwnProperty(d_id)) { + is_private = private_persons[d_id]; + return is_private; + } + const d = data_stash.find(d0 => d0.id === d_id); + if (!d) + throw new Error('no d'); + if (d._new_rel_data) + return; + if (condition(d)) { + is_private = true; + return true; + } + const rels = d.rels; + [...rels.parents, ...(rels.spouses || [])].forEach(d0_id => { + if (!d0_id) + return; + if (parents_and_spouses_checked.includes(d0_id)) + return; + parents_and_spouses_checked.push(d0_id); + checkParentsAndSpouses(d0_id); + }); + } + } +} +function getMaxDepth(d_id, data_stash) { + const datum = data_stash.find(d => d.id === d_id); + if (!datum) + throw new Error('no datum'); + const root_ancestry = d3.hierarchy(datum, d => hierarchyGetterParents(d)); + const root_progeny = d3.hierarchy(datum, d => hierarchyGetterChildren(d)); + return { + ancestry: root_ancestry.height, + progeny: root_progeny.height + }; + function hierarchyGetterChildren(d) { + return [...(d.rels.children || [])] + .map(id => data_stash.find(d => d.id === id)) + .filter(d => d && !d._new_rel_data && !d.to_add); + } + function hierarchyGetterParents(d) { + return d.rels.parents + .filter(d => d) + .map(id => data_stash.find(d => d.id === id)) + .filter(d => d && !d._new_rel_data && !d.to_add); + } +} + +function createNewPerson({ data, rels }) { + return { + id: generateUUID(), + data: data || {}, + rels: Object.assign({ parents: [], children: [], spouses: [] }, (rels || {})) + }; +} +function createNewPersonWithGenderFromRel({ data, rel_type, rel_datum }) { + const gender = getGenderFromRelative(rel_datum, rel_type); + data = Object.assign(data || {}, { gender }); + return createNewPerson({ data }); + function getGenderFromRelative(rel_datum, rel_type) { + return (["daughter", "mother"].includes(rel_type) || rel_type === "spouse" && rel_datum.data.gender === "M") ? "F" : "M"; + } +} +function addNewPerson({ data_stash, datum }) { + data_stash.push(datum); +} +function generateUUID() { + var d = new Date().getTime(); + var d2 = (performance && performance.now && (performance.now() * 1000)) || 0; //Time in microseconds since page-load or 0 if unsupported + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + var r = Math.random() * 16; + if (d > 0) { //Use timestamp until depleted + r = (d + r) % 16 | 0; + d = Math.floor(d / 16); + } + else { //Use microseconds since page-load if supported + r = (d2 + r) % 16 | 0; + d2 = Math.floor(d2 / 16); + } + return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); + }); +} + +function isAllRelativeDisplayed(d, data) { + const r = d.data.rels; + const all_rels = [...r.parents, ...(r.spouses || []), ...(r.children || [])].filter(v => v); + return all_rels.every(rel_id => data.some(d => d.data.id === rel_id)); +} +function calculateDelay(tree, d, transition_time) { + const delay_level = transition_time * .4; + const ancestry_levels = Math.max(...tree.data.map(d => d.is_ancestry ? d.depth : 0)); + let delay = d.depth * delay_level; + if ((d.depth !== 0 || !!d.spouse) && !d.is_ancestry) { + delay += (ancestry_levels) * delay_level; // after ancestry + if (d.spouse) + delay += delay_level; // spouse after bloodline + delay += (d.depth) * delay_level; // double the delay for each level because of additional spouse delay + } + return delay; +} + +function handleDuplicateSpouseToggle(tree) { + tree.forEach(d => { + if (!d.spouse) return + const spouse = d.spouse; + if (d.duplicate && spouse.data._tgdp_sp) { + const parent_id = spouse.data.main ? 'main' : spouse.parent.data.id; + if (spouse.data._tgdp_sp[parent_id]?.hasOwnProperty(d.data.id)) { + d._toggle = spouse.data._tgdp_sp[parent_id][d.data.id]; + } + } + }); +} + +function handleDuplicateHierarchyProgeny(root, data_stash, on_toggle_one_close_others=true) { + const progeny_duplicates = []; + loopChildren(root); + setToggleIds(progeny_duplicates); + + function loopChildren(d) { + if (!d.children) return + const p1 = d.data; + const spouses = (d.data.rels.spouses || []).map(id => data_stash.find(d => d.id === id)); + + const children_by_spouse = getChildrenBySpouse(d); + spouses.forEach(p2 => { + if (progeny_duplicates.some(d => d.some(d => checkIfDuplicate([p1, p2], [d.p1, d.p2])))) { + return + } + const duplicates = findDuplicates(d, p1, p2); + if (duplicates.length > 0) { + const all_duplicates = [{d, p1, p2}, ...duplicates]; + progeny_duplicates.push(all_duplicates); + assignDuplicateValues(all_duplicates); + handleToggleOff(all_duplicates); + } else { + let parent_id = root === d ? 'main' : d.parent.data.id; + stashTgdpSpouse(d, parent_id, p2); + (children_by_spouse[p2.id] || []).forEach(child => { + loopChildren(child); + }); + } + }); + } + + function assignDuplicateValues(all_duplicates) { + all_duplicates.forEach(({d, p1, p2}, i) => { + if (!d.data._tgdp_sp) d.data._tgdp_sp = {}; + let parent_id = root === d ? 'main' : d.parent.data.id; + unstashTgdpSpouse(d, parent_id, p2); + if (!d.data._tgdp_sp[parent_id]) d.data._tgdp_sp[parent_id] = {}; + let val = 1; + if (!d.data._tgdp_sp[parent_id].hasOwnProperty(p2.id)) d.data._tgdp_sp[parent_id][p2.id] = val; + else val = d.data._tgdp_sp[parent_id][p2.id]; + all_duplicates[i].val = val; + }); + + if (on_toggle_one_close_others) { + if (all_duplicates.every(d => d.val < 0)) { + const first_duplicate = all_duplicates.sort((a, b) => b.val - a.val)[0]; + const {d, p1, p2} = first_duplicate; + const parent_id = root === d ? 'main' : d.parent.data.id; + d.data._tgdp_sp[parent_id][p2.id] = 1; + } + + if (all_duplicates.filter(d => d.val > 0).length > 1) { + const latest_duplicate = all_duplicates.sort((a, b) => b.val - a.val)[0]; + all_duplicates.forEach(dupl => { + if (dupl === latest_duplicate) return + const {d, p1, p2} = dupl; + const parent_id = root === d ? 'main' : d.parent.data.id; + d.data._tgdp_sp[parent_id][p2.id] = -1; + }); + } + } + } + + function handleToggleOff(all_duplicates) { + all_duplicates.forEach(({d, p1, p2}) => { + const parent_id = root === d ? 'main' : d.parent.data.id; + if (d.data._tgdp_sp[parent_id][p2.id] < 0) { + const children_by_spouse = getChildrenBySpouse(d); + if (children_by_spouse[p2.id]) { + d.children = d.children.filter(c => !children_by_spouse[p2.id].includes(c)); + if (d.children.length === 0) delete d.children; + } + } + }); + } + + function stashTgdpSpouse(d, parent_id, p2) { + if (d.data._tgdp_sp && d.data._tgdp_sp[parent_id] && d.data._tgdp_sp[parent_id].hasOwnProperty(p2.id)) { + if (!d.data.__tgdp_sp) d.data.__tgdp_sp = {}; + if (!d.data.__tgdp_sp[parent_id]) d.data.__tgdp_sp[parent_id] = {}; + d.data.__tgdp_sp[parent_id][p2.id] = d.data._tgdp_sp[parent_id][p2.id]; + delete d.data._tgdp_sp[parent_id][p2.id]; + } + } + + function unstashTgdpSpouse(d, parent_id, p2) { + if (d.data.__tgdp_sp && d.data.__tgdp_sp[parent_id] && d.data.__tgdp_sp[parent_id].hasOwnProperty(p2.id)) { + d.data._tgdp_sp[parent_id][p2.id] = d.data.__tgdp_sp[parent_id][p2.id]; + delete d.data.__tgdp_sp[parent_id][p2.id]; + } + } + + function findDuplicates(datum, partner1, partner2) { + const duplicates = []; + checkChildren(root); + return duplicates + + function checkChildren(d) { + if (d === datum) return + if (d.children) { + const p1 = d.data; + const spouses = (d.data.rels.spouses || []).map(id => data_stash.find(d => d.id === id)); + const children_by_spouse = getChildrenBySpouse(d); + spouses.forEach(p2 => { + if (checkIfDuplicate([partner1, partner2], [p1, p2])) { + duplicates.push({d, p1, p2}); + } else { + (children_by_spouse[p2.id] || []).forEach(child => { + checkChildren(child); + }); + } + }); + } + } + } + + function checkIfDuplicate(arr1, arr2) { + return arr1.every(d => arr2.some(d0 => d.id === d0.id)) + } + + function getChildrenBySpouse(d) { + const children_by_spouse = {}; + const p1 = d; + (d.children || []).forEach(child => { + const ch_rels = child.data.rels; + const p2_id = ch_rels.parents[0] === p1.data.id ? ch_rels.parents[1] : ch_rels.parents[0]; + if (!children_by_spouse[p2_id]) children_by_spouse[p2_id] = []; + children_by_spouse[p2_id].push(child); + }); + return children_by_spouse + } + + function setToggleIds(progeny_duplicates) { + let toggle_id = 0; + progeny_duplicates.forEach(dupl_arr => { + toggle_id = toggle_id+1; + dupl_arr.forEach(d => { + if (!d.d._toggle_id_sp) d.d._toggle_id_sp = {}; + d.d._toggle_id_sp[d.p2.id] = toggle_id; + }); + }); + } +} + +function handleDuplicateHierarchyAncestry(root, on_toggle_one_close_others=true) { + const ancestry_duplicates = []; + + loopChildren(root); + + setToggleIds(ancestry_duplicates); + + + function loopChildren(d) { + if (d.children) { + if (ancestry_duplicates.some(d0 => d0.includes(d))) { + return + } + const duplicates = findDuplicates(d.children); + if (duplicates.length > 0) { + const all_duplicates = [d, ...duplicates]; + ancestry_duplicates.push(all_duplicates); + assignDuplicateValues(all_duplicates); + handleToggleOff(all_duplicates); + } else { + d.children.forEach(child => { + loopChildren(child); + }); + } + } + } + + function assignDuplicateValues(all_duplicates) { + all_duplicates.forEach(d => { + if (!d.data._tgdp) d.data._tgdp = {}; + const parent_id = root === d ? 'main' : d.parent.data.id; + if (!d.data._tgdp[parent_id]) d.data._tgdp[parent_id] = -1; + d._toggle = d.data._tgdp[parent_id]; + }); + + if (on_toggle_one_close_others) { + if (all_duplicates.every(d => d._toggle < 0)) { + const first_duplicate = all_duplicates.sort((a, b) => b._toggle - a._toggle)[0]; + const d= first_duplicate; + const parent_id = root === d ? 'main' : d.parent.data.id; + d.data._tgdp[parent_id] = 1; + } + + if (all_duplicates.filter(d => d._toggle > 0).length > 1) { + const latest_duplicate = all_duplicates.sort((a, b) => b._toggle - a._toggle)[0]; + all_duplicates.forEach(dupl => { + if (dupl === latest_duplicate) return + const d = dupl; + const parent_id = root === d ? 'main' : d.parent.data.id; + d.data._tgdp[parent_id] = -1; + }); + } + } + } + + function handleToggleOff(all_duplicates) { + all_duplicates.forEach(d => { + const parent_id = root === d ? 'main' : d.parent.data.id; + if (d.data._tgdp[parent_id] < 0) delete d.children; + }); + } + + function findDuplicates(children_1) { + const duplicates = []; + checkChildren(root); + return duplicates + + function checkChildren(d) { + if (d.children) { + if (checkIfDuplicate(children_1, d.children)) { + duplicates.push(d); + } else { + d.children.forEach(child => { + checkChildren(child); + }); + } + } + } + } + + function checkIfDuplicate(arr1, arr2) { + return arr1 !== arr2 && arr1.every(d => arr2.some(d0 => d.data.id === d0.data.id)) + } + + function setToggleIds(ancestry_duplicates) { + let toggle_id = 0; + ancestry_duplicates.forEach(dupl_arr => { + toggle_id = toggle_id+1; + dupl_arr.forEach(d => { + d._toggle_id = toggle_id; + }); + }); + } +} + +function formatData(data) { + data.forEach((d) => { + if (!d.rels.parents) + d.rels.parents = []; + if (!d.rels.spouses) + d.rels.spouses = []; + if (!d.rels.children) + d.rels.children = []; + convertFatherMotherToParents(d); + }); + return data; + function convertFatherMotherToParents(d) { + if (!d.rels.parents) + d.rels.parents = []; + if (d.rels.father) + d.rels.parents.push(d.rels.father); + if (d.rels.mother) + d.rels.parents.push(d.rels.mother); + delete d.rels.father; + delete d.rels.mother; + } +} +function formatDataForExport(data, legacy_format = false) { + data.forEach(d => { + var _a; + if (legacy_format) { + let father; + let mother; + (_a = d.rels.parents) === null || _a === void 0 ? void 0 : _a.forEach(p => { + const parent = data.find(d => d.id === p); + if (!parent) + throw new Error('Parent not found'); + if (parent.data.gender === "M") { + if (!father) + father = parent.id; + else + mother = parent.id; // for same sex parents, we set some parent to father and some to mother + } + if (parent.data.gender === "F") { + if (!mother) + mother = parent.id; + else + father = parent.id; // for same sex parents, we set some parent to father and some to mother + } + }); + if (father) + d.rels.father = father; + if (mother) + d.rels.mother = mother; + delete d.rels.parents; + } + if (d.rels.parents && d.rels.parents.length === 0) + delete d.rels.parents; + if (d.rels.spouses && d.rels.spouses.length === 0) + delete d.rels.spouses; + if (d.rels.children && d.rels.children.length === 0) + delete d.rels.children; + }); + return data; +} + +function calculateTree(data, { main_id = null, node_separation = 250, level_separation = 150, single_parent_empty_card = true, is_horizontal = false, one_level_rels = false, sortChildrenFunction = undefined, sortSpousesFunction = undefined, ancestry_depth = undefined, progeny_depth = undefined, show_siblings_of_main = false, modifyTreeHierarchy = undefined, private_cards_config = undefined, duplicate_branch_toggle = false, on_toggle_one_close_others = true, }) { + if (!data || !data.length) + throw new Error('No data'); + if (is_horizontal) + [node_separation, level_separation] = [level_separation, node_separation]; + const data_stash = single_parent_empty_card ? createRelsToAdd(data) : data; + if (!main_id || !data_stash.find(d => d.id === main_id)) + main_id = data_stash[0].id; + const main = data_stash.find(d => d.id === main_id); + if (!main) + throw new Error('Main not found'); + const tree_children = calculateTreePositions(main, 'children', false); + const tree_parents = calculateTreePositions(main, 'parents', true); + data_stash.forEach(d => d.main = d === main); + levelOutEachSide(tree_parents, tree_children); + const tree = mergeSides(tree_parents, tree_children); + setupChildrenAndParents(tree); + setupSpouses(tree, node_separation); + if (show_siblings_of_main && !one_level_rels) + setupSiblings({ tree, data_stash, node_separation, sortChildrenFunction }); + setupProgenyParentsPos(tree); + nodePositioning(tree); + tree.forEach(d => d.all_rels_displayed = isAllRelativeDisplayed(d, tree)); + if (private_cards_config) + handlePrivateCards({ tree, data_stash, private_cards_config }); + setupTid(tree); + // setupFromTo(tree) + if (duplicate_branch_toggle) + handleDuplicateSpouseToggle(tree); + const dim = calculateTreeDim(tree, node_separation, level_separation); + return { data: tree, data_stash, dim, main_id: main.id, is_horizontal }; + function calculateTreePositions(datum, rt, is_ancestry) { + const hierarchyGetter = rt === "children" ? hierarchyGetterChildren : hierarchyGetterParents; + const d3_tree = d3.tree().nodeSize([node_separation, level_separation]).separation(separation); + const root = d3.hierarchy(datum, hierarchyGetter); + trimTree(root, is_ancestry); + if (duplicate_branch_toggle) + handleDuplicateHierarchy(root, data_stash, is_ancestry); + if (modifyTreeHierarchy) + modifyTreeHierarchy(root, is_ancestry); + d3_tree(root); + const tree = root.descendants(); + tree.forEach(d => { + if (d.x === undefined) + d.x = 0; + if (d.y === undefined) + d.y = 0; + }); + return tree; + function separation(a, b) { + let offset = 1; + if (!is_ancestry) { + if (!sameParent(a, b)) + offset += .25; + if (!one_level_rels) { + if (someSpouses(a, b)) + offset += offsetOnPartners(a, b); + } + if (sameParent(a, b) && !sameBothParents(a, b)) + offset += .125; + } + return offset; + } + function sameParent(a, b) { return a.parent == b.parent; } + function sameBothParents(a, b) { + const parentsA = [...a.data.rels.parents].sort(); + const parentsB = [...b.data.rels.parents].sort(); + return parentsA.length === parentsB.length && parentsA.every((p, i) => p === parentsB[i]); + } + function hasSpouses(d) { return d.data.rels.spouses && d.data.rels.spouses.length > 0; } + function someSpouses(a, b) { return hasSpouses(a) || hasSpouses(b); } + function hierarchyGetterChildren(d) { + const children = [...(d.rels.children || [])].map(id => data_stash.find(d => d.id === id)).filter(d => d !== undefined); + if (sortChildrenFunction) + children.sort(sortChildrenFunction); // first sort by custom function if provided + sortAddNewChildren(children); // then put new children at the end + if (sortSpousesFunction) + sortSpousesFunction(d, data_stash); + sortChildrenWithSpouses(children, d, data_stash); // then sort by order of spouses + return children; + } + function hierarchyGetterParents(d) { + let parents = [...d.rels.parents]; + const p1 = data_stash.find(d => d.id === parents[0]); + if (p1 && p1.data.gender === "F") + parents.reverse(); + return parents + .filter(d => d).map(id => data_stash.find(d => d.id === id)).filter(d => d !== undefined); + } + function offsetOnPartners(a, b) { + return ((a.data.rels.spouses || []).length + (b.data.rels.spouses || []).length) * .5; + } + } + function levelOutEachSide(parents, children) { + const mid_diff = (parents[0].x - children[0].x) / 2; + parents.forEach(d => d.x -= mid_diff); + children.forEach(d => d.x += mid_diff); + } + function mergeSides(parents, children) { + parents.forEach(d => { d.is_ancestry = true; }); + parents.forEach(d => d.depth === 1 ? d.parent = children[0] : null); + return [...children, ...parents.slice(1)]; + } + function nodePositioning(tree) { + tree.forEach(d => { + d.y *= (d.is_ancestry ? -1 : 1); + if (is_horizontal) { + const d_x = d.x; + d.x = d.y; + d.y = d_x; + } + }); + } + function setupSpouses(tree, node_separation) { + for (let i = tree.length; i--;) { + const d = tree[i]; + if (!d.is_ancestry) { + let spouses = d.data.rels.spouses || []; + if (d._ignore_spouses) + spouses = spouses.filter(sp_id => !d._ignore_spouses.includes(sp_id)); + if (spouses.length > 0) { + if (one_level_rels && d.depth > 0) + continue; + const side = d.data.data.gender === "M" ? -1 : 1; // female on right + d.x += spouses.length / 2 * node_separation * side; + spouses.forEach((sp_id, i) => { + const spouse = { + data: data_stash.find(d0 => d0.id === sp_id), + added: true, + depth: d.depth, + spouse: d, + x: d.x - (node_separation * (i + 1)) * side, + y: d.y, + tid: `${d.data.id}-spouse-${i}`, + }; + spouse.sx = i > 0 ? spouse.x : spouse.x + (node_separation / 2) * side; + spouse.sy = i > 0 ? spouse.y : spouse.y + (node_separation / 2) * side; + if (!d.spouses) + d.spouses = []; + d.spouses.push(spouse); + tree.push(spouse); + }); + } + } + if (d.parents && d.parents.length === 2) { + const p1 = d.parents[0]; + const p2 = d.parents[1]; + const midd = p1.x - (p1.x - p2.x) / 2; + const x = (d, sp) => midd + (node_separation / 2) * (d.x < sp.x ? 1 : -1); + p2.x = x(p1, p2); + p1.x = x(p2, p1); + } + } + } + function setupProgenyParentsPos(tree) { + tree.forEach(d => { + if (d.is_ancestry) + return; + if (d.depth === 0) + return; + if (d.added) + return; + if (d.sibling) + return; + const p1 = d.parent; + const p2 = ((p1 === null || p1 === void 0 ? void 0 : p1.spouses) || []).find((d0) => d.data.rels.parents.includes(d0.data.id)); + if (p1 && p2) { + if (!p1.added && !p2.added) + console.error('no added spouse', p1, p2); + const added_spouse = p1.added ? p1 : p2; + setupParentPos(d, added_spouse); + } + else if (p1 || p2) { + const parent = p1 || p2; + if (!parent) + throw new Error('no progeny parent'); + parent.sx = parent.x; + parent.sy = parent.y; + setupParentPos(d, parent); + } + function setupParentPos(d, p) { + d.psx = !is_horizontal ? p.sx : p.y; + d.psy = !is_horizontal ? p.y : p.sx; + } + }); + } + function setupChildrenAndParents(tree) { + tree.forEach(d0 => { + delete d0.children; + tree.forEach(d1 => { + if (d1.parent === d0) { + if (d1.is_ancestry) { + if (!d0.parents) + d0.parents = []; + d0.parents.push(d1); + } + else { + if (!d0.children) + d0.children = []; + d0.children.push(d1); + } + } + }); + if (d0.parents && d0.parents.length === 2) { + const p1 = d0.parents[0]; + const p2 = d0.parents[1]; + p1.coparent = p2; + p2.coparent = p1; + } + }); + } + function calculateTreeDim(tree, node_separation, level_separation) { + if (is_horizontal) + [node_separation, level_separation] = [level_separation, node_separation]; + const w_extent = d3.extent(tree, (d) => d.x); + const h_extent = d3.extent(tree, (d) => d.y); + if (w_extent[0] === undefined || w_extent[1] === undefined || h_extent[0] === undefined || h_extent[1] === undefined) + throw new Error('No extent'); + return { + width: w_extent[1] - w_extent[0] + node_separation, height: h_extent[1] - h_extent[0] + level_separation, x_off: -w_extent[0] + node_separation / 2, y_off: -h_extent[0] + level_separation / 2 + }; + } + function createRelsToAdd(data) { + const to_add_spouses = []; + for (let i = 0; i < data.length; i++) { + const d = data[i]; + if (d.rels.children && d.rels.children.length > 0) { + if (!d.rels.spouses) + d.rels.spouses = []; + let to_add_spouse; + d.rels.children.forEach(d0 => { + const child = data.find(d1 => d1.id === d0); + if (child.rels.parents.length === 2) + return; + if (!to_add_spouse) { + to_add_spouse = findOrCreateToAddSpouse(d); + } + if (!to_add_spouse.rels.children) + to_add_spouse.rels.children = []; + to_add_spouse.rels.children.push(child.id); + if (child.rels.parents.length !== 1) + throw new Error('child has more than 1 parent'); + child.rels.parents.push(to_add_spouse.id); + }); + } + } + to_add_spouses.forEach(d => data.push(d)); + return data; + function findOrCreateToAddSpouse(d) { + const spouses = (d.rels.spouses || []).map(sp_id => data.find(d0 => d0.id === sp_id)).filter(d => d !== undefined); + return spouses.find(sp => sp.to_add) || createToAddSpouse(d); + } + function createToAddSpouse(d) { + const spouse = createNewPerson({ + data: { gender: d.data.gender === "M" ? "F" : "M" }, + rels: { spouses: [d.id] } + }); + spouse.to_add = true; + to_add_spouses.push(spouse); + if (!d.rels.spouses) + d.rels.spouses = []; + d.rels.spouses.push(spouse.id); + return spouse; + } + } + function trimTree(root, is_ancestry) { + let max_depth = is_ancestry ? ancestry_depth : progeny_depth; + if (one_level_rels) + max_depth = 1; + if (!max_depth && max_depth !== 0) + return root; + trimNode(root, 0); + return root; + function trimNode(node, depth) { + if (depth === max_depth) { + if (node.children) + delete node.children; + } + else if (node.children) { + node.children.forEach(child => { + trimNode(child, depth + 1); + }); + } + } + } + function handleDuplicateHierarchy(root, data_stash, is_ancestry) { + if (is_ancestry) + handleDuplicateHierarchyAncestry(root, on_toggle_one_close_others); + else + handleDuplicateHierarchyProgeny(root, data_stash, on_toggle_one_close_others); + } +} +function setupTid(tree) { + const ids = []; + tree.forEach(d => { + if (ids.includes(d.data.id)) { + const duplicates = tree.filter(d0 => d0.data.id === d.data.id); + duplicates.forEach((d0, i) => { + d0.tid = `${d.data.id}--x${i + 1}`; + d0.duplicate = duplicates.length; + ids.push(d.data.id); + }); + } + else { + d.tid = d.data.id; + ids.push(d.data.id); + } + }); +} +/** + * Calculate the tree + * @param options - The options for the tree + * @param options.data - The data for the tree + * @returns The tree + * @deprecated Use f3.calculateTree instead + */ +function CalculateTree(options) { + return calculateTreeWithV1Data(options.data, options); +} +/** + * Calculate the tree with v1 data + * @param data - The data for the tree + * @param options - The options for the tree + * @returns The tree + */ +function calculateTreeWithV1Data(data, options) { + const formatted_data = formatData(data); + return calculateTree(formatted_data, options); +} + +function createStore(initial_state) { + let onUpdate; + const state = Object.assign({ transition_time: 1000 }, initial_state); + state.main_id_history = []; + if (state.data) { + checkIfFmFormat(state.data); + formatData(state.data); + } + const store = { + state, + updateTree: (props) => { + if (!state.data || state.data.length === 0) + return; + state.tree = calcTree(); + if (!state.main_id && state.tree) + updateMainId(state.tree.main_id); + if (onUpdate) + onUpdate(props); + }, + updateData: (data) => { + checkIfFmFormat(data); + formatData(data); + state.data = data; + validateMainId(); + }, + updateMainId, + getMainId: () => state.main_id, + getData: () => state.data, + getTree: () => state.tree, + setOnUpdate: (f) => onUpdate = f, + getMainDatum, + getDatum, + getTreeMainDatum, + getTreeDatum, + getLastAvailableMainDatum, + methods: {}, + }; + return store; + function calcTree() { + const args = { + main_id: state.main_id, + }; + if (state.node_separation !== undefined) + args.node_separation = state.node_separation; + if (state.level_separation !== undefined) + args.level_separation = state.level_separation; + if (state.single_parent_empty_card !== undefined) + args.single_parent_empty_card = state.single_parent_empty_card; + if (state.is_horizontal !== undefined) + args.is_horizontal = state.is_horizontal; + if (state.one_level_rels !== undefined) + args.one_level_rels = state.one_level_rels; + if (state.modifyTreeHierarchy !== undefined) + args.modifyTreeHierarchy = state.modifyTreeHierarchy; + if (state.sortChildrenFunction !== undefined) + args.sortChildrenFunction = state.sortChildrenFunction; + if (state.sortSpousesFunction !== undefined) + args.sortSpousesFunction = state.sortSpousesFunction; + if (state.ancestry_depth !== undefined) + args.ancestry_depth = state.ancestry_depth; + if (state.progeny_depth !== undefined) + args.progeny_depth = state.progeny_depth; + if (state.show_siblings_of_main !== undefined) + args.show_siblings_of_main = state.show_siblings_of_main; + if (state.private_cards_config !== undefined) + args.private_cards_config = state.private_cards_config; + if (state.duplicate_branch_toggle !== undefined) + args.duplicate_branch_toggle = state.duplicate_branch_toggle; + return calculateTree(state.data, args); + } + function getMainDatum() { + const datum = state.data.find(d => d.id === state.main_id); + if (!datum) + throw new Error("Main datum not found"); + return datum; + } + function getDatum(id) { + const datum = state.data.find(d => d.id === id); + if (!datum) + return undefined; + return datum; + } + function getTreeMainDatum() { + if (!state.tree) + throw new Error("No tree"); + const found = state.tree.data.find(d => d.data.id === state.main_id); + if (!found) + throw new Error("No tree main datum"); + return found; + } + function getTreeDatum(id) { + if (!state.tree) + throw new Error("No tree"); + const found = state.tree.data.find(d => d.data.id === id); + if (!found) + return undefined; + return found; + } + function updateMainId(id) { + if (id === state.main_id) + return; + state.main_id_history = state.main_id_history.filter(d => d !== id).slice(-10); + state.main_id_history.push(id); + state.main_id = id; + } + function validateMainId() { + if (state.main_id) { + const mainExists = state.data.find(d => d.id === state.main_id); + if (!mainExists && state.data.length > 0) { + // Set first datum as main if current main doesn't exist + updateMainId(state.data[0].id); + } + } + else { + if (state.data.length > 0) { + updateMainId(state.data[0].id); + } + } + } + // if main_id is deleted, get the last available main_id + function getLastAvailableMainDatum() { + let main_id = state.main_id_history.slice(0).reverse().find(id => getDatum(id)); + if (!main_id && state.data.length > 0) + main_id = state.data[0].id; + if (!main_id) + throw new Error("No main id"); + if (main_id !== state.main_id) + updateMainId(main_id); + const main_datum = getDatum(main_id); + if (!main_datum) + throw new Error("Main datum not found"); + return main_datum; + } + function checkIfFmFormat(data) { + if (state.legacy_format !== undefined) + return; // already checked + for (let d of data) { + if (d.rels.father || d.rels.mother) { + state.legacy_format = true; + return; + } + } + state.legacy_format = false; + } +} + +function positionTree({ t, svg, transition_time = 2000 }) { + const el_listener = getZoomListener(svg); + const zoom = el_listener.__zoomObj; + d3.select(el_listener).transition().duration(transition_time || 0).delay(transition_time ? 100 : 0) // delay 100 because of weird error of undefined something in d3 zoom + .call(zoom.transform, d3.zoomIdentity.scale(t.k).translate(t.x, t.y)); +} +function treeFit({ svg, svg_dim, tree_dim, transition_time }) { + const t = calculateTreeFit(svg_dim, tree_dim); + positionTree({ t, svg, transition_time }); +} +function calculateTreeFit(svg_dim, tree_dim) { + let k = Math.min(svg_dim.width / tree_dim.width, svg_dim.height / tree_dim.height); + if (k > 1) + k = 1; + const x = tree_dim.x_off + (svg_dim.width - tree_dim.width * k) / k / 2; + const y = tree_dim.y_off + (svg_dim.height - tree_dim.height * k) / k / 2; + return { k, x, y }; +} +function cardToMiddle({ datum, svg, svg_dim, scale, transition_time }) { + const k = scale || 1, x = svg_dim.width / 2 - datum.x * k, y = svg_dim.height / 2 - datum.y * k, t = { k, x: x / k, y: y / k }; + positionTree({ t, svg, transition_time }); +} +function manualZoom({ amount, svg, transition_time = 500 }) { + const el_listener = getZoomListener(svg); + const zoom = el_listener.__zoomObj; + if (!zoom) + throw new Error('Zoom object not found'); + d3.select(el_listener).transition().duration(transition_time || 0).delay(transition_time ? 100 : 0) // delay 100 because of weird error of undefined something in d3 zoom + .call(zoom.scaleBy, amount); +} +function getCurrentZoom(svg) { + const el_listener = getZoomListener(svg); + const currentTransform = d3.zoomTransform(el_listener); + return currentTransform; +} +function zoomTo(svg, zoom_level) { + const el_listener = getZoomListener(svg); + const currentTransform = d3.zoomTransform(el_listener); + manualZoom({ amount: zoom_level / currentTransform.k, svg }); +} +function getZoomListener(svg) { + const el_listener = svg.__zoomObj ? svg : svg.parentNode; + if (!el_listener.__zoomObj) + throw new Error('Zoom object not found'); + return el_listener; +} +function setupZoom(el, props = {}) { + if (el.__zoom) { + console.log('zoom already setup'); + return; + } + const view = el.querySelector('.view'); + const zoom = d3.zoom().on("zoom", (props.onZoom || zoomed)); + d3.select(el).call(zoom); + el.__zoomObj = zoom; + if (props.zoom_polite) + zoom.filter(zoomFilter); + function zoomed(e) { + d3.select(view).attr("transform", e.transform); + } + function zoomFilter(e) { + if (e.type === "wheel" && !e.ctrlKey) + return false; + else if (e.touches && e.touches.length < 2) + return false; + else + return true; + } +} + +function createLinks(d, is_horizontal = false) { + const links = []; + // d.spouses is always added to non-ancestry side for main blodline nodes + // d.coparent is added to ancestry side + if (d.spouses || d.coparent) + handleSpouse(d); + handleAncestrySide(d); + handleProgenySide(d); + return links; + function handleAncestrySide(d) { + if (!d.parents) + return; + const p1 = d.parents[0]; + const p2 = d.parents[1] || p1; + const p = { x: getMid(p1, p2, 'x'), y: getMid(p1, p2, 'y') }; + links.push({ + d: Link(d, p), + _d: () => { + const _d = { x: d.x, y: d.y }, _p = { x: d.x, y: d.y }; + return Link(_d, _p); + }, + curve: true, + id: linkId(d, p1, p2), + depth: d.depth + 1, + is_ancestry: true, + source: d, + target: [p1, p2] + }); + } + function handleProgenySide(d) { + if (!d.children || d.children.length === 0) + return; + d.children.forEach((child, i) => { + const other_parent = otherParent(child, d) || d; + const sx = other_parent.sx; + if (typeof sx !== 'number') + throw new Error('sx is not a number'); + const parent_pos = !is_horizontal ? { x: sx, y: d.y } : { x: d.x, y: sx }; + links.push({ + d: Link(child, parent_pos), + _d: () => Link(parent_pos, { x: _or(parent_pos, 'x'), y: _or(parent_pos, 'y') }), + curve: true, + id: linkId(child, d, other_parent), + depth: d.depth + 1, + is_ancestry: false, + source: [d, other_parent], + target: child + }); + }); + } + function handleSpouse(d) { + if (d.spouses) { + d.spouses.forEach(spouse => links.push(createSpouseLink(d, spouse))); + } + else if (d.coparent) { + links.push(createSpouseLink(d, d.coparent)); + } + function createSpouseLink(d, spouse) { + return { + d: [[d.x, d.y], [spouse.x, spouse.y]], + _d: () => [ + d.is_ancestry ? [_or(d, 'x') - .0001, _or(d, 'y')] : [d.x, d.y], // add -.0001 to line to have some length if d.x === spouse.x + d.is_ancestry ? [_or(spouse, 'x'), _or(spouse, 'y')] : [d.x - .0001, d.y] + ], + curve: false, + id: linkId(d, spouse), + depth: d.depth, + spouse: true, + is_ancestry: spouse.is_ancestry, + source: d, + target: spouse + }; + } + } + /// + function getMid(d1, d2, side, is_ = false) { + if (is_) + return _or(d1, side) - (_or(d1, side) - _or(d2, side)) / 2; + else + return d1[side] - (d1[side] - d2[side]) / 2; + } + function _or(d, side) { + const n = d.hasOwnProperty(`_${side}`) ? d[`_${side}`] : d[side]; + if (typeof n !== 'number') + throw new Error(`${side} is not a number`); + return n; + } + function Link(d, p) { + return is_horizontal ? LinkHorizontal(d, p) : LinkVertical(d, p); + } + function LinkVertical(d, p) { + const hy = (d.y + (p.y - d.y) / 2); + return [ + [d.x, d.y], + [d.x, hy], + [d.x, hy], + [p.x, hy], + [p.x, hy], + [p.x, p.y], + ]; + } + function LinkHorizontal(d, p) { + const hx = (d.x + (p.x - d.x) / 2); + return [ + [d.x, d.y], + [hx, d.y], + [hx, d.y], + [hx, p.y], + [hx, p.y], + [p.x, p.y], + ]; + } + function linkId(...args) { + return args.map(d => d.tid).sort().join(", "); // make unique id + } + function otherParent(child, p1) { + const p2 = (p1.spouses || []).find(d => child.data.rels.parents.includes(d.data.id)); + return p2; + } +} + +function updateLinks(svg, tree, props = {}) { + const links_data_dct = tree.data.reduce((acc, d) => { + createLinks(d, tree.is_horizontal).forEach(l => acc[l.id] = l); + return acc; + }, {}); + const links_data = Object.values(links_data_dct); + const link = d3 + .select(svg) + .select(".links_view") + .selectAll("path.link") + .data(links_data, d => d.id); + if (props.transition_time === undefined) + throw new Error('transition_time is undefined'); + const link_exit = link.exit(); + const link_enter = link.enter().append("path").attr("class", "link"); + const link_update = link_enter.merge(link); + link_exit.each(linkExit); + link_enter.each(linkEnter); + link_update.each(linkUpdate); + function linkEnter(d) { + d3.select(this).attr("fill", "none").attr("stroke", "#fff").attr("stroke-width", 1).style("opacity", 0) + .attr("d", createPath(d, true)); + } + function linkUpdate(d) { + const path = d3.select(this); + const delay = props.initial ? calculateDelay(tree, d, props.transition_time) : 0; + path.transition('path').duration(props.transition_time).delay(delay).attr("d", createPath(d)).style("opacity", 1); + } + function linkExit(d) { + const path = d3.select(this); + path.transition('op').duration(800).style("opacity", 0); + path.transition('path').duration(props.transition_time).attr("d", createPath(d, true)) + .on("end", () => path.remove()); + } +} +function createPath(d, is_ = false) { + const line = d3.line().curve(d3.curveMonotoneY); + const lineCurve = d3.line().curve(d3.curveBasis); + const path_data = is_ ? d._d() : d.d; + if (!d.curve) + return line(path_data); + else + return lineCurve(path_data); +} + +function updateCardsSvg(svg, tree, Card, props = {}) { + const card = d3 + .select(svg) + .select(".cards_view") + .selectAll("g.card_cont") + .data(tree.data, d => d.data.id); + const card_exit = card.exit(); + const card_enter = card.enter().append("g").attr("class", "card_cont"); + const card_update = card_enter.merge(card); + card_exit.each(d => calculateEnterAndExitPositions(d, false, true)); + card_enter.each(d => calculateEnterAndExitPositions(d, true, false)); + card_exit.each(cardExit); + card.each(cardUpdateNoEnter); + card_enter.each(cardEnter); + card_update.each(cardUpdate); + function cardEnter(d) { + d3.select(this) + .attr("transform", `translate(${d._x}, ${d._y})`) + .style("opacity", 0); + Card.call(this, d); + } + function cardUpdateNoEnter(d) { } + function cardUpdate(d) { + Card.call(this, d); + const delay = props.initial ? calculateDelay(tree, d, props.transition_time) : 0; + d3.select(this).transition().duration(props.transition_time).delay(delay).attr("transform", `translate(${d.x}, ${d.y})`).style("opacity", 1); + } + function cardExit(d) { + const tree_datum = d; + const pos = tree_datum ? [tree_datum._x, tree_datum._y] : [0, 0]; + const g = d3.select(this); + g.transition().duration(props.transition_time) + .style("opacity", 0) + .attr("transform", `translate(${pos[0]}, ${pos[1]})`) + .on("end", () => g.remove()); + } +} + +function updateCardsHtml(svg, tree, Card, props = {}) { + const div = getHtmlDiv(svg); + const card = d3.select(div).select(".cards_view").selectAll("div.card_cont").data(tree.data, d => d.tid); + const card_exit = card.exit(); + const card_enter = card.enter().append("div").attr("class", "card_cont").style('pointer-events', 'none'); + const card_update = card_enter.merge(card); + card_exit.each(d => calculateEnterAndExitPositions(d, false, true)); + card_enter.each(d => calculateEnterAndExitPositions(d, true, false)); + card_exit.each(cardExit); + card.each(cardUpdateNoEnter); + card_enter.each(cardEnter); + card_update.each(cardUpdate); + function cardEnter(d) { + d3.select(this) + .style('position', 'absolute') + .style('top', '0').style('left', '0') + .style("transform", `translate(${d._x}px, ${d._y}px)`) + .style("opacity", 0); + Card.call(this, d); + } + function cardUpdateNoEnter(d) { } + function cardUpdate(d) { + Card.call(this, d); + const delay = props.initial ? calculateDelay(tree, d, props.transition_time) : 0; + d3.select(this).transition().duration(props.transition_time).delay(delay).style("transform", `translate(${d.x}px, ${d.y}px)`).style("opacity", 1); + } + function cardExit(d) { + const tree_datum = d; + const pos = tree_datum ? [tree_datum._x, tree_datum._y] : [0, 0]; + const g = d3.select(this); + g.transition().duration(props.transition_time) + .style("opacity", 0) + .style("transform", `translate(${pos[0]}px, ${pos[1]}px)`) + .on("end", () => g.remove()); + } + function getHtmlDiv(svg) { + if (props.cardHtmlDiv) + return props.cardHtmlDiv; + const canvas = svg.closest('#f3Canvas'); + if (!canvas) + throw new Error('canvas not found'); + const htmlSvg = canvas.querySelector('#htmlSvg'); + if (!htmlSvg) + throw new Error('htmlSvg not found'); + return htmlSvg; + } +} + +function createSvg(cont, props = {}) { + const svg_dim = cont.getBoundingClientRect(); + const svg_html = (` + + + + + + + + + + + `); + const f3Canvas = getOrCreateF3Canvas(cont); + const temp_div = d3.create('div').node(); + temp_div.innerHTML = svg_html; + const svg = temp_div.querySelector('svg'); + f3Canvas.appendChild(svg); + cont.appendChild(f3Canvas); + setupZoom(f3Canvas, props); + return svg; + function getOrCreateF3Canvas(cont) { + let f3Canvas = cont.querySelector('#f3Canvas'); + if (!f3Canvas) { + f3Canvas = d3.create('div').attr('id', 'f3Canvas').attr('style', 'position: relative; overflow: hidden; width: 100%; height: 100%;').node(); + } + return f3Canvas; + } +} + +function htmlContSetup(cont) { + const getSvgView = () => cont.querySelector('svg .view'); + const getHtmlView = () => cont.querySelector('#htmlSvg .cards_view'); + createSvg(cont, { onZoom: onZoomSetup(getSvgView, getHtmlView) }); + createHtmlSvg(cont); + return { + svg: cont.querySelector('svg.main_svg'), + svgView: cont.querySelector('svg .view'), + htmlSvg: cont.querySelector('#htmlSvg'), + htmlView: cont.querySelector('#htmlSvg .cards_view') + }; +} +function createHtmlSvg(cont) { + const f3Canvas = d3.select(cont).select('#f3Canvas'); + const cardHtml = f3Canvas.append('div').attr('id', 'htmlSvg') + .attr('style', 'position: absolute; width: 100%; height: 100%; z-index: 2; top: 0; left: 0'); + cardHtml.append('div').attr('class', 'cards_view').style('transform-origin', '0 0'); + return cardHtml.node(); +} +function onZoomSetup(getSvgView, getHtmlView) { + return function onZoom(e) { + const t = e.transform; + d3.select(getSvgView()).style('transform', `translate(${t.x}px, ${t.y}px) scale(${t.k}) `); + d3.select(getHtmlView()).style('transform', `translate(${t.x}px, ${t.y}px) scale(${t.k}) `); + }; +} + +var htmlHandlers = /*#__PURE__*/Object.freeze({ + __proto__: null, + createHtmlSvg: createHtmlSvg, + default: htmlContSetup, + onZoomSetup: onZoomSetup +}); + +function cardComponentSetup(cont) { + const getSvgView = () => cont.querySelector('svg .view'); + const getHtmlSvg = () => cont.querySelector('#htmlSvg'); + const getHtmlView = () => cont.querySelector('#htmlSvg .cards_view'); + createSvg(cont, { onZoom: onZoomSetup(getSvgView, getHtmlView) }); + d3.select(getHtmlSvg()).append("div").attr("class", "cards_view_fake").style('display', 'none'); // important for handling data + return setupReactiveTreeData(getHtmlSvg); +} +function setupReactiveTreeData(getHtmlSvg) { + let tree_data = []; + return function getReactiveTreeData(new_tree_data) { + const tree_data_exit = getTreeDataExit(new_tree_data, tree_data); + tree_data = [...new_tree_data, ...tree_data_exit]; + assignUniqueIdToTreeData(getCardsViewFake(getHtmlSvg), tree_data); + return tree_data; + }; + function assignUniqueIdToTreeData(div, tree_data) { + const card = d3.select(div).selectAll("div.card_cont_2fake").data(tree_data, d => d.data.id); // how this doesn't break if there is multiple cards with the same id? + const card_exit = card.exit(); + const card_enter = card.enter().append("div").attr("class", "card_cont_2fake").style('display', 'none').attr("data-id", () => Math.random()); + const card_update = card_enter.merge(card); + card_exit.each(cardExit); + card_enter.each(cardEnter); + card_update.each(cardUpdate); + function cardEnter(d) { + d.unique_id = d3.select(this).attr("data-id"); + } + function cardUpdate(d) { + d.unique_id = d3.select(this).attr("data-id"); + } + function cardExit(d) { + if (!d) + return; + d.unique_id = d3.select(this).attr("data-id"); + d3.select(this).remove(); + } + } + function getTreeDataExit(new_tree_data, old_tree_data) { + if (old_tree_data.length > 0) { + return old_tree_data.filter(d => !new_tree_data.find(t => t.data.id === d.data.id)); + } + else { + return []; + } + } +} +function getCardsViewFake(getHtmlSvg) { + return d3.select(getHtmlSvg()).select("div.cards_view_fake").node(); +} +/** @deprecated This export will be removed in a future version. Use setupReactiveTreeData instead. */ +function setupHtmlSvg(getHtmlSvg) { + d3.select(getHtmlSvg()).append("div").attr("class", "cards_view_fake").style('display', 'none'); // important for handling data +} +/** @deprecated This export will be removed in a future version. Use setupReactiveTreeData instead. */ +const _setupReactiveTreeData = setupReactiveTreeData; +/** @deprecated This export will be removed in a future version. Use setupReactiveTreeData instead. */ +function getUniqueId(d) { + return d.unique_id; +} + +function updateCardsComponent(svg, tree, Card, props = {}) { + const div = props.cardHtmlDiv ? props.cardHtmlDiv : svg.closest('#f3Canvas').querySelector('#htmlSvg'); + const card = d3.select(getCardsViewFake(() => div)).selectAll("div.card_cont_fake").data(tree.data, d => d.data.id); + const card_exit = card.exit(); + const card_enter = card.enter().append("div").attr("class", "card_cont_fake").style('display', 'none'); + const card_update = card_enter.merge(card); + card_exit.each(d => calculateEnterAndExitPositions(d, false, true)); + card_enter.each(d => calculateEnterAndExitPositions(d, true, false)); + card_exit.each(cardExit); + card.each(cardUpdateNoEnter); + card_enter.each(cardEnter); + card_update.each(cardUpdate); + function cardEnter(d) { + const card_element = d3.select(Card(d)); + card_element + .style('position', 'absolute') + .style('top', '0').style('left', '0').style("opacity", 0) + .style("transform", `translate(${d._x}px, ${d._y}px)`); + } + function cardUpdateNoEnter(d) { } + function cardUpdate(d) { + const card_element = d3.select(Card(d)); + const delay = props.initial ? calculateDelay(tree, d, props.transition_time) : 0; + card_element.transition().duration(props.transition_time).delay(delay).style("transform", `translate(${d.x}px, ${d.y}px)`).style("opacity", 1); + } + function cardExit(d) { + const tree_datum = d; + const pos = tree_datum ? [tree_datum._x, tree_datum._y] : [0, 0]; + const card_element = d3.select(Card(d)); + const g = d3.select(this); + card_element.transition().duration(props.transition_time).style("opacity", 0).style("transform", `translate(${pos[0]}px, ${pos[1]}px)`) + .on("end", () => g.remove()); // remove the card_cont_fake + } +} + +function view (tree, svg, Card, props = {}) { + props.initial = props.hasOwnProperty('initial') ? props.initial : !d3.select(svg.parentNode).select('.card_cont').node(); + props.transition_time = props.hasOwnProperty('transition_time') ? props.transition_time : 1000; + if (props.cardComponent) + updateCardsComponent(svg, tree, Card, props); + else if (props.cardHtml) + updateCardsHtml(svg, tree, Card, props); + else + updateCardsSvg(svg, tree, Card, props); + updateLinks(svg, tree, props); + const tree_position = props.tree_position || 'fit'; + if (props.initial) + treeFit({ svg, svg_dim: svg.getBoundingClientRect(), tree_dim: tree.dim, transition_time: 0 }); + else if (tree_position === 'fit') + treeFit({ svg, svg_dim: svg.getBoundingClientRect(), tree_dim: tree.dim, transition_time: props.transition_time }); + else if (tree_position === 'main_to_middle') + cardToMiddle({ datum: tree.data[0], svg, svg_dim: svg.getBoundingClientRect(), scale: props.scale, transition_time: props.transition_time }); + else ; + return true; +} + +function cardChangeMain(store, { d }) { + store.updateMainId(d.data.id); + store.updateTree({}); + return true; +} + +function checkIfRelativesConnectedWithoutPerson(datum, data_stash) { + const r = datum.rels; + const r_ids = [...r.parents, ...(r.spouses || []), ...(r.children || [])].filter(r_id => !!r_id); + for (const r_id of r_ids) { + const person = data_stash.find(d => d.id === r_id); + if (!checkIfConnectedToFirstPerson(person, data_stash, [datum.id])) + return false; + } + return true; +} +function checkIfConnectedToFirstPerson(datum, data_stash, exclude_ids = []) { + const first_person = data_stash[0]; + if (datum.id === first_person.id) + return true; + const rels_checked = [...exclude_ids]; + let connected = false; + checkRels(datum); + return connected; + function checkRels(d0) { + if (connected) + return; + const r = d0.rels; + const r_ids = [...r.parents, ...(r.spouses || []), ...(r.children || [])].filter(r_id => !!r_id); + r_ids.forEach(r_id => { + if (rels_checked.includes(r_id)) + return; + rels_checked.push(r_id); + const person = data_stash.find(d => d.id === r_id); + if (person.id === first_person.id) + connected = true; + else + checkRels(person); + }); + } +} + +function submitFormData(datum, data_stash, form_data) { + form_data.forEach((v, k) => datum.data[k] = v); + syncRelReference(datum, data_stash); + if (datum.to_add) + delete datum.to_add; + if (datum.unknown) + delete datum.unknown; +} +function syncRelReference(datum, data_stash) { + Object.keys(datum.data).forEach(k => { + if (k.includes('__ref__')) { + const rel_id = k.split('__ref__')[1]; + const rel = data_stash.find(d => d.id === rel_id); + if (!rel) + return; + const ref_field_id = k.split('__ref__')[0] + '__ref__' + datum.id; + rel.data[ref_field_id] = datum.data[k]; + } + }); +} +function onDeleteSyncRelReference(datum, data_stash) { + Object.keys(datum.data).forEach(k => { + if (k.includes('__ref__')) { + const rel_id = k.split('__ref__')[1]; + const rel = data_stash.find(d => d.id === rel_id); + if (!rel) + return; + const ref_field_id = k.split('__ref__')[0] + '__ref__' + datum.id; + delete rel.data[ref_field_id]; + } + }); +} +function moveToAddToAdded(datum, data_stash) { + delete datum.to_add; + return datum; +} +function removeToAdd(datum, data_stash) { + deletePerson(datum, data_stash, false); + return false; +} +function deletePerson(datum, data_stash, clean_to_add = true) { + if (!checkIfRelativesConnectedWithoutPerson(datum, data_stash)) { + changeToUnknown(); + return { success: true }; + } + else { + executeDelete(); + if (clean_to_add) + removeToAddFromData(data_stash); + return { success: true }; + } + function executeDelete() { + data_stash.forEach(d => { + for (let k in d.rels) { + if (!d.rels.hasOwnProperty(k)) + continue; + const key = k; + if (Array.isArray(d.rels[key]) && d.rels[key].includes(datum.id)) { + d.rels[key].splice(d.rels[key].findIndex(did => did === datum.id), 1); + } + } + }); + onDeleteSyncRelReference(datum, data_stash); + data_stash.splice(data_stash.findIndex(d => d.id === datum.id), 1); + if (data_stash.length === 0) + data_stash.push(createNewPerson({ data: { gender: 'M' } })); + } + function changeToUnknown() { + onDeleteSyncRelReference(datum, data_stash); + datum.data = { + gender: datum.data.gender, + }; + datum.unknown = true; + } +} +function cleanupDataJson(data) { + removeToAddFromData(data); + data.forEach(d => { + delete d.main; + delete d._tgdp; + delete d._tgdp_sp; + delete d.__tgdp_sp; + }); + data.forEach(d => { + Object.keys(d).forEach(k => { + if (k[0] === '_') + console.error('key starts with _', k); + }); + }); + return data; +} +function removeToAddFromData(data) { + for (let i = data.length - 1; i >= 0; i--) { + if (data[i].to_add) + removeToAdd(data[i], data); + } +} + +function userIcon() { + return (` + + ${bgCircle()} + + + `); +} +function userEditIcon() { + return (` + + ${bgCircle()} + + + `); +} +function userPlusIcon() { + return (` + + ${bgCircle()} + + + `); +} +function userPlusCloseIcon() { + return (` + + ${bgCircle()} + + + + `); +} +function plusIcon() { + return (` + + ${bgCircle()} + + + `); +} +function pencilIcon() { + return (` + + ${bgCircle()} + + + `); +} +function pencilOffIcon() { + return (` + + ${bgCircle()} + + + `); +} +function trashIcon() { + return (` + + ${bgCircle()} + + + `); +} +function historyBackIcon() { + return (` + + ${bgCircle()} + + + `); +} +function historyForwardIcon() { + return (` + + ${bgCircle()} + + + `); +} +function personIcon() { + return (` + + + + `); +} +function miniTreeIcon() { + return (` + + + + + + + + + + + `); +} +function toggleIconOn() { + return (` + + ${bgCircle()} + + + + `); +} +function toggleIconOff() { + return (` + + ${bgCircle()} + + + + `); +} +function chevronDownIcon() { + return (` + + ${bgCircle()} + + + `); +} +function chevronUpIcon() { + return (` + + ${bgCircle()} + + + `); +} +function linkOffIcon() { + return (` + + ${bgCircle()} + + + `); +} +function infoIcon() { + return (` + + ${bgCircle()} + + + `); +} +function userSvgIcon() { return svgWrapper(userIcon()); } +function userEditSvgIcon() { return svgWrapper(userEditIcon()); } +function userPlusSvgIcon() { return svgWrapper(userPlusIcon()); } +function userPlusCloseSvgIcon() { return svgWrapper(userPlusCloseIcon()); } +function plusSvgIcon() { return svgWrapper(plusIcon()); } +function pencilSvgIcon() { return svgWrapper(pencilIcon()); } +function pencilOffSvgIcon() { return svgWrapper(pencilOffIcon()); } +function trashSvgIcon() { return svgWrapper(trashIcon()); } +function historyBackSvgIcon() { return svgWrapper(historyBackIcon()); } +function historyForwardSvgIcon() { return svgWrapper(historyForwardIcon()); } +function personSvgIcon() { return svgWrapper(personIcon(), '0 0 512 512'); } +function miniTreeSvgIcon() { return svgWrapper(miniTreeIcon(), '0 0 72 25'); } +function toggleSvgIconOn() { return svgWrapper(toggleIconOn()); } +function toggleSvgIconOff() { return svgWrapper(toggleIconOff()); } +function chevronDownSvgIcon() { return svgWrapper(chevronDownIcon()); } +function chevronUpSvgIcon() { return svgWrapper(chevronUpIcon()); } +function linkOffSvgIcon() { return svgWrapper(linkOffIcon()); } +function infoSvgIcon() { return svgWrapper(infoIcon()); } +function svgWrapper(icon, viewBox = '0 0 24 24') { + const match = icon.match(/data-icon="([^"]+)"/); + const dataIcon = match ? `data-icon="${match[1]}"` : ''; + return (` + + ${icon} + + `); +} +function bgCircle() { + return (` + + `); +} + +var icons = /*#__PURE__*/Object.freeze({ + __proto__: null, + chevronDownIcon: chevronDownIcon, + chevronDownSvgIcon: chevronDownSvgIcon, + chevronUpIcon: chevronUpIcon, + chevronUpSvgIcon: chevronUpSvgIcon, + historyBackIcon: historyBackIcon, + historyBackSvgIcon: historyBackSvgIcon, + historyForwardIcon: historyForwardIcon, + historyForwardSvgIcon: historyForwardSvgIcon, + infoIcon: infoIcon, + infoSvgIcon: infoSvgIcon, + linkOffIcon: linkOffIcon, + linkOffSvgIcon: linkOffSvgIcon, + miniTreeIcon: miniTreeIcon, + miniTreeSvgIcon: miniTreeSvgIcon, + pencilIcon: pencilIcon, + pencilOffIcon: pencilOffIcon, + pencilOffSvgIcon: pencilOffSvgIcon, + pencilSvgIcon: pencilSvgIcon, + personIcon: personIcon, + personSvgIcon: personSvgIcon, + plusIcon: plusIcon, + plusSvgIcon: plusSvgIcon, + toggleIconOff: toggleIconOff, + toggleIconOn: toggleIconOn, + toggleSvgIconOff: toggleSvgIconOff, + toggleSvgIconOn: toggleSvgIconOn, + trashIcon: trashIcon, + trashSvgIcon: trashSvgIcon, + userEditIcon: userEditIcon, + userEditSvgIcon: userEditSvgIcon, + userIcon: userIcon, + userPlusCloseIcon: userPlusCloseIcon, + userPlusCloseSvgIcon: userPlusCloseSvgIcon, + userPlusIcon: userPlusIcon, + userPlusSvgIcon: userPlusSvgIcon, + userSvgIcon: userSvgIcon +}); + +function getHtmlNew(form_creator) { + return (` +
+ ${closeBtn()} +

${form_creator.title}

+ ${genderRadio(form_creator)} + + ${fields(form_creator)} + +
+ + +
+ + ${form_creator.linkExistingRelative ? addLinkExistingRelative(form_creator) : ''} +
+ `); +} +function getHtmlEdit(form_creator) { + return (` +
+ ${closeBtn()} +
+ ${!form_creator.no_edit ? addRelativeBtn(form_creator) : ''} + ${form_creator.no_edit ? spaceDiv() : editBtn(form_creator)} +
+ + ${genderRadio(form_creator)} + + ${fields(form_creator)} + +
+ + +
+ + ${form_creator.linkExistingRelative ? addLinkExistingRelative(form_creator) : ''} + +
+ ${deleteBtn(form_creator)} + + ${removeRelativeBtn(form_creator)} +
+ `); +} +function deleteBtn(form_creator) { + return (` +
+ +
+ `); +} +function removeRelativeBtn(form_creator) { + return (` +
+ +
+ `); +} +function addRelativeBtn(form_creator) { + return (` + + ${form_creator.addRelativeActive ? userPlusCloseSvgIcon() : userPlusSvgIcon()} + + `); +} +function editBtn(form_creator) { + return (` + + ${form_creator.editable ? pencilOffSvgIcon() : pencilSvgIcon()} + + `); +} +function genderRadio(form_creator) { + if (!form_creator.editable) + return ''; + return (` +
+ ${form_creator.gender_field.options.map(option => (` + + `)).join('')} +
+ `); +} +function fields(form_creator) { + if (!form_creator.editable) + return infoField(); + let fields_html = ''; + form_creator.fields.forEach(field => { + if (field.type === 'text') { + fields_html += ` +
+ + +
`; + } + else if (field.type === 'textarea') { + fields_html += ` +
+ + +
`; + } + else if (field.type === 'select') { + const select_field = field; + fields_html += ` +
+ + +
`; + } + else if (field.type === 'rel_reference') { + fields_html += ` +
+ + +
`; + } + }); + return fields_html; + function infoField() { + let fields_html = ''; + form_creator.fields.forEach(field => { + var _a; + if (field.type === 'rel_reference') { + if (!field.initial_value) + return; + fields_html += ` +
+ ${field.label} - ${field.rel_label} + ${field.initial_value || ''} +
`; + } + else if (field.type === 'select') { + const select_field = field; + if (!field.initial_value) + return; + fields_html += ` +
+ ${select_field.label} + ${((_a = select_field.options.find(option => option.value === select_field.initial_value)) === null || _a === void 0 ? void 0 : _a.label) || ''} +
`; + } + else { + fields_html += ` +
+ ${field.label} + ${field.initial_value || ''} +
`; + } + }); + return fields_html; + } +} +function addLinkExistingRelative(form_creator) { + const title = form_creator.linkExistingRelative.hasOwnProperty('title') ? form_creator.linkExistingRelative.title : 'Profile already exists?'; + const select_placeholder = form_creator.linkExistingRelative.hasOwnProperty('select_placeholder') ? form_creator.linkExistingRelative.select_placeholder : 'Select profile'; + const options = form_creator.linkExistingRelative.options; + return (` +
+
+ +
+ `); +} +function closeBtn() { + return (` + + × + + `); +} +function spaceDiv() { + return `
`; +} + +function createFormNew(form_creator, closeCallback) { + return createForm(form_creator, closeCallback); +} +function createFormEdit(form_creator, closeCallback) { + return createForm(form_creator, closeCallback); +} +function createForm(form_creator, closeCallback) { + const is_new = isNewRelFormCreator(form_creator); + const formContainer = document.createElement('div'); + reload(); + return formContainer; + function reload() { + const formHtml = is_new ? getHtmlNew(form_creator) : getHtmlEdit(form_creator); + formContainer.innerHTML = formHtml; + setupEventListenersBase(formContainer, form_creator, closeCallback, reload); + if (is_new) + setupEventListenersNew(formContainer, form_creator); + else + setupEventListenersEdit(formContainer, form_creator, reload); + if (form_creator.onFormCreation) { + form_creator.onFormCreation({ + cont: formContainer, + form_creator: form_creator + }); + } + } + function isNewRelFormCreator(form_creator) { + return 'new_rel' in form_creator; + } +} +function setupEventListenersBase(formContainer, form_creator, closeCallback, reload) { + const form = formContainer.querySelector('form'); + form.addEventListener('submit', form_creator.onSubmit); + const cancel_btn = form.querySelector('.f3-cancel-btn'); + cancel_btn.addEventListener('click', onCancel); + const close_btn = form.querySelector('.f3-close-btn'); + close_btn.addEventListener('click', closeCallback); + function onCancel() { + form_creator.editable = false; + if (form_creator.onCancel) + form_creator.onCancel(); + reload(); + } +} +function setupEventListenersNew(formContainer, form_creator) { + const form = formContainer.querySelector('form'); + const link_existing_relative_select = form.querySelector('.f3-link-existing-relative select'); + if (link_existing_relative_select) { + link_existing_relative_select.addEventListener('change', form_creator.linkExistingRelative.onSelect); + } +} +function setupEventListenersEdit(formContainer, form_creator, reload) { + const form = formContainer.querySelector('form'); + const edit_btn = form.querySelector('.f3-edit-btn'); + if (edit_btn) + edit_btn.addEventListener('click', onEdit); + const delete_btn = form.querySelector('.f3-delete-btn'); + if (delete_btn && form_creator.onDelete) { + delete_btn.addEventListener('click', form_creator.onDelete); + } + const add_relative_btn = form.querySelector('.f3-add-relative-btn'); + if (add_relative_btn && form_creator.addRelative) { + add_relative_btn.addEventListener('click', () => { + if (form_creator.addRelativeActive) + form_creator.addRelativeCancel(); + else + form_creator.addRelative(); + form_creator.addRelativeActive = !form_creator.addRelativeActive; + reload(); + }); + } + const remove_relative_btn = form.querySelector('.f3-remove-relative-btn'); + if (remove_relative_btn && form_creator.removeRelative) { + remove_relative_btn.addEventListener('click', () => { + if (form_creator.removeRelativeActive) + form_creator.removeRelativeCancel(); + else + form_creator.removeRelative(); + form_creator.removeRelativeActive = !form_creator.removeRelativeActive; + reload(); + }); + } + const link_existing_relative_select = form.querySelector('.f3-link-existing-relative select'); + if (link_existing_relative_select) { + link_existing_relative_select.addEventListener('change', form_creator.linkExistingRelative.onSelect); + } + function onEdit() { + form_creator.editable = !form_creator.editable; + reload(); + } +} + +function createHistory(store, getStoreDataCopy, onUpdate) { + let history = []; + let history_index = -1; + return { + changed, + back, + forward, + canForward, + canBack + }; + function changed() { + if (history_index < history.length - 1) + history = history.slice(0, history_index + 1); + const clean_data = getStoreDataCopy(); + clean_data.main_id = store.getMainId(); + history.push(clean_data); + history_index++; + } + function back() { + if (!canBack()) + return; + history_index--; + updateData(history[history_index]); + } + function forward() { + if (!canForward()) + return; + history_index++; + updateData(history[history_index]); + } + function canForward() { + return history_index < history.length - 1; + } + function canBack() { + return history_index > 0; + } + function updateData(data) { + const current_main_id = store.getMainId(); + data = JSON.parse(JSON.stringify(data)); + if (!data.find(d => d.id === current_main_id)) + store.updateMainId(data.main_id); + store.updateData(data); + onUpdate(); + } +} +function createHistoryControls(cont, history) { + const history_controls = d3.select(cont).append("div").attr("class", "f3-history-controls"); + cont.insertBefore(history_controls.node(), cont.firstChild); + const back_btn = history_controls.append("button").attr("class", "f3-back-button").on("click", () => { + history.back(); + updateButtons(); + }); + const forward_btn = history_controls.append("button").attr("class", "f3-forward-button").on("click", () => { + history.forward(); + updateButtons(); + }); + back_btn.html(historyBackSvgIcon()); + forward_btn.html(historyForwardSvgIcon()); + return { + back_btn: back_btn.node(), + forward_btn: forward_btn.node(), + updateButtons, + destroy + }; + function updateButtons() { + back_btn.classed("disabled", !history.canBack()); + forward_btn.classed("disabled", !history.canForward()); + if (!history.canBack() && !history.canForward()) { + history_controls.style("opacity", 0).style("pointer-events", "none"); + } + else { + history_controls.style("opacity", 1).style("pointer-events", "auto"); + } + } + function destroy() { + d3.select(cont).select('.f3-history-controls').remove(); + } +} + +var handlers = /*#__PURE__*/Object.freeze({ + __proto__: null, + addNewPerson: addNewPerson, + calculateDelay: calculateDelay, + calculateTreeFit: calculateTreeFit, + cardChangeMain: cardChangeMain, + cardComponentSetup: cardComponentSetup, + cardToMiddle: cardToMiddle, + checkIfConnectedToFirstPerson: checkIfConnectedToFirstPerson, + checkIfRelativesConnectedWithoutPerson: checkIfRelativesConnectedWithoutPerson, + cleanupDataJson: cleanupDataJson, + createFormEdit: createFormEdit, + createFormNew: createFormNew, + createHistory: createHistory, + createHistoryControls: createHistoryControls, + createNewPerson: createNewPerson, + createNewPersonWithGenderFromRel: createNewPersonWithGenderFromRel, + deletePerson: deletePerson, + getCurrentZoom: getCurrentZoom, + htmlContSetup: htmlContSetup, + isAllRelativeDisplayed: isAllRelativeDisplayed, + manualZoom: manualZoom, + moveToAddToAdded: moveToAddToAdded, + onDeleteSyncRelReference: onDeleteSyncRelReference, + removeToAdd: removeToAdd, + removeToAddFromData: removeToAddFromData, + setupZoom: setupZoom, + submitFormData: submitFormData, + syncRelReference: syncRelReference, + treeFit: treeFit, + zoomTo: zoomTo +}); + +function CardBody({ d, card_dim, card_display }) { + return { template: (` + + + ${CardText({ d, card_dim, card_display }).template} + + `) + }; +} +function CardBodyAddNewRel({ d, card_dim, label }) { + return { template: (` + + + + ${label} + + + `) + }; +} +function CardText({ d, card_dim, card_display }) { + return { template: (` + + + + + ${Array.isArray(card_display) ? card_display.map(cd => `${cd(d.data)}`).join('\n') : card_display(d.data)} + + + + + + `) + }; +} +function CardBodyOutline({ d, card_dim, is_new }) { + return { template: (` + + `) + }; +} +function MiniTree({ d, card_dim }) { + return ({ template: (` + + + + + + + + + + + `) }); +} +function CardImage({ d, image, card_dim, maleIcon, femaleIcon }) { + return ({ template: (` + + ${image + ? `` + : (d.data.data.gender === "F" && false) ? femaleIcon({ card_dim }) + : (d.data.data.gender === "M" && false) ? maleIcon({ card_dim }) + : GenderlessIcon()} + + `) }); + function GenderlessIcon() { + return (` + + + + + + + `); + } +} +function appendTemplate(template, parent, is_first) { + const g = document.createElementNS("http://www.w3.org/2000/svg", 'g'); + g.innerHTML = template; + if (is_first) + parent.insertBefore(g, parent.firstChild); + else + parent.appendChild(g); +} + +const CardElements = { + miniTree, + cardBody, + cardImage +}; +function miniTree(d, props) { + if (d.data.to_add) + return; + const card_dim = props.card_dim; + if (d.all_rels_displayed) + return; + const g = d3.create('svg:g').html(MiniTree({ d, card_dim }).template); + g.on("click", function (e) { + e.stopPropagation(); + if (props.onMiniTreeClick) + props.onMiniTreeClick.call(this, e, d); + else + cardChangeMain(props.store, { d }); + }); + return g.node(); +} +function cardBody(d, props) { + const card_dim = props.card_dim; + const g = d3.create('svg:g').html(CardBody({ d, card_dim, card_display: props.card_display }).template); + g.on("click", function (e) { + e.stopPropagation(); + if (props.onCardClick) + props.onCardClick.call(this, e, d); + else + cardChangeMain(props.store, { d }); + }); + return g.node(); +} +function cardImage(d, props) { + if (d.data.to_add) + return; + const card_dim = props.card_dim; + const g = d3.create('svg:g').html(CardImage({ d, image: d.data.data.avatar || null, card_dim, maleIcon: undefined, femaleIcon: undefined }).template); + return g.node(); +} +function appendElement(el_maybe, parent, is_first = false) { + if (!el_maybe) + return; + if (is_first) + parent.insertBefore(el_maybe, parent.firstChild); + else + parent.appendChild(el_maybe); +} + +function handleCardDuplicateToggle(node, d, is_horizontal, updateTree) { + if (!d.hasOwnProperty('_toggle')) return + + const card = node.querySelector('.card'); + const card_inner = card.querySelector('.card-inner'); + const card_width = node.querySelector('.card').offsetWidth; + node.querySelector('.card').offsetHeight; + let toggle_is_off; + let toggle_id; + const pos = {}; + if (d.spouse) { + const spouse = d.spouse; + const parent_id = spouse.data.main ? 'main' : spouse.parent.data.id; + toggle_is_off = spouse.data._tgdp_sp[parent_id][d.data.id] < 0; + pos.top = 60; + pos.left = d.sx-d.x-30+card_width/2; + if (is_horizontal) { + pos.top = d.sy - d.x + 4; + pos.left = card_width/2 + 4; + if ((Math.abs(d.sx - d.y)) < 10) pos.left = card_width - 4; + } + toggle_id = spouse._toggle_id_sp ? spouse._toggle_id_sp[d.data.id] : -1; + if (toggle_id === -1) return + } else { + const parent_id = d.data.main ? 'main' : d.parent.data.id; + toggle_is_off = d.data._tgdp[parent_id] < 0; + pos.top = -65; + pos.left = -30+card_width/2; + if (is_horizontal) { + pos.top = 5; + pos.left = -55; + } + toggle_id = d._toggle_id; + } + + card_inner.style.zIndex = 1; + + const toggle_div = d3.select(card) + .append('div') + .attr('class', 'f3-toggle-div') + .attr('style', 'cursor: pointer; width: 60px; height: 60px;position: absolute; z-index: -1;') + .style('top', pos.top+'px') + .style('left', pos.left+'px') + .on('click', (e) => { + e.stopPropagation(); + if (d.spouse) { + const spouse = d.spouse; + const parent_id = spouse.data.main ? 'main' : spouse.parent.data.id; + if (!spouse.data._tgdp_sp[parent_id].hasOwnProperty(d.data.id)) console.error('no toggle', d, spouse); + let val = spouse.data._tgdp_sp[parent_id][d.data.id]; + if (val < 0) val = new Date().getTime(); + else val = -new Date().getTime(); + spouse.data._tgdp_sp[parent_id][d.data.id] = val; + } else { + const parent_id = d.data.main ? 'main' : d.parent.data.id; + let val = d.data._tgdp[parent_id]; + if (val < 0) val = new Date().getTime(); + else val = -new Date().getTime(); + d.data._tgdp[parent_id] = val; + } + + updateTree(); + }); + + toggle_div + .append('div') + .html(toggle_is_off ? toggleSvgIconOff() : toggleSvgIconOn()) + .select('svg') + .classed('f3-toggle-icon', true) + .style('color', toggle_is_off ? '#585656' : '#61bf52') + .style('padding', '0'); + + d3.select(card) + .select('.f3-toggle-icon .f3-small-circle') + .style('fill', '#fff'); + + d3.select(card) + .select('.f3-toggle-icon') + .append('text') + .attr('transform', toggle_is_off ? 'translate(10.6, 14.5)' : 'translate(4.1, 14.5)') + .attr('fill', toggle_is_off ? '#fff' : '#fff') + .attr('font-size', '7px') + .text('C'+toggle_id); + + + if (toggle_is_off) { + let transform; + if (d.is_ancestry) { + if (is_horizontal) transform = 'translate(5, -30)rotate(-90)'; + else transform = 'translate(0, -10)'; + } else { + if (is_horizontal) transform = 'translate(11, -22)rotate(90)'; + else transform = 'translate(-7, -32)rotate(180)'; + } + d3.select(card) + .select('.f3-toggle-div') + .insert('div') + .html(miniTreeSvgIcon()) + .select('svg') + .attr('style', 'position: absolute; z-index: -1;top: 0;left: 0;border-radius: 0;') + .style('width', '66px') + .style('height', '112px') + .attr('transform', transform) + .attr('viewBox', '0 0 72 125') + .select('line') + .attr('y1', d.is_ancestry ? '62' : '92'); + } +} + +function CardHtml$2(props) { + const cardInner = props.style === 'default' ? cardInnerDefault + : props.style === 'imageCircleRect' ? cardInnerImageCircleRect + : props.style === 'imageCircle' ? cardInnerImageCircle + : props.style === 'imageRect' ? cardInnerImageRect + : props.style === 'rect' ? cardInnerRect + : cardInnerDefault; + return function (d) { + this.innerHTML = (` +
+ ${props.mini_tree ? getMiniTree(d) : ''} + ${(props.cardInnerHtmlCreator && !d.data._new_rel_data) ? props.cardInnerHtmlCreator(d) : cardInner(d)} +
+ `); + this.querySelector('.card').addEventListener('click', (e) => props.onCardClick(e, d)); + if (props.onCardUpdate) + props.onCardUpdate.call(this, d); + if (props.onCardMouseenter) + d3.select(this).select('.card').on('mouseenter', e => props.onCardMouseenter(e, d)); + if (props.onCardMouseleave) + d3.select(this).select('.card').on('mouseleave', e => props.onCardMouseleave(e, d)); + if (d.duplicate) + handleCardDuplicateHover(this, d); + if (props.duplicate_branch_toggle) + handleCardDuplicateToggle(this, d, props.store.state.is_horizontal, props.store.updateTree); + if (location.origin.includes('localhost')) { + d.__node = this.querySelector('.card'); + d.__label = d.data.data['first name']; + if (d.data.to_add) { + const spouse = d.spouse || d.coparent || null; + if (spouse) + d3.select(this).select('.card').attr('data-to-add', spouse.data.data['first name']); + } + } + }; + function getCardInnerImageCircle(d) { + return (` +
+ ${d.data.data[props.cardImageField] ? `` : noImageIcon(d)} +
${textDisplay(d)}
+ ${d.duplicate ? getCardDuplicateTag(d) : ''} +
+ `); + } + function getCardInnerImageRect(d) { + return (` +
+ ${d.data.data[props.cardImageField] ? `` : noImageIcon(d)} +
${textDisplay(d)}
+ ${d.duplicate ? getCardDuplicateTag(d) : ''} +
+ `); + } + function getCardInnerRect(d) { + return (` +
+ ${textDisplay(d)} + ${d.duplicate ? getCardDuplicateTag(d) : ''} +
+ `); + } + function textDisplay(d) { + if (d.data._new_rel_data) + return newRelDataDisplay(d); + if (d.data.to_add) + return `
${props.empty_card_label || 'ADD'}
`; + if (d.data.unknown) + return `
${props.unknown_card_label || 'UNKNOWN'}
`; + return (` + ${props.card_display.map(display => `
${display(d.data)}
`).join('')} + `); + } + function newRelDataDisplay(d) { + const attr_list = []; + attr_list.push(`data-rel-type="${d.data._new_rel_data.rel_type}"`); + if (['son', 'daughter'].includes(d.data._new_rel_data.rel_type)) + attr_list.push(`data-other-parent-id="${d.data._new_rel_data.other_parent_id}"`); + return `
${d.data._new_rel_data.label}
`; + } + function getMiniTree(d) { + if (!props.mini_tree) + return ''; + if (d.data.to_add) + return ''; + if (d.data._new_rel_data) + return ''; + if (d.all_rels_displayed) + return ''; + return `
${miniTreeSvgIcon()}
`; + } + function cardInnerImageCircleRect(d) { + return d.data.data[props.cardImageField] ? cardInnerImageCircle(d) : cardInnerRect(d); + } + function cardInnerDefault(d) { + return getCardInnerImageRect(d); + } + function cardInnerImageCircle(d) { + return getCardInnerImageCircle(d); + } + function cardInnerImageRect(d) { + return getCardInnerImageRect(d); + } + function cardInnerRect(d) { + return getCardInnerRect(d); + } + function getClassList(d) { + const class_list = []; + if (d.data.data.gender === 'M') + class_list.push('card-male'); + else if (d.data.data.gender === 'F') + class_list.push('card-female'); + else + class_list.push('card-genderless'); + class_list.push(`card-depth-${d.is_ancestry ? -d.depth : d.depth}`); + if (d.data.main) + class_list.push('card-main'); + if (d.data._new_rel_data) + class_list.push('card-new-rel'); + if (d.data.to_add) + class_list.push('card-to-add'); + if (d.data.unknown) + class_list.push('card-unknown'); + return class_list; + } + function getCardStyle() { + let style = 'style="'; + if (props.card_dim.w || props.card_dim.h) { + style += `width: ${props.card_dim.w}px; min-height: ${props.card_dim.h}px;`; + if (props.card_dim.height_auto) + style += 'height: auto;'; + else + style += `height: ${props.card_dim.h}px;`; + } + else { + return ''; + } + style += '"'; + return style; + } + function getCardImageStyle() { + let style = 'style="position: relative;'; + if (props.card_dim.img_w || props.card_dim.img_h || props.card_dim.img_x || props.card_dim.img_y) { + style += `width: ${props.card_dim.img_w}px; height: ${props.card_dim.img_h}px;`; + style += `left: ${props.card_dim.img_x}px; top: ${props.card_dim.img_y}px;`; + } + else { + return ''; + } + style += '"'; + return style; + } + function noImageIcon(d) { + if (d.data._new_rel_data) + return `
${plusSvgIcon()}
`; + return `
${props.defaultPersonIcon ? props.defaultPersonIcon(d) : personSvgIcon()}
`; + } + function getCardDuplicateTag(d) { + return `
x${d.duplicate}
`; + } + function handleCardDuplicateHover(node, d) { + d3.select(node).on('mouseenter', e => { + d3.select(node.closest('.cards_view')).selectAll('.card_cont').select('.card').classed('f3-card-duplicate-hover', d0 => d0.data.id === d.data.id); + }); + d3.select(node).on('mouseleave', e => { + d3.select(node.closest('.cards_view')).selectAll('.card_cont').select('.card').classed('f3-card-duplicate-hover', false); + }); + } +} + +function setupCardSvgDefs(svg, card_dim) { + if (svg.querySelector("defs#f3CardDef")) + return; + svg.insertAdjacentHTML('afterbegin', (` + + + + + + + + + + + + + `)); + function curvedRectPath(dim, curve, no_curve_corners) { + const { w, h } = dim, c = curve, ncc = no_curve_corners || [], ncc_check = (corner) => ncc.includes(corner), lx = ncc_check('lx') ? `M0,0` : `M0,${c} Q 0,0 5,0`, rx = ncc_check('rx') ? `H${w}` : `H${w - c} Q ${w},0 ${w},5`, ry = ncc_check('ry') ? `V${h}` : `V${h - c} Q ${w},${h} ${w - c},${h}`, ly = ncc_check('ly') ? `H0` : `H${c} Q 0,${h} 0,${h - c}`; + return (`${lx} ${rx} ${ry} ${ly} z`); + } +} +function updateCardSvgDefs(svg, card_dim) { + if (svg.querySelector("defs#f3CardDef")) { + svg.querySelector("defs#f3CardDef").remove(); + } + setupCardSvgDefs(svg, card_dim); +} + +function CardSvg$2(props) { + props = setupProps(props); + setupCardSvgDefs(props.svg, props.card_dim); + return function (d) { + const gender_class = d.data.data.gender === 'M' ? 'card-male' : d.data.data.gender === 'F' ? 'card-female' : 'card-genderless'; + const card_dim = props.card_dim; + const card = d3.create('svg:g').attr('class', `card ${gender_class}`).attr('transform', `translate(${[-card_dim.w / 2, -card_dim.h / 2]})`); + card.append('g').attr('class', 'card-inner').attr('clip-path', 'url(#card_clip)'); + this.innerHTML = ''; + this.appendChild(card.node()); + card.on("click", function (e) { + e.stopPropagation(); + props.onCardClick.call(this, e, d); + }); + if (d.data._new_rel_data) { + appendTemplate(CardBodyOutline({ d, card_dim, is_new: d.data.to_add }).template, card.node(), true); + appendTemplate(CardBodyAddNewRel({ d, card_dim, label: d.data._new_rel_data.label }).template, this.querySelector('.card-inner'), true); + d3.select(this.querySelector('.card-inner')) + .append('g') + .attr('class', 'card-edit-icon') + .attr('fill', 'currentColor') + .attr('transform', `translate(-1,2)scale(${card_dim.img_h / 22})`) + .html(plusIcon()); + } + else { + appendTemplate(CardBodyOutline({ d, card_dim, is_new: d.data.to_add }).template, card.node(), true); + appendTemplate(CardBody({ d, card_dim, card_display: props.card_display }).template, this.querySelector('.card-inner'), false); + if (props.img) + appendElement(CardElements.cardImage(d, props), this.querySelector('.card')); + if (props.mini_tree) + appendElement(CardElements.miniTree(d, props), this.querySelector('.card'), true); + } + if (props.onCardUpdate) + props.onCardUpdate.call(this, d); + }; + function setupProps(props) { + const default_props = { + img: true, + mini_tree: true, + link_break: false, + card_dim: { w: 220, h: 70, text_x: 75, text_y: 15, img_w: 60, img_h: 60, img_x: 5, img_y: 5 } + }; + if (!props) + props = {}; + for (const k in default_props) { + if (typeof props[k] === 'undefined') + props[k] = default_props[k]; + } + return props; + } +} +/** + * @deprecated Use cardSvg instead. This export will be removed in a future version. + */ +function Card(props) { + if (props.onCardClick === undefined) + props.onCardClick = (e, d) => { + props.store.updateMainId(d.data.id); + props.store.updateTree({}); + }; + return CardSvg$2(props); +} + +function createInfoPopup (cont, onClose) { return new InfoPopup(cont, onClose); } +class InfoPopup { + constructor(cont, onClose) { + this.cont = cont; + this.active = false; + this.onClose = onClose; + this.popup_cont = d3.select(this.cont).append('div').attr('class', 'f3-popup').node(); + this.create(); + } + create() { + const popup = d3.select(this.popup_cont); + popup.html(` +
+ × +
+
+ `); + popup.select('.f3-popup-close').on('click', () => { + this.close(); + }); + popup.on('click', (event) => { + if (event.target == popup.node()) { + this.close(); + } + }); + } + activate(content) { + const popup_content_inner = d3.select(this.popup_cont).select('.f3-popup-content-inner').node(); + if (content) + popup_content_inner.appendChild(content); + this.open(); + } + open() { + this.active = true; + } + close() { + this.popup_cont.remove(); + this.active = false; + if (this.onClose) + this.onClose(); + } +} + +// https://support.ancestry.co.uk/s/article/Understanding-Kinship-Terms +function calculateKinships(d_id, data_stash, kinship_info_config) { + const main_datum = data_stash.find(d => d.id === d_id); + const kinships = {}; + loopCheck(main_datum.id, 'self', 0); + setupHalfKinships(kinships); + if (kinship_info_config.show_in_law) + setupInLawKinships(kinships, data_stash); + setupKinshipsGender(kinships); + return kinships; + function loopCheck(d_id, kinship, depth, prev_rel_id = undefined) { + if (!d_id) + return; + // if (kinships[d_id] && kinships[d_id] !== kinship) console.error('kinship mismatch, kinship 1: ', kinships[d_id], 'kinship 2: ', kinship) + if (kinships[d_id]) + return; + if (kinship) + kinships[d_id] = kinship; + const datum = data_stash.find(d => d.id === d_id); + const rels = datum.rels; + if (kinship === 'self') { + rels.parents.forEach(p_id => loopCheck(p_id, 'parent', depth - 1, d_id)); + (rels.spouses || []).forEach(id => loopCheck(id, 'spouse', depth)); + (rels.children || []).forEach(id => loopCheck(id, 'child', depth + 1)); + } + else if (kinship === 'parent') { + rels.parents.forEach(p_id => loopCheck(p_id, 'grandparent', depth - 1, d_id)); + (rels.children || []).forEach(id => { + if (prev_rel_id && prev_rel_id === id) + return; + loopCheck(id, 'sibling', depth + 1); + }); + } + else if (kinship === 'spouse') ; + else if (kinship === 'child') { + (rels.children || []).forEach(id => loopCheck(id, 'grandchild', depth + 1)); + } + else if (kinship === 'sibling') { + (rels.children || []).forEach(id => loopCheck(id, 'nephew', depth + 1)); + } + else if (kinship === 'grandparent') { + if (!prev_rel_id) + console.error(`${kinship} should have prev_rel_id`); + rels.parents.forEach(p_id => loopCheck(p_id, 'great-grandparent', depth - 1, d_id)); + (rels.children || []).forEach(id => { + if (prev_rel_id && prev_rel_id === id) + return; + loopCheck(id, 'uncle', depth + 1); + }); + } + else if (kinship.includes('grandchild')) { + (rels.children || []).forEach(id => loopCheck(id, getGreatKinship(kinship, depth + 1), depth + 1)); + } + else if (kinship.includes('great-grandparent')) { + if (!prev_rel_id) + console.error(`${kinship} should have prev_rel_id`); + rels.parents.forEach(p_id => loopCheck(p_id, getGreatKinship(kinship, depth - 1), depth - 1, d_id)); + (rels.children || []).forEach(id => { + if (prev_rel_id && prev_rel_id === id) + return; + const great_count = getGreatCount(depth + 1); + if (great_count === 0) + loopCheck(id, 'granduncle', depth + 1); + else if (great_count > 0) + loopCheck(id, getGreatKinship('granduncle', depth + 1), depth + 1); + else + console.error(`${kinship} should have great_count > -1`); + }); + } + else if (kinship === 'nephew') { + (rels.children || []).forEach(id => loopCheck(id, 'grandnephew', depth + 1)); + } + else if (kinship.includes('grandnephew')) { + (rels.children || []).forEach(id => loopCheck(id, getGreatKinship(kinship, depth + 1), depth + 1)); + } + else if (kinship === 'uncle') { + (rels.children || []).forEach(id => loopCheck(id, '1st Cousin', depth + 1)); + } + else if (kinship === 'granduncle') { + (rels.children || []).forEach(id => loopCheck(id, '1st Cousin 1x removed', depth + 1)); + } + else if (kinship.includes('great-granduncle')) { + const child_depth = depth + 1; + const removed_count = Math.abs(child_depth); + (rels.children || []).forEach(id => loopCheck(id, `1st Cousin ${removed_count}x removed`, child_depth)); + } + else if (kinship.slice(4).startsWith('Cousin')) { + (rels.children || []).forEach(id => { + const child_depth = depth + 1; + const removed_count = Math.abs(child_depth); + const cousin_count = +kinship[0]; + if (child_depth === 0) { + loopCheck(id, `${getOrdinal(cousin_count + 1)} Cousin`, child_depth); + } + else if (child_depth < 0) { + loopCheck(id, `${getOrdinal(cousin_count + 1)} Cousin ${removed_count}x removed`, child_depth); + } + else if (child_depth > 0) { + loopCheck(id, `${getOrdinal(cousin_count)} Cousin ${removed_count}x removed`, child_depth); + } + }); + } + else + console.error(`${kinship} not found`); + } + function setupHalfKinships(kinships) { + const half_kinships = []; + Object.keys(kinships).forEach(d_id => { + const kinship = kinships[d_id]; + if (kinship.includes('child')) + return; + if (kinship === 'spouse') + return; + const same_ancestors = findSameAncestor(main_datum.id, d_id, data_stash); + if (!same_ancestors) + return console.error(`${data_stash.find(d => d.id === d_id).data} not found in main_ancestry`); + if (same_ancestors.is_half_kin) + half_kinships.push(d_id); + }); + half_kinships.forEach(d_id => { + kinships[d_id] = `Half ${kinships[d_id]}`; + }); + } + function setupInLawKinships(kinships, data_stash) { + Object.keys(kinships).forEach(d_id => { + const kinship = kinships[d_id]; + const datum = data_stash.find(d => d.id === d_id); + if (kinship === 'spouse') { + const siblings = []; + datum.rels.parents.forEach(p_id => (getD(p_id).rels.children || []).forEach(d_id => siblings.push(d_id))); + siblings.forEach(sibling_id => { if (!kinships[sibling_id]) + kinships[sibling_id] = 'sibling-in-law'; }); // gender label is added in setupKinshipsGender + } + if (kinship === 'sibling') { + (datum.rels.spouses || []).forEach(spouse_id => { + if (!kinships[spouse_id]) + kinships[spouse_id] = 'sibling-in-law'; + }); + } + if (kinship === 'child') { + (datum.rels.spouses || []).forEach(spouse_id => { if (!kinships[spouse_id]) + kinships[spouse_id] = 'child-in-law'; }); // gender label is added in setupKinshipsGender + } + if (kinship === 'uncle') { + (datum.rels.spouses || []).forEach(spouse_id => { if (!kinships[spouse_id]) + kinships[spouse_id] = 'uncle-in-law'; }); // gender label is added in setupKinshipsGender + } + if (kinship.includes('Cousin')) { + (datum.rels.spouses || []).forEach(spouse_id => { if (!kinships[spouse_id]) + kinships[spouse_id] = `${kinship} in-law`; }); // gender label is added in setupKinshipsGender + } + }); + } + function setupKinshipsGender(kinships) { + Object.keys(kinships).forEach(d_id => { + const kinship = kinships[d_id]; + const datum = data_stash.find(d => d.id === d_id); + const gender = datum.data.gender; + if (kinship.includes('parent')) { + const rel_type_general = 'parent'; + const rel_type = gender === 'M' ? 'father' : gender === 'F' ? 'mother' : rel_type_general; + kinships[d_id] = kinships[d_id].replace('parent', rel_type); + } + else if (kinship.includes('sibling')) { + const rel_type_general = 'sibling'; + const rel_type = gender === 'M' ? 'brother' : gender === 'F' ? 'sister' : rel_type_general; + kinships[d_id] = kinships[d_id].replace('sibling', rel_type); + } + else if (kinship.includes('child')) { + const rel_type_general = 'child'; + const rel_type = gender === 'M' ? 'son' : gender === 'F' ? 'daughter' : rel_type_general; + kinships[d_id] = kinships[d_id].replace('child', rel_type); + } + else if (kinship.includes('uncle')) { + const rel_type_general = 'aunt/uncle'; + const rel_type = gender === 'M' ? 'uncle' : gender === 'F' ? 'aunt' : rel_type_general; + kinships[d_id] = kinships[d_id].replace('uncle', rel_type); + } + else if (kinship.includes('nephew')) { + const rel_type_general = 'neice/nephew'; + const rel_type = gender === 'M' ? 'nephew' : gender === 'F' ? 'niece' : rel_type_general; + kinships[d_id] = kinships[d_id].replace('nephew', rel_type); + } + }); + } + function getD(d_id) { + return data_stash.find(d => d.id === d_id); + } +} +function findSameAncestor(main_id, rel_id, data_stash) { + const main_ancestry = getAncestry(main_id); + let found; + let is_ancestor; + let is_half_kin; + checkIfRel(rel_id); + checkIfSpouse(rel_id); + loopCheck(rel_id); + if (!found) + return null; + return { found, is_ancestor, is_half_kin }; + function loopCheck(rel_id) { + if (found) + return; + if (rel_id === main_id) { + is_ancestor = true; + found = rel_id; + is_half_kin = false; + return; + } + const d = data_stash.find(d => d.id === rel_id); + const rels = d.rels; + const parents = getParents(rels); + const found_parent = main_ancestry.find(p => (p[0] && parents[0] && p[0] === parents[0]) || (p[1] && parents[1] && p[1] === parents[1])); + if (found_parent) { + found = parents.filter((p, i) => p === found_parent[i]); + is_half_kin = checkIfHalfKin(parents, found_parent); + return; + } + rels.parents.forEach(p_id => loopCheck(p_id)); + } + function getAncestry(rel_id) { + const ancestry = []; + loopAdd(rel_id); + return ancestry; + function loopAdd(rel_id) { + const d = data_stash.find(d => d.id === rel_id); + const rels = d.rels; + ancestry.push(getParents(rels)); + rels.parents.forEach(p_id => loopAdd(p_id)); + } + } + function getParents(rels) { + return rels.parents; + } + function checkIfRel(rel_id) { + const d = data_stash.find(d => d.id === rel_id); + const found_parent = main_ancestry.find(p => p[0] === d.id || p[1] === d.id); + if (found_parent) { + is_ancestor = true; + found = rel_id; + is_half_kin = false; + } + } + function checkIfSpouse(rel_id) { + const main_datum = data_stash.find(d => d.id === main_id); + if ((main_datum.rels.spouses || []).includes(rel_id)) { + found = [main_id, rel_id]; + } + } + function checkIfHalfKin(ancestors1, ancestors2) { + return ancestors1.some((p, i) => p !== ancestors2[i]) || ancestors2.some((p, i) => p !== ancestors1[i]); + } +} +function getOrdinal(n) { + const s = ['st', 'nd', 'rd']; + return s[n - 1] ? n + s[n - 1] : n + 'th'; +} +function getGreatCount(depth) { + const depth_abs = Math.abs(depth); + return depth_abs - 2; +} +function getGreatKinship(kinship, depth) { + const great_count = getGreatCount(depth); + if (kinship.includes('great-')) + kinship = kinship.split('great-')[1]; + if (great_count === 1) { + return `great-${kinship}`; + } + else if (great_count > 1) { + return `${great_count}x-great-${kinship}`; + } + else { + console.error(`${kinship} should have great_count > 1`); + return kinship; + } +} + +function getKinshipsDataStash(main_id, rel_id, data_stash, kinships) { + var _a; + let in_law_id; + const kinship = kinships[rel_id].toLowerCase(); + if (kinship.includes('in-law')) { + in_law_id = rel_id; + const datum = data_stash.find(d => d.id === in_law_id); + if (kinship.includes('sister') || kinship.includes('brother')) { + rel_id = main_id; + } + else { + rel_id = (_a = datum.rels.spouses) === null || _a === void 0 ? void 0 : _a.find(d_id => kinships[d_id] && !kinships[d_id].includes('in-law')); + } + } + const same_ancestors = findSameAncestor(main_id, rel_id, data_stash); + if (!same_ancestors) + return console.error(`${rel_id} not found in main_ancestry`); + const same_ancestor_id = same_ancestors.is_ancestor ? same_ancestors.found : same_ancestors.found[0]; + const same_ancestor = data_stash.find(d => d.id === same_ancestor_id); + const root = d3.hierarchy(same_ancestor, hierarchyGetterChildren); + const same_ancestor_progeny = root.descendants().map(d => d.data.id); + const main_ancestry = getCleanAncestry(main_id, same_ancestor_progeny); + const rel_ancestry = getCleanAncestry(rel_id, same_ancestor_progeny); + loopClean(root); + const kinship_data_stash = root.descendants().map(d => { + const datum = { + id: d.data.id, + data: JSON.parse(JSON.stringify(d.data.data)), + kinship: kinships[d.data.id], + rels: { + parents: [], + spouses: [], + children: [] + } + }; + if (d.children && d.children.length > 0) + datum.rels.children = d.children.map(c => c.data.id); + return datum; + }); + if (kinship_data_stash.length > 0 && !same_ancestors.is_ancestor && !same_ancestors.is_half_kin) + addRootSpouse(kinship_data_stash); + if (in_law_id) + addInLawConnection(kinship_data_stash); + return kinship_data_stash; + function loopClean(tree_datum) { + tree_datum.children = (tree_datum.children || []).filter(child => { + if (main_ancestry.includes(child.data.id)) + return true; + if (rel_ancestry.includes(child.data.id)) + return true; + return false; + }); + tree_datum.children.forEach(child => loopClean(child)); + if (tree_datum.children.length === 0) + delete tree_datum.children; + } + function hierarchyGetterChildren(d) { + const children = [...(d.rels.children || [])].map(id => data_stash.find(d => d.id === id)).filter(d => d); + return children; + } + function getCleanAncestry(d_id, same_ancestor_progeny) { + const ancestry = [d_id]; + loopAdd(d_id); + return ancestry; + function loopAdd(d_id) { + const d = data_stash.find(d => d.id === d_id); + const rels = d.rels; + rels.parents.forEach(p_id => { + if (same_ancestor_progeny.includes(p_id)) { + ancestry.push(p_id); + loopAdd(p_id); + } + }); + } + } + function addRootSpouse(kinship_data_stash) { + const datum = kinship_data_stash[0]; + if (!same_ancestors) + return console.error(`${rel_id} not found in main_ancestry`); + const spouse_id = same_ancestor_id === same_ancestors.found[0] ? same_ancestors.found[1] : same_ancestors.found[0]; + datum.rels.spouses = [spouse_id]; + const spouse = data_stash.find(d => d.id === spouse_id); + const spouse_datum = { + id: spouse.id, + data: JSON.parse(JSON.stringify(spouse.data)), + kinship: kinships[spouse.id], + rels: { + spouses: [datum.id], + children: datum.rels.children, + parents: [] + } + }; + kinship_data_stash.push(spouse_datum); + (datum.rels.children || []).forEach(child_id => { + const child = data_stash.find(d => d.id === child_id); + const kinship_child = kinship_data_stash.find(d => d.id === child_id); + kinship_child.rels.parents = [...child.rels.parents]; + }); + } + function addInLawConnection(kinship_data_stash) { + if (kinship.includes('sister') || kinship.includes('brother')) { + addInLawSibling(kinship_data_stash); + } + else { + addInLawSpouse(kinship_data_stash); + } + } + function addInLawSpouse(kinship_data_stash) { + const datum = kinship_data_stash.find(d => d.id === rel_id); + const spouse_id = in_law_id; + datum.rels.spouses = [spouse_id]; + const spouse = data_stash.find(d => d.id === spouse_id); + const spouse_datum = { + id: spouse.id, + data: JSON.parse(JSON.stringify(spouse.data)), + kinship: kinships[spouse.id], + rels: { + spouses: [datum.id], + children: [], + parents: [] + } + }; + kinship_data_stash.push(spouse_datum); + } + function addInLawSibling(kinship_data_stash) { + var _a; + const datum = kinship_data_stash.find(d => d.id === rel_id); + const in_law_datum = getD(in_law_id); + kinship_data_stash.push({ + id: in_law_id, + data: JSON.parse(JSON.stringify(in_law_datum.data)), + kinship: kinships[in_law_id], + rels: { + spouses: [], + children: [], + parents: [] + } + }); + const siblings = []; + in_law_datum.rels.parents.forEach(p_id => (getD(p_id).rels.children || []).forEach(d_id => siblings.push(d_id))); + const spouse_id = (_a = getD(rel_id).rels.spouses) === null || _a === void 0 ? void 0 : _a.find(d_id => siblings.includes(d_id)); + datum.rels.spouses = [spouse_id]; + const spouse = getD(spouse_id); + const spouse_datum = { + id: spouse.id, + data: JSON.parse(JSON.stringify(spouse.data)), + kinship: kinships[spouse.id], + rels: { + spouses: [datum.id], + children: [], + parents: [] + } + }; + kinship_data_stash.push(spouse_datum); + in_law_datum.rels.parents.forEach(p_id => { + const parent = getD(p_id); + const kinship_label = parent.data.gender === 'M' ? 'Father-in-law' : parent.data.gender === 'F' ? 'Mother-in-law' : 'Parent-in-law'; + const parent_datum = { + id: parent.id, + data: JSON.parse(JSON.stringify(parent.data)), + kinship: kinship_label, + rels: { + spouses: [], + children: [spouse_id, in_law_id], + parents: [] + } + }; + const p2_id = in_law_datum.rels.parents.find(p_id => p_id !== p_id); + if (p2_id) + parent_datum.rels.parents.push(p2_id); + kinship_data_stash.unshift(parent_datum); + }); + } + function getD(d_id) { + return data_stash.find(d => d.id === d_id); + } +} + +function handleLinkRel(updated_datum, link_rel_id, store_data) { + const new_rel_id = updated_datum.id; + store_data.forEach(d => { + if (d.rels.parents.includes(new_rel_id)) { + d.rels.parents[d.rels.parents.indexOf(new_rel_id)] = link_rel_id; + } + if (d.rels.spouses && d.rels.spouses.includes(new_rel_id)) { + d.rels.spouses = d.rels.spouses.filter(id => id !== new_rel_id); + if (!d.rels.spouses.includes(link_rel_id)) + d.rels.spouses.push(link_rel_id); + } + if (d.rels.children && d.rels.children.includes(new_rel_id)) { + d.rels.children = d.rels.children.filter(id => id !== new_rel_id); + if (!d.rels.children.includes(link_rel_id)) + d.rels.children.push(link_rel_id); + } + }); + const link_rel = store_data.find(d => d.id === link_rel_id); + const new_rel = store_data.find(d => d.id === new_rel_id); + if (!new_rel) + throw new Error('New rel not found'); + if (!link_rel) + throw new Error('Link rel not found'); + (new_rel.rels.children || []).forEach(child_id => { + if (!link_rel.rels.children) + link_rel.rels.children = []; + if (!link_rel.rels.children.includes(child_id)) + link_rel.rels.children.push(child_id); + }); + (new_rel.rels.spouses || []).forEach(spouse_id => { + if (!link_rel.rels.spouses) + link_rel.rels.spouses = []; + if (!link_rel.rels.spouses.includes(spouse_id)) + link_rel.rels.spouses.push(spouse_id); + }); + if (link_rel.rels.parents.length === 0) { + link_rel.rels.parents = [...new_rel.rels.parents]; + } + else { + const link_rel_father = link_rel.rels.parents.find(id => { var _a; return ((_a = store_data.find(d => d.id === id)) === null || _a === void 0 ? void 0 : _a.data.gender) === "M"; }); + const link_rel_mother = link_rel.rels.parents.find(id => { var _a; return ((_a = store_data.find(d => d.id === id)) === null || _a === void 0 ? void 0 : _a.data.gender) === "F"; }); + const new_rel_father = new_rel.rels.parents.find(id => { var _a; return ((_a = store_data.find(d => d.id === id)) === null || _a === void 0 ? void 0 : _a.data.gender) === "M"; }); + const new_rel_mother = new_rel.rels.parents.find(id => { var _a; return ((_a = store_data.find(d => d.id === id)) === null || _a === void 0 ? void 0 : _a.data.gender) === "F"; }); + if (new_rel_father) { + if (link_rel_father) { + console.error('link rel already has father'); + link_rel.rels.parents[link_rel.rels.parents.indexOf(link_rel_father)] = new_rel_father; + } + else + link_rel.rels.parents.push(new_rel_father); + } + if (new_rel_mother) { + if (link_rel_mother) { + console.error('link rel already has mother'); + link_rel.rels.parents[link_rel.rels.parents.indexOf(link_rel_mother)] = new_rel_mother; + } + else + link_rel.rels.parents.push(new_rel_mother); + } + } + store_data.splice(store_data.findIndex(d => d.id === new_rel_id), 1); +} +function getLinkRelOptions(datum, data) { + const rel_datum = datum._new_rel_data ? data.find(d => d.id === datum._new_rel_data.rel_id) : null; + const ancestry_ids = getAncestry(datum, data); + const progeny_ids = getProgeny(datum, data); + if (datum._new_rel_data && ['son', 'daughter'].includes(datum._new_rel_data.rel_type)) { + if (!rel_datum) + throw new Error('Rel datum not found'); + progeny_ids.push(...getProgeny(rel_datum, data)); + } + return data.filter(d => d.id !== datum.id && d.id !== (rel_datum === null || rel_datum === void 0 ? void 0 : rel_datum.id) && !d._new_rel_data && !d.to_add && !d.unknown) + .filter(d => !ancestry_ids.includes(d.id)) + .filter(d => !progeny_ids.includes(d.id)) + .filter(d => !(d.rels.spouses || []).includes(datum.id)); + function getAncestry(datum, data_stash) { + const ancestry_ids = []; + loopCheck(datum); + return ancestry_ids; + function loopCheck(d) { + d.rels.parents.forEach(p_id => { + if (p_id) { + ancestry_ids.push(p_id); + const parent = data_stash.find(d => d.id === p_id); + if (!parent) + throw new Error('Parent not found'); + loopCheck(parent); + } + }); + } + } + function getProgeny(datum, data_stash) { + const progeny_ids = []; + loopCheck(datum); + return progeny_ids; + function loopCheck(d) { + const children = d.rels.children ? [...d.rels.children] : []; + children.forEach(c_id => { + progeny_ids.push(c_id); + const child = data_stash.find(d => d.id === c_id); + if (!child) + throw new Error('Child not found'); + loopCheck(child); + }); + } + } +} + +function formCreatorSetup({ datum, store, fields, postSubmitHandler, addRelative, removeRelative, deletePerson, onCancel, editFirst, link_existing_rel_config, onFormCreation, no_edit, onSubmit, onDelete, canEdit, canDelete, }) { + let can_delete = canDelete ? canDelete(datum) : true; + const can_edit = canEdit ? canEdit(datum) : true; + if (!can_edit) { + no_edit = true; + can_delete = false; + } + let form_creator; + const base_form_creator = { + datum_id: datum.id, + fields: [], + onSubmit: submitFormChanges, + onCancel: onCancel, + onFormCreation: onFormCreation, + no_edit: no_edit, + gender_field: getGenderField(), + }; + // Existing datum form creator + if (!datum._new_rel_data) { + if (!addRelative) + throw new Error('addRelative is required'); + if (!removeRelative) + throw new Error('removeRelative is required'); + form_creator = Object.assign(Object.assign({}, base_form_creator), { onDelete: deletePersonWithPostSubmit, addRelative: () => addRelative.activate(datum), addRelativeCancel: () => addRelative.onCancel(), addRelativeActive: addRelative.is_active, removeRelative: () => removeRelative.activate(datum), removeRelativeCancel: () => removeRelative.onCancel(), removeRelativeActive: removeRelative.is_active, editable: false, can_delete: can_delete }); + } + // New rel form creator + else { + form_creator = Object.assign(Object.assign({}, base_form_creator), { title: datum._new_rel_data.label, new_rel: true, editable: true }); + } + if (datum._new_rel_data || datum.to_add || datum.unknown) { + if (link_existing_rel_config) + form_creator.linkExistingRelative = createLinkExistingRelative(datum, store.getData(), link_existing_rel_config); + } + if (no_edit) + form_creator.editable = false; + else if (editFirst) + form_creator.editable = true; + fields.forEach(field => { + if (field.type === 'rel_reference') + addRelReferenceField(field); + else if (field.type === 'select') + addSelectField(field); + else + form_creator.fields.push({ + id: field.id, + type: field.type, + label: field.label, + initial_value: datum.data[field.id], + }); + }); + return form_creator; + function getGenderField() { + return { + id: 'gender', + type: 'switch', + label: 'Gender', + initial_value: datum.data.gender, + disabled: false, + options: [{ value: 'M', label: 'Male' }, { value: 'F', label: 'Female' }] + }; + } + function addRelReferenceField(field) { + if (!field.getRelLabel) + console.error('getRelLabel is not set'); + if (field.rel_type === 'spouse') { + (datum.rels.spouses || []).forEach(spouse_id => { + const spouse = store.getDatum(spouse_id); + if (!spouse) + throw new Error('Spouse not found'); + const marriage_date_id = `${field.id}__ref__${spouse_id}`; + const rel_reference_field = { + id: marriage_date_id, + type: 'rel_reference', + label: field.label, + rel_id: spouse_id, + rel_label: field.getRelLabel(spouse), + initial_value: datum.data[marriage_date_id], + rel_type: field.rel_type, + }; + form_creator.fields.push(rel_reference_field); + }); + } + } + function addSelectField(field) { + if (!field.options && !field.optionCreator) + return console.error('optionCreator or options is not set for field', field); + const select_field = { + id: field.id, + type: field.type, + label: field.label, + initial_value: datum.data[field.id], + placeholder: field.placeholder, + options: field.options || field.optionCreator(datum), + }; + form_creator.fields.push(select_field); + } + function createLinkExistingRelative(datum, data, link_existing_rel_config) { + if (!link_existing_rel_config) + throw new Error('link_existing_rel_config is required'); + const obj = { + title: link_existing_rel_config.title, + select_placeholder: link_existing_rel_config.select_placeholder, + options: getLinkRelOptions(datum, data) + .map((d) => ({ value: d.id, label: link_existing_rel_config.linkRelLabel(d) })) + .sort((a, b) => { + if (typeof a.label === 'string' && typeof b.label === 'string') + return a.label.localeCompare(b.label); + else + return a.label < b.label ? -1 : 1; + }), + onSelect: submitLinkExistingRelative + }; + return obj; + } + function submitFormChanges(e) { + if (onSubmit) { + onSubmit(e, datum, applyChanges, () => postSubmitHandler({})); + } + else { + e.preventDefault(); + applyChanges(); + postSubmitHandler({}); + } + function applyChanges() { + const form_data = new FormData(e.target); + submitFormData(datum, store.getData(), form_data); + } + } + function submitLinkExistingRelative(e) { + const link_rel_id = e.target.value; + postSubmitHandler({ link_rel_id: link_rel_id }); + } + function deletePersonWithPostSubmit() { + if (onDelete) { + onDelete(datum, () => deletePerson(), () => postSubmitHandler({ delete: true })); + } + else { + deletePerson(); + postSubmitHandler({ delete: true }); + } + } +} + +function updateGendersForNewRelatives(updated_datum, data) { + // if gender on main datum is changed, we need to switch mother/father ids for new children + data.forEach(d => { + const rd = d._new_rel_data; + if (!rd) + return; + if (rd.rel_type === 'spouse') + d.data.gender = d.data.gender === 'M' ? 'F' : 'M'; + }); +} +function cleanUp(data) { + for (let i = data.length - 1; i >= 0; i--) { + const d = data[i]; + if (d._new_rel_data) { + data.forEach(d2 => { + if (d2.rels.parents.includes(d.id)) + d2.rels.parents.splice(d2.rels.parents.indexOf(d.id), 1); + if (d2.rels.children && d2.rels.children.includes(d.id)) + d2.rels.children.splice(d2.rels.children.indexOf(d.id), 1); + if (d2.rels.spouses && d2.rels.spouses.includes(d.id)) + d2.rels.spouses.splice(d2.rels.spouses.indexOf(d.id), 1); + }); + data.splice(i, 1); + } + } +} +function addDatumRelsPlaceholders(datum, store_data, addRelLabels, canAdd) { + let can_add = { parent: true, spouse: true, child: true }; + if (canAdd) + can_add = Object.assign(can_add, canAdd(datum)); + if (!datum.rels.spouses) + datum.rels.spouses = []; + if (!datum.rels.children) + datum.rels.children = []; + if (can_add.parent) + addParents(); + if (can_add.spouse) { + addSpouseForSingleParentChildren(); + addSpouse(); + } + if (can_add.child) + addChildren(); + function addParents() { + const parents = datum.rels.parents; + const father = parents.find(d_id => { var _a; return ((_a = store_data.find(d => d.id === d_id)) === null || _a === void 0 ? void 0 : _a.data.gender) === "M"; }); + const mother = parents.find(d_id => { var _a; return ((_a = store_data.find(d => d.id === d_id)) === null || _a === void 0 ? void 0 : _a.data.gender) === "F"; }); + if (parents.length < 2 && !father) { + const father = createNewPerson({ data: { gender: "M" }, rels: { children: [datum.id] } }); + father._new_rel_data = { rel_type: "father", label: addRelLabels.father, rel_id: datum.id }; + datum.rels.parents.push(father.id); + store_data.push(father); + } + if (parents.length < 2 && !mother) { + const mother = createNewPerson({ data: { gender: "F" }, rels: { children: [datum.id] } }); + mother._new_rel_data = { rel_type: "mother", label: addRelLabels.mother, rel_id: datum.id }; + datum.rels.parents.push(mother.id); + store_data.push(mother); + } + const p1 = store_data.find(d => d.id === datum.rels.parents[0]); + const p2 = store_data.find(d => d.id === datum.rels.parents[1]); + if (!p1.rels.spouses) + p1.rels.spouses = []; + if (!p2.rels.spouses) + p2.rels.spouses = []; + if (!p1.rels.spouses.includes(p2.id)) + p1.rels.spouses.push(p2.id); + if (!p2.rels.spouses.includes(p1.id)) + p2.rels.spouses.push(p1.id); + if (!p1.rels.children) + p1.rels.children = []; + if (!p2.rels.children) + p2.rels.children = []; + if (!p1.rels.children.includes(datum.id)) + p1.rels.children.push(datum.id); + if (!p2.rels.children.includes(datum.id)) + p2.rels.children.push(datum.id); + } + function addSpouseForSingleParentChildren() { + if (!datum.rels.spouses) + datum.rels.spouses = []; + if (datum.rels.children) { + let new_spouse; + datum.rels.children.forEach(child_id => { + const child = store_data.find(d => d.id === child_id); + if (child.rels.parents.length === 1) { + const p1 = store_data.find(d => d.id === child.rels.parents[0]); + const new_spouse_gender = p1.data.gender === "M" ? "F" : "M"; + if (!new_spouse) + new_spouse = createNewPerson({ data: { gender: new_spouse_gender }, rels: { spouses: [datum.id] } }); + new_spouse._new_rel_data = { rel_type: "spouse", label: addRelLabels.spouse, rel_id: datum.id }; + new_spouse.rels.children.push(child.id); + datum.rels.spouses.push(new_spouse.id); + child.rels.parents.push(new_spouse.id); + store_data.push(new_spouse); + } + }); + } + } + function addSpouse() { + if (!datum.rels.spouses) + datum.rels.spouses = []; + const spouse_gender = datum.data.gender === "M" ? "F" : "M"; + const new_spouse = createNewPerson({ data: { gender: spouse_gender }, rels: { spouses: [datum.id] } }); + new_spouse._new_rel_data = { rel_type: "spouse", label: addRelLabels.spouse, rel_id: datum.id }; + datum.rels.spouses.push(new_spouse.id); + store_data.push(new_spouse); + } + function addChildren() { + if (!datum.rels.children) + datum.rels.children = []; + if (!datum.rels.spouses) + datum.rels.spouses = []; + datum.rels.spouses.forEach(spouse_id => { + const spouse = store_data.find(d => d.id === spouse_id); + if (!spouse.rels.children) + spouse.rels.children = []; + const new_son = createNewPerson({ data: { gender: "M" }, rels: { parents: [datum.id, spouse.id] } }); + new_son._new_rel_data = { rel_type: "son", label: addRelLabels.son, other_parent_id: spouse.id, rel_id: datum.id }; + spouse.rels.children.push(new_son.id); + datum.rels.children.push(new_son.id); + store_data.push(new_son); + const new_daughter = createNewPerson({ data: { gender: "F" }, rels: { parents: [datum.id, spouse.id] } }); + new_daughter._new_rel_data = { rel_type: "daughter", label: addRelLabels.daughter, other_parent_id: spouse.id, rel_id: datum.id }; + spouse.rels.children.push(new_daughter.id); + datum.rels.children.push(new_daughter.id); + store_data.push(new_daughter); + }); + } + return store_data; +} + +var addRelative = (store, onActivate, cancelCallback) => { return new AddRelative(store, onActivate, cancelCallback); }; +class AddRelative { + constructor(store, onActivate, cancelCallback) { + this.store = store; + this.onActivate = onActivate; + this.cancelCallback = cancelCallback; + this.datum = null; + this.onChange = null; + this.onCancel = null; + this.is_active = false; + this.addRelLabels = this.addRelLabelsDefault(); + return this; + } + activate(datum) { + if (this.is_active) + this.onCancel(); + this.onActivate(); + this.is_active = true; + this.store.state.one_level_rels = true; + const store = this.store; + this.datum = datum; + let gender_stash = this.datum.data.gender; + addDatumRelsPlaceholders(datum, this.getStoreData(), this.addRelLabels, this.canAdd); + store.updateTree({}); + this.onChange = onChange; + this.onCancel = () => onCancel(this); + function onChange(updated_datum, props) { + if (updated_datum === null || updated_datum === void 0 ? void 0 : updated_datum._new_rel_data) { + if (props === null || props === void 0 ? void 0 : props.link_rel_id) + handleLinkRel(updated_datum, props.link_rel_id, store.getData()); + else + delete updated_datum._new_rel_data; + } + else if (updated_datum.id === datum.id) { + if (updated_datum.data.gender !== gender_stash) { + gender_stash = updated_datum.data.gender; + updateGendersForNewRelatives(updated_datum, store.getData()); + } + } + else { + console.error('Something went wrong'); + } + } + function onCancel(self) { + if (!self.is_active) + return; + self.is_active = false; + self.store.state.one_level_rels = false; + self.cleanUp(); + self.cancelCallback(self.datum); + self.datum = null; + self.onChange = null; + self.onCancel = null; + } + } + setAddRelLabels(add_rel_labels) { + if (typeof add_rel_labels !== 'object') { + console.error('add_rel_labels must be an object'); + return; + } + for (const key in add_rel_labels) { + const key_str = key; + this.addRelLabels[key_str] = add_rel_labels[key_str]; + } + return this; + } + setCanAdd(canAdd) { + this.canAdd = canAdd; + return this; + } + addRelLabelsDefault() { + return { + father: 'Add Father', + mother: 'Add Mother', + spouse: 'Add Spouse', + son: 'Add Son', + daughter: 'Add Daughter' + }; + } + getStoreData() { + return this.store.getData(); + } + cleanUp(data) { + if (!data) + data = this.store.getData(); + cleanUp(data); + return data; + } +} + +var removeRelative = (store, onActivate, cancelCallback, modal) => { return new RemoveRelative(store, onActivate, cancelCallback, modal); }; +class RemoveRelative { + constructor(store, onActivate, cancelCallback, modal) { + this.store = store; + this.onActivate = onActivate; + this.cancelCallback = cancelCallback; + this.modal = modal; + this.datum = null; + this.onChange = null; + this.onCancel = null; + this.is_active = false; + return this; + } + activate(datum) { + if (this.is_active) + this.onCancel(); + this.onActivate(); + this.is_active = true; + this.store.state.one_level_rels = true; + const store = this.store; + store.updateTree({}); + this.datum = datum; + this.onChange = onChange.bind(this); + this.onCancel = onCancel.bind(this); + function onChange(rel_tree_datum, onAccept) { + const rel_type = findRelType(rel_tree_datum); + const rels = datum.rels; + if (rel_type === 'parent') + handleParentRemoval.call(this); + else if (rel_type === 'spouse') + handleSpouseRemoval.call(this); + else if (rel_type === 'children') + handleChildrenRemoval.call(this); + function handleParentRemoval() { + const rel_id = rel_tree_datum.data.id; + const parent = store.getDatum(rel_id); + if (!parent) + throw new Error('Parent not found'); + if (!parent.rels.children) + throw new Error('Parent has no children'); + parent.rels.children = parent.rels.children.filter(id => id !== datum.id); + rels.parents = rels.parents.filter(id => id !== rel_id); + onAccept(); + } + function handleSpouseRemoval() { + const spouse = rel_tree_datum.data; + if (checkIfChildrenWithSpouse()) + openModal.call(this); + else + remove.call(this, true); + function checkIfChildrenWithSpouse() { + const children = spouse.rels.children || []; + return children.some(ch_id => { + const child = store.getDatum(ch_id); + if (!child) + throw new Error('Child not found'); + if (child.rels.parents.includes(spouse.id)) + return true; + return false; + }); + } + function openModal() { + const current_gender_class = datum.data.gender === 'M' ? 'f3-male-bg' : datum.data.gender === 'F' ? 'f3-female-bg' : null; + const spouse_gender_class = spouse.data.gender === 'M' ? 'f3-male-bg' : spouse.data.gender === 'F' ? 'f3-female-bg' : null; + const div = d3.create('div').html(` +

You are removing a spouse relationship. Since there are shared children, please choose which parent should keep them in the family tree.

+
+ + +
+ `); + div.selectAll('[data-option="assign-to-current"]').on('click', () => { + remove(true); + this.modal.close(); + }); + div.selectAll('[data-option="assign-to-spouse"]').on('click', () => { + remove(false); + this.modal.close(); + }); + this.modal.activate(div.node()); + } + function remove(to_current) { + rel_tree_datum.data.rels.spouses = rel_tree_datum.data.rels.spouses.filter(id => id !== datum.id); + rels.spouses = rels.spouses.filter(id => id !== rel_tree_datum.data.id); + const childrens_parent = to_current ? datum : rel_tree_datum.data; + const other_parent = to_current ? rel_tree_datum.data : datum; + (rels.children || []).forEach(id => { + const child = store.getDatum(id); + if (!child) + throw new Error('Child not found'); + if (child.rels.parents.includes(other_parent.id)) + child.rels.parents = child.rels.parents.filter(id => id !== other_parent.id); + }); + if (other_parent.rels.children) { + other_parent.rels.children = other_parent.rels.children.filter(ch_id => !(childrens_parent.rels.children || []).includes(ch_id)); + } + onAccept(); + } + } + function handleChildrenRemoval() { + if (!rels.children) + throw new Error('Children not found'); + rels.children = rels.children.filter(id => id !== rel_tree_datum.data.id); + rel_tree_datum.data.rels.parents = rel_tree_datum.data.rels.parents.filter(id => id !== datum.id); + onAccept(); + } + function findRelType(d) { + if (d.is_ancestry) { + if (datum.rels.parents.includes(d.data.id)) + return 'parent'; + } + else if (d.spouse) { + if (!datum.rels.spouses) + throw new Error('Spouses not found'); + if (datum.rels.spouses.includes(d.data.id)) + return 'spouse'; + } + else { + if (!datum.rels.children) + throw new Error('Children not found'); + if (datum.rels.children.includes(d.data.id)) + return 'children'; + } + return null; + } + } + function onCancel() { + if (!this.is_active) + return; + this.is_active = false; + this.store.state.one_level_rels = false; + if (!this.datum) + throw new Error('Datum not found'); + this.cancelCallback(this.datum); + this.datum = null; + this.onChange = null; + this.onCancel = null; + } + } +} + +function modal (cont) { return new Modal(cont); } +class Modal { + constructor(cont) { + this.cont = cont; + this.active = false; + this.onClose = null; + this.modal_cont = d3.select(this.cont).append('div').attr('class', 'f3-modal').node(); + d3.select(this.modal_cont).style('display', 'none'); + this.create(); + } + create() { + const modal = d3.select(this.modal_cont); + modal.html(` +
+ × +
+
+
+ `); + modal.select('.f3-modal-close').on('click', () => { + this.close(); + }); + modal.on('click', (event) => { + if (event.target == modal.node()) { + this.close(); + } + }); + } + activate(content, { boolean, onAccept, onCancel } = {}) { + this.reset(); + const modal_content_inner = d3.select(this.modal_cont).select('.f3-modal-content-inner').node(); + if (typeof content === 'string') { + modal_content_inner.innerHTML = content; + } + else { + modal_content_inner.appendChild(content); + } + if (boolean) { + if (!onAccept) + throw new Error('onAccept is required'); + if (!onCancel) + throw new Error('onCancel is required'); + d3.select(this.modal_cont).select('.f3-modal-content-bottom').html(` + + + `); + d3.select(this.modal_cont).select('.f3-modal-accept').on('click', () => { onAccept(); this.reset(); this.close(); }); + d3.select(this.modal_cont).select('.f3-modal-cancel').on('click', () => { this.close(); }); + this.onClose = onCancel; + } + this.open(); + } + reset() { + this.onClose = null; + d3.select(this.modal_cont).select('.f3-modal-content-inner').html(''); + d3.select(this.modal_cont).select('.f3-modal-content-bottom').html(''); + } + open() { + this.modal_cont.style.display = 'block'; + this.active = true; + } + close() { + this.modal_cont.style.display = 'none'; + this.active = false; + if (this.onClose) + this.onClose(); + } +} + +var editTree = (cont, store) => new EditTree(cont, store); +/** + * EditTree class - Provides comprehensive editing capabilities for family tree data. + * + * This class handles all editing operations for family tree data, including: + * - Adding new family members and relationships + * - Editing existing person information + * - Removing family members and relationships + * - Form management and validation + * - History tracking and undo/redo functionality + * - Modal dialogs and user interactions + * + * @example + * ```typescript + * import * as f3 from 'family-chart' + * const f3Chart = f3.createChart('#FamilyChart', data) + * const f3EditTree = f3Chart.editTree() // returns an EditTree instance + * .setFields(["first name","last name","birthday"]) + * .setOnChange(() => { + * const updated_data = f3EditTree.exportData() + * // do something with the updated data + * }) + * ``` + */ +class EditTree { + constructor(cont, store) { + this.cont = cont; + this.store = store; + this.fields = [ + { type: 'text', label: 'first name', id: 'first name' }, + { type: 'text', label: 'last name', id: 'last name' }, + { type: 'text', label: 'birthday', id: 'birthday' }, + { type: 'text', label: 'avatar', id: 'avatar' } + ]; + this.is_fixed = true; + this.no_edit = false; + this.onChange = null; + this.editFirst = false; + this.postSubmit = null; + this.onFormCreation = null; + this.createFormEdit = null; + this.createFormNew = null; + this.formCont = this.getFormContDefault(); + this.modal = this.setupModal(); + this.addRelativeInstance = this.setupAddRelative(); + this.removeRelativeInstance = this.setupRemoveRelative(); + this.history = this.createHistory(); + return this; + } + /** + * Open the edit form + * @param datum - The datum to edit + */ + open(datum) { + if (!datum.rels) + datum = datum.data; // if TreeDatum is used, it will be converted to Datum. will be removed in a future version. + if (this.addRelativeInstance.is_active) + handleAddRelative(this); + else if (this.removeRelativeInstance.is_active) + handleRemoveRelative(this, this.store.getTreeDatum(datum.id)); + else { + this.cardEditForm(datum); + } + function handleAddRelative(self) { + if (datum._new_rel_data) { + self.cardEditForm(datum); + } + else { + self.addRelativeInstance.onCancel(); + self.cardEditForm(datum); + self.store.updateMainId(datum.id); + self.store.updateTree({}); + } + } + function handleRemoveRelative(self, tree_datum) { + if (!tree_datum) + throw new Error('Tree datum not found'); + if (!self.removeRelativeInstance.datum) + throw new Error('Remove relative datum not found'); + if (!self.removeRelativeInstance.onCancel) + throw new Error('Remove relative onCancel not found'); + if (!self.removeRelativeInstance.onChange) + throw new Error('Remove relative onChange not found'); + if (datum.id === self.removeRelativeInstance.datum.id) { + self.removeRelativeInstance.onCancel(); + self.cardEditForm(datum); + } + else { + self.removeRelativeInstance.onChange(tree_datum, onAccept.bind(self)); + function onAccept() { + self.removeRelativeInstance.onCancel(); + self.updateHistory(); + self.store.updateTree({}); + } + } + } + } + setupAddRelative() { + return addRelative(this.store, () => onActivate(this), (datum) => cancelCallback(this, datum)); + function onActivate(self) { + if (self.removeRelativeInstance.is_active) + self.removeRelativeInstance.onCancel(); + } + function cancelCallback(self, datum) { + self.store.updateMainId(datum.id); + self.store.updateTree({}); + self.openFormWithId(datum.id); + } + } + setupRemoveRelative() { + return removeRelative(this.store, onActivate.bind(this), cancelCallback.bind(this), this.modal); + function onActivate() { + if (this.addRelativeInstance.is_active) + this.addRelativeInstance.onCancel(); + setClass(this.cont, true); + } + function cancelCallback(datum) { + setClass(this.cont, false); + this.store.updateMainId(datum.id); + this.store.updateTree({}); + this.openFormWithId(datum.id); + } + function setClass(cont, add) { + d3.select(cont).select('#f3Canvas').classed('f3-remove-relative-active', add); + } + } + createHistory() { + const history = createHistory(this.store, this._getStoreDataCopy.bind(this), historyUpdateTree.bind(this)); + const nav_cont = this.cont.querySelector('.f3-nav-cont'); + if (!nav_cont) + throw new Error("Nav cont not found"); + const controls = createHistoryControls(nav_cont, history); + history.changed(); + controls.updateButtons(); + return Object.assign(Object.assign({}, history), { controls }); + function historyUpdateTree() { + var _a; + console.log('historyUpdateTree'); + if (this.addRelativeInstance.is_active) + this.addRelativeInstance.onCancel(); + if (this.removeRelativeInstance.is_active) + this.removeRelativeInstance.onCancel(); + this.store.updateTree({ initial: false }); + this.history.controls.updateButtons(); + this.openFormWithId((_a = this.store.getMainDatum()) === null || _a === void 0 ? void 0 : _a.id); + if (this.onChange) + this.onChange(); + } + } + /** + * Open the edit form without canceling the add relative or remove relative view + * @param datum - The datum to edit + */ + openWithoutRelCancel(datum) { + this.cardEditForm(datum); + } + getFormContDefault() { + let form_cont = d3.select(this.cont).select('div.f3-form-cont').node(); + if (!form_cont) + form_cont = d3.select(this.cont).append('div').classed('f3-form-cont', true).node(); + return { + el: form_cont, + populate(form_element) { + form_cont.innerHTML = ''; + form_cont.appendChild(form_element); + }, + open() { + d3.select(form_cont).classed('opened', true); + }, + close() { + d3.select(form_cont).classed('opened', false).html(''); + }, + }; + } + setFormCont(formCont) { + this.formCont = formCont; + return this; + } + cardEditForm(datum) { + const props = {}; + const is_new_rel = datum === null || datum === void 0 ? void 0 : datum._new_rel_data; + if (is_new_rel) { + props.onCancel = () => this.addRelativeInstance.onCancel(); + } + else { + props.addRelative = this.addRelativeInstance; + props.removeRelative = this.removeRelativeInstance; + props.deletePerson = () => { + deletePerson(datum, this.store.getData()); + this.openFormWithId(this.store.getLastAvailableMainDatum().id); + this.store.updateTree({}); + }; + } + const form_creator = formCreatorSetup(Object.assign({ store: this.store, datum, postSubmitHandler: (props) => postSubmitHandler(this, props), fields: this.fields, onCancel: () => { }, editFirst: this.editFirst, no_edit: this.no_edit, link_existing_rel_config: this.link_existing_rel_config, onFormCreation: this.onFormCreation, onSubmit: this.onSubmit, onDelete: this.onDelete, canEdit: this.canEdit, canDelete: this.canDelete }, props)); + const form_cont = is_new_rel + ? (this.createFormNew || createFormNew)(form_creator, this.closeForm.bind(this)) + : (this.createFormEdit || createFormEdit)(form_creator, this.closeForm.bind(this)); + this.formCont.populate(form_cont); + this.openForm(); + function postSubmitHandler(self, props) { + if (self.addRelativeInstance.is_active) { + self.addRelativeInstance.onChange(datum, props); + if (self.postSubmit) + self.postSubmit(datum, self.store.getData()); + const active_datum = self.addRelativeInstance.datum; + if (!active_datum) + throw new Error('Active datum not found'); + self.store.updateMainId(active_datum.id); + self.openWithoutRelCancel(active_datum); + } + else if ((datum.to_add || datum.unknown) && (props === null || props === void 0 ? void 0 : props.link_rel_id)) { + handleLinkRel(datum, props.link_rel_id, self.store.getData()); + self.store.updateMainId(props.link_rel_id); + self.openFormWithId(props.link_rel_id); + } + else if (!(props === null || props === void 0 ? void 0 : props.delete)) { + if (self.postSubmit) + self.postSubmit(datum, self.store.getData()); + self.openFormWithId(datum.id); + } + if (!self.is_fixed) + self.closeForm(); + self.store.updateTree({}); + self.updateHistory(); + } + } + openForm() { + this.formCont.open(); + } + closeForm() { + this.formCont.close(); + this.store.updateTree({}); + } + fixed() { + this.is_fixed = true; + if (this.formCont.el) + d3.select(this.formCont.el).style('position', 'relative'); + return this; + } + absolute() { + this.is_fixed = false; + if (this.formCont.el) + d3.select(this.formCont.el).style('position', 'absolute'); + return this; + } + setCardClickOpen(card) { + card.setOnCardClick((e, d) => { + if (this.isAddingRelative()) { + this.open(d.data); + } + else if (this.isRemovingRelative()) { + this.open(d.data); + } + else { + this.open(d.data); + card.onCardClickDefault(e, d); + } + }); + return this; + } + openFormWithId(d_id) { + if (d_id) { + const d = this.store.getDatum(d_id); + if (!d) + throw new Error('Datum not found'); + this.openWithoutRelCancel(d); + } + else { + const d = this.store.getMainDatum(); + if (!d) + throw new Error('Main datum not found'); + this.openWithoutRelCancel(d); + } + } + setNoEdit() { + this.no_edit = true; + return this; + } + setEdit() { + this.no_edit = false; + return this; + } + setFields(fields) { + const new_fields = []; + if (!Array.isArray(fields)) { + console.error('fields must be an array'); + return this; + } + for (const field of fields) { + if (typeof field === 'string') { + new_fields.push({ type: 'text', label: field, id: field }); + } + else if (typeof field === 'object') { + if (!field.id) { + console.error('fields must be an array of objects with id property'); + } + else { + new_fields.push(field); + } + } + else { + console.error('fields must be an array of strings or objects'); + } + } + this.fields = new_fields; + return this; + } + /** + * Set the onChange function to be called when the data changes via editing, adding, or removing a relative + * @param fn - The onChange function + */ + setOnChange(fn) { + this.onChange = fn; + return this; + } + setCanEdit(canEdit) { + this.canEdit = canEdit; + return this; + } + setCanDelete(canDelete) { + this.canDelete = canDelete; + return this; + } + setCanAdd(canAdd) { + this.addRelativeInstance.setCanAdd(canAdd); + return this; + } + addRelative(datum) { + if (!datum) + datum = this.store.getMainDatum(); + this.addRelativeInstance.activate(datum); + return this; + } + setupModal() { + return modal(this.cont); + } + setEditFirst(editFirst) { + this.editFirst = editFirst; + return this; + } + isAddingRelative() { + return this.addRelativeInstance.is_active; + } + isRemovingRelative() { + return this.removeRelativeInstance.is_active; + } + setAddRelLabels(add_rel_labels) { + this.addRelativeInstance.setAddRelLabels(add_rel_labels); + return this; + } + setLinkExistingRelConfig(link_existing_rel_config) { + this.link_existing_rel_config = link_existing_rel_config; + return this; + } + setOnFormCreation(onFormCreation) { + this.onFormCreation = onFormCreation; + return this; + } + setCreateFormEdit(createFormEdit) { + this.createFormEdit = createFormEdit; + return this; + } + setCreateFormNew(createFormNew) { + this.createFormNew = createFormNew; + return this; + } + _getStoreDataCopy() { + let data = JSON.parse(JSON.stringify(this.store.getData())); // important to make a deep copy of the data + if (this.addRelativeInstance.is_active) + data = this.addRelativeInstance.cleanUp(data); + data = cleanupDataJson(data); + return data; + } + /** + * deprecated: use exportData instead. This function will be removed in a future version. + * Export the data + * @returns family chart data + */ + getStoreDataCopy() { + return this.exportData(); + } + /** + * @returns family chart data + */ + exportData() { + let data = this._getStoreDataCopy(); + data = formatDataForExport(data, this.store.state.legacy_format); + return data; + } + getDataJson() { + return JSON.stringify(this.exportData(), null, 2); + } + updateHistory() { + if (this.history) { + this.history.changed(); + this.history.controls.updateButtons(); + } + if (this.onChange) + this.onChange(); + } + setPostSubmit(postSubmit) { + this.postSubmit = postSubmit; + return this; + } + setOnSubmit(onSubmit) { + this.onSubmit = onSubmit; + return this; + } + setOnDelete(onDelete) { + this.onDelete = onDelete; + return this; + } + destroy() { + this.history.controls.destroy(); + this.history = null; + if (this.formCont.el) + d3.select(this.formCont.el).remove(); + if (this.addRelativeInstance.onCancel) + this.addRelativeInstance.onCancel(); + this.store.updateTree({}); + return this; + } +} + +function linkSpouseText(svg, tree, props) { + const links_data = []; + tree.data.forEach(d => { + if (d.coparent && d.data.data.gender === 'F') + links_data.push({ nodes: [d, d.coparent], id: `${d.data.id}--${d.coparent.data.id}` }); + if (d.spouses) + d.spouses.forEach(sp => links_data.push({ nodes: [sp, d], id: `${sp.data.id}--${d.data.id}` })); + }); + const link = d3.select(svg) + .select(".links_view") + .selectAll("g.link-text") + .data(links_data, (d) => d.id); + const link_exit = link.exit(); + const link_enter = link.enter().append("g").attr("class", "link-text"); + const link_update = link_enter.merge(link); + const spouseLineX = (sp1, sp2) => { + if (sp1.spouse && sp1.data.data.gender === 'F') + return sp1.x - props.node_separation / 2; + else if (sp2.spouse && sp2.data.data.gender === 'M') + return sp2.x + props.node_separation / 2; + else + return Math.min(sp1.x, sp2.x) + props.node_separation / 2; + }; + link_exit.each(linkExit); + link_enter.each(linkEnter); + link_update.each(linkUpdate); + function linkEnter(d) { + const [sp1, sp2] = d.nodes; + const text_g = d3.select(this); + text_g + .attr('transform', `translate(${spouseLineX(sp1, sp2)}, ${sp1.y - 3})`) + .style('opacity', 0); + text_g.append("text").style('font-size', '12px').style('fill', '#fff').style('text-anchor', 'middle'); + } + function linkUpdate(d) { + const [sp1, sp2] = d.nodes; + const text_g = d3.select(this); + const delay = props.initial ? calculateDelay(tree, sp1, props.transition_time) : 0; + text_g.select('text').text(props.linkSpouseText(sp1, sp2)); + text_g.transition('text').duration(props.transition_time).delay(delay) + .attr('transform', `translate(${spouseLineX(sp1, sp2)}, ${sp1.y - 3})`); + text_g.transition('text-op').duration(100).delay(delay + props.transition_time).style('opacity', 1); + } + function linkExit(d) { + const text_g = d3.select(this); + text_g.transition('text').duration(100).style('opacity', 0) + .on("end", () => text_g.remove()); + } +} + +function autocomplete (cont, onSelect, config = {}) { return new Autocomplete(cont, onSelect, config); } +class Autocomplete { + constructor(cont, onSelect, config = {}) { + this.cont = cont; + this.options = []; + this.onSelect = onSelect; + this.config = config; + this.autocomplete_cont = d3.select(this.cont).append('div').attr('class', 'f3-autocomplete-cont').node(); + this.create(); + } + create() { + var _a; + const self = this; + d3.select(this.autocomplete_cont).html(` +
+
+ + ${chevronDownSvgIcon()} +
+
+
+ `); + const search_cont = d3.select(this.autocomplete_cont).select(".f3-autocomplete"); + const search_input = search_cont.select("input"); + const dropdown = search_cont.select(".f3-autocomplete-items"); + search_cont.on("focusout", () => { + setTimeout(() => { + const search_cont_node = search_cont.node(); + if (!search_cont_node.contains(document.activeElement)) { + closeDropdown(); + } + }, 200); + }); + search_input + .on("focus", () => { + updateOptions(); + activateDropdown(); + }) + .on("input", activateDropdown) + .on("keydown", handleArrowKeys); + dropdown.on("wheel", e => e.stopPropagation()); + search_cont.select(".f3-autocomplete-toggle") + .on("click", (e) => { + e.stopPropagation(); + const is_active = search_cont.classed("active"); + search_cont.classed("active", !is_active); + if (is_active) { + closeDropdown(); + } + else { + const search_input_node = search_input.node(); + search_input_node.focus(); + activateDropdown(); + } + }); + function activateDropdown() { + search_cont.classed("active", true); + const search_input_value = search_input.property("value"); + const filtered_options = self.options.filter(d => d.label.toLowerCase().includes(search_input_value.toLowerCase())); + filtered_options.forEach(setHtmlLabel); + filtered_options.sort(sortByLabel); + updateDropdown(filtered_options); + function setHtmlLabel(d) { + const index = d.label.toLowerCase().indexOf(search_input_value.toLowerCase()); + if (index !== -1) + d.label_html = itemLabel(); + else + d.label_html = d.label; + function itemLabel() { + return d.label.substring(0, index) + + '' + d.label.substring(index, index + search_input_value.length) + + '' + d.label.substring(index + search_input_value.length); + } + } + function sortByLabel(a, b) { + if (a.label < b.label) + return -1; + else if (a.label > b.label) + return 1; + else + return 0; + } + } + function closeDropdown() { + search_cont.classed("active", false); + updateDropdown([]); + } + function updateDropdown(filtered_options) { + dropdown.selectAll("div.f3-autocomplete-item") + .data(filtered_options, d => d === null || d === void 0 ? void 0 : d.value).join("div") + .attr("class", "f3-autocomplete-item") + .on("click", (e, d) => { + self.onSelect(d.value); + }) + .html(d => d.optionHtml ? d.optionHtml(d) : itemHtml(d)); + function itemHtml(d) { + return `
${d.label_html}
`; + } + } + function updateOptions() { + self.options = self.getOptions(); + } + function handleArrowKeys(e) { + const items = dropdown.selectAll("div.f3-autocomplete-item").nodes(); + const currentIndex = items.findIndex(item => d3.select(item).classed("f3-selected")); + if (e.key === "ArrowDown") { + e.preventDefault(); + const nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0; + selectItem(items, nextIndex); + } + else if (e.key === "ArrowUp") { + e.preventDefault(); + const prevIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1; + selectItem(items, prevIndex); + } + else if (e.key === "Enter" && currentIndex !== -1) { + e.preventDefault(); + const d = d3.select(items[currentIndex]).datum(); + if (d) { + self.onSelect(d.value); + } + } + function selectItem(items, index) { + items.forEach(item => d3.select(item).classed("f3-selected", false)); + if (items[index]) { + d3.select(items[index]).classed("f3-selected", true); + items[index].scrollIntoView({ block: "nearest" }); + } + } + } + } + setOptionsGetter(getOptions) { + this.getOptions = getOptions; + return this; + } + setOptionsGetterPerson(getData, getLabel) { + this.getOptions = () => { + const options = []; + const data = getData(); + data.forEach(d => { + if (d.to_add || d.unknown || d._new_rel_data) + return; + if (options.find(d0 => d0.value === d.id)) + return; + options.push({ + label: getLabel(d), + value: d.id, + optionHtml: optionHtml(d) + }); + }); + return options; + }; + return this; + function optionHtml(d) { + const link_off = !checkIfConnectedToFirstPerson(d, getData()); + return (option) => (` +
+ ${personSvgIcon()} + ${option.label_html} + ${link_off ? `${linkOffSvgIcon()}` : ''} +
+ `); + } + function getPersonGender(d) { + if (d.data.gender === "M") + return "male"; + else if (d.data.gender === "F") + return "female"; + else + return "genderless"; + } + } + destroy() { + this.autocomplete_cont.remove(); + } +} + +function processCardDisplay(card_display) { + const card_display_arr = []; + if (Array.isArray(card_display)) { + card_display.forEach(d => { + if (typeof d === 'function') { + card_display_arr.push(d); + } + else if (typeof d === 'string') { + card_display_arr.push((d1) => d1.data[d]); + } + else if (Array.isArray(d)) { + card_display_arr.push((d1) => d.map(key => d1.data[key]).join(' ')); + } + }); + } + else if (typeof card_display === 'function') { + card_display_arr.push(card_display); + } + else if (typeof card_display === 'string') { + card_display_arr.push((d1) => d1.data[card_display]); + } + return card_display_arr; +} + +function pathToMain(cards, links, datum, main_datum) { + const is_ancestry = datum.is_ancestry; + const links_data = links.data(); + let links_node_to_main = []; + let cards_node_to_main = []; + if (is_ancestry) { + const links_to_main = []; + let parent = datum; + let itteration1 = 0; + while (parent !== main_datum && itteration1 < 100) { + itteration1++; // to prevent infinite loop + const spouse_link = links_data.find(d => d.spouse === true && (d.source === parent || d.target === parent)); + if (spouse_link) { + const child_links = links_data.filter(d => Array.isArray(d.target) && d.target.includes(spouse_link.source) && d.target.includes(spouse_link.target)); + const child_link = getChildLinkFromAncestrySide(child_links, main_datum); + if (!child_link) + break; + links_to_main.push(spouse_link); + links_to_main.push(child_link); + parent = child_link.source; + } + else { + // single parent + const child_links = links_data.filter(d => Array.isArray(d.target) && d.target.includes(parent)); + const child_link = getChildLinkFromAncestrySide(child_links, main_datum); + if (!child_link) + break; + links_to_main.push(child_link); + parent = child_link.source; + } + } + links.each(function (d) { + if (links_to_main.includes(d)) { + links_node_to_main.push({ link: d, node: this }); + } + }); + const cards_to_main = getCardsToMain(datum, links_to_main); + cards.each(function (d) { + if (cards_to_main.includes(d)) { + cards_node_to_main.push({ card: d, node: this }); + } + }); + } + else if (datum.spouse && datum.spouse.data === main_datum.data) { + links.each(function (d) { + if (d.target === datum) + links_node_to_main.push({ link: d, node: this }); + }); + const cards_to_main = [main_datum, datum]; + cards.each(function (d) { + if (cards_to_main.includes(d)) { + cards_node_to_main.push({ card: d, node: this }); + } + }); + } + else if (datum.sibling) { + links.each(function (d) { + if (!Array.isArray(datum.parents)) + throw new Error('datum.parents is not an array'); + if (d.source === datum) + links_node_to_main.push({ link: d, node: this }); + if (d.source === main_datum && Array.isArray(d.target) && d.target.length === 2) + links_node_to_main.push({ link: d, node: this }); + if (datum.parents.includes(d.source) && !Array.isArray(d.target) && datum.parents.includes(d.target)) + links_node_to_main.push({ link: d, node: this }); + }); + const cards_to_main = [main_datum, datum, ...(datum.parents || [])]; + cards.each(function (d) { + if (cards_to_main.includes(d)) { + cards_node_to_main.push({ card: d, node: this }); + } + }); + } + else { + let links_to_main = []; + let child = datum; + let itteration1 = 0; + while (child !== main_datum && itteration1 < 100) { + itteration1++; // to prevent infinite loop + const child_link = links_data.find(d => d.target === child && Array.isArray(d.source)); + if (child_link) { + const spouse_link = links_data.find(d => d.spouse === true && sameArray([d.source, d.target], child_link.source)); + links_to_main.push(child_link); + links_to_main.push(spouse_link); + if (spouse_link) + child = spouse_link.source; + else + child = child_link.source[0]; + } + else { + const spouse_link = links_data.find(d => d.target === child && !Array.isArray(d.source)); // spouse link + if (!spouse_link) + break; + links_to_main.push(spouse_link); + child = spouse_link.source; + } + } + links.each(function (d) { + if (links_to_main.includes(d)) { + links_node_to_main.push({ link: d, node: this }); + } + }); + const cards_to_main = getCardsToMain(main_datum, links_to_main); + cards.each(function (d) { + if (cards_to_main.includes(d)) { + cards_node_to_main.push({ card: d, node: this }); + } + }); + } + return { cards_node_to_main, links_node_to_main }; + function sameArray(arr1, arr2) { + return arr1.every(d1 => arr2.some(d2 => d1 === d2)); + } + function getCardsToMain(first_parent, links_to_main) { + const all_cards = links_to_main.filter(d => d).reduce((acc, d) => { + if (Array.isArray(d.target)) + acc.push(...d.target); + else + acc.push(d.target); + if (Array.isArray(d.source)) + acc.push(...d.source); + else + acc.push(d.source); + return acc; + }, []); + const cards_to_main = [main_datum, datum]; + getChildren(first_parent); + return cards_to_main; + function getChildren(d) { + if (d.data.rels.children) { + d.data.rels.children.forEach(child_id => { + const child = all_cards.find(d0 => d0.data.id === child_id); + if (child) { + cards_to_main.push(child); + getChildren(child); + } + }); + } + } + } + function getChildLinkFromAncestrySide(child_links, main_datum) { + if (child_links.length === 0) + return null; + else if (child_links.length === 1) + return child_links[0]; + else { + // siblings of main + // should be last level where we go to the main and not its siblings + return child_links.find(d => d.source === main_datum); + } + } +} + +function CardHtmlWrapper(cont, store) { return new CardHtml$1(cont, store); } +/** + * CardHtml class - Handles HTML-based card rendering and customization for family tree nodes. + * + * @example + * ```typescript + * import * as f3 from 'family-chart' + * const f3Chart = f3.createChart('#FamilyChart', data) + * const f3Card = f3Chart.setCardHtml() // returns a CardHtml instance + * .setCardDisplay([["first name","last name"],["birthday"]]); + * ``` + */ +let CardHtml$1 = class CardHtml { + constructor(cont, store) { + this.cont = cont; + this.svg = this.cont.querySelector('svg.main_svg'); + this.store = store; + this.card_display = [(d) => `${d.data["first name"]} ${d.data["last name"]}`]; + this.cardImageField = 'avatar'; + this.onCardClick = this.onCardClickDefault; + this.style = 'default'; + this.mini_tree = false; + this.card_dim = {}; + return this; + } + getCard() { + return CardHtml$2({ + store: this.store, + card_display: this.card_display, + cardImageField: this.cardImageField, + defaultPersonIcon: this.defaultPersonIcon, + onCardClick: this.onCardClick, + style: this.style, + mini_tree: this.mini_tree, + onCardUpdate: this.onCardUpdate, + card_dim: this.card_dim, + empty_card_label: this.store.state.single_parent_empty_card_label || '', + unknown_card_label: this.store.state.unknown_card_label || '', + cardInnerHtmlCreator: this.cardInnerHtmlCreator, + duplicate_branch_toggle: this.store.state.duplicate_branch_toggle, + onCardMouseenter: this.onCardMouseenter ? this.onCardMouseenter.bind(this) : undefined, + onCardMouseleave: this.onCardMouseleave ? this.onCardMouseleave.bind(this) : undefined + }); + } + setCardDisplay(card_display) { + this.card_display = processCardDisplay(card_display); + return this; + } + setCardImageField(cardImageField) { + this.cardImageField = cardImageField; + return this; + } + setDefaultPersonIcon(defaultPersonIcon) { + this.defaultPersonIcon = defaultPersonIcon; + return this; + } + setOnCardClick(onCardClick) { + this.onCardClick = onCardClick; + return this; + } + onCardClickDefault(e, d) { + this.store.updateMainId(d.data.id); + this.store.updateTree({}); + } + setStyle(style) { + this.style = style; + return this; + } + setMiniTree(mini_tree) { + this.mini_tree = mini_tree; + return this; + } + setOnCardUpdate(onCardUpdate) { + this.onCardUpdate = onCardUpdate; + return this; + } + setCardDim(card_dim) { + if (typeof card_dim !== 'object') { + console.error('card_dim must be an object'); + return this; + } + for (let key in card_dim) { + const val = card_dim[key]; + if (typeof val !== 'number' && typeof val !== 'boolean') { + console.error(`card_dim.${key} must be a number or boolean`); + return this; + } + if (key === 'width') + key = 'w'; + if (key === 'height') + key = 'h'; + if (key === 'img_width') + key = 'img_w'; + if (key === 'img_height') + key = 'img_h'; + if (key === 'img_x') + key = 'img_x'; + if (key === 'img_y') + key = 'img_y'; + this.card_dim[key] = val; + } + return this; + } + resetCardDim() { + this.card_dim = {}; + return this; + } + setCardInnerHtmlCreator(cardInnerHtmlCreator) { + this.cardInnerHtmlCreator = cardInnerHtmlCreator; + return this; + } + setOnHoverPathToMain() { + this.onCardMouseenter = this.onEnterPathToMain.bind(this); + this.onCardMouseleave = this.onLeavePathToMain.bind(this); + return this; + } + unsetOnHoverPathToMain() { + this.onCardMouseenter = undefined; + this.onCardMouseleave = undefined; + return this; + } + onEnterPathToMain(e, datum) { + this.to_transition = datum.data.id; + const main_datum = this.store.getTreeMainDatum(); + const cards = d3.select(this.cont).select('div.cards_view').selectAll('.card_cont'); + const links = d3.select(this.cont).select('svg.main_svg .links_view').selectAll('.link'); + const { cards_node_to_main, links_node_to_main } = pathToMain(cards, links, datum, main_datum); + cards_node_to_main.forEach(d => { + const delay = Math.abs(datum.depth - d.card.depth) * 200; + d3.select(d.node.querySelector('div.card-inner')) + .transition().duration(0).delay(delay) + .on('end', () => this.to_transition === datum.data.id && d3.select(d.node.querySelector('div.card-inner')).classed('f3-path-to-main', true)); + }); + links_node_to_main.forEach(d => { + const delay = Math.abs(datum.depth - d.link.depth) * 200; + d3.select(d.node) + .transition().duration(0).delay(delay) + .on('end', () => this.to_transition === datum.data.id && d3.select(d.node).classed('f3-path-to-main', true)); + }); + return this; + } + onLeavePathToMain(e, d) { + this.to_transition = false; + d3.select(this.cont).select('div.cards_view').selectAll('div.card-inner').classed('f3-path-to-main', false); + d3.select(this.cont).select('svg.main_svg .links_view').selectAll('.link').classed('f3-path-to-main', false); + return this; + } +}; + +function CardSvgWrapper(cont, store) { return new CardSvg$1(cont, store); } +let CardSvg$1 = class CardSvg { + constructor(cont, store) { + this.cont = cont; + this.store = store; + this.svg = this.cont.querySelector('svg.main_svg'); + this.card_dim = { w: 220, h: 70, text_x: 75, text_y: 15, img_w: 60, img_h: 60, img_x: 5, img_y: 5 }; + this.card_display = []; + this.mini_tree = true; + this.link_break = false; + this.onCardClick = this.onCardClickDefault.bind(this); + return this; + } + getCard() { + return CardSvg$2({ + store: this.store, + svg: this.svg, + card_dim: this.card_dim, + card_display: this.card_display, + mini_tree: this.mini_tree, + link_break: this.link_break, + onCardClick: this.onCardClick, + onCardUpdate: this.onCardUpdate + }); + } + setCardDisplay(card_display) { + this.card_display = processCardDisplay(card_display); + return this; + } + setCardDim(card_dim) { + if (typeof card_dim !== 'object') { + console.error('card_dim must be an object'); + return this; + } + for (let key in card_dim) { + const val = card_dim[key]; + if (typeof val !== 'number' && typeof val !== 'boolean') { + console.error(`card_dim.${key} must be a number or boolean`); + return this; + } + if (key === 'width') + key = 'w'; + if (key === 'height') + key = 'h'; + if (key === 'img_width') + key = 'img_w'; + if (key === 'img_height') + key = 'img_h'; + if (key === 'img_x') + key = 'img_x'; + if (key === 'img_y') + key = 'img_y'; + this.card_dim[key] = val; + } + updateCardSvgDefs(this.svg, this.card_dim); + return this; + } + setOnCardUpdate(onCardUpdate) { + this.onCardUpdate = onCardUpdate; + return this; + } + setMiniTree(mini_tree) { + this.mini_tree = mini_tree; + return this; + } + setLinkBreak(link_break) { + this.link_break = link_break; + return this; + } + onCardClickDefault(e, d) { + this.store.updateMainId(d.data.id); + this.store.updateTree({}); + } + setOnCardClick(onCardClick) { + this.onCardClick = onCardClick; + return this; + } +}; + +function createChart(cont, data) { + return new Chart(cont, data); +} +/** + * Main Chart class - The primary class for creating and managing family tree visualizations. + * + * This is the main entry point for the Family Chart library. Use this class to: + * - Create and configure family tree visualizations + * - Set up data, styling, and interaction options + * - Control tree layout, orientation, and display settings + * - Manage user interactions and updates + * + * @example + * ```typescript + * const f3Chart = createChart('#FamilyChart', data) // returns a Chart instance; + * ``` + */ +class Chart { + constructor(cont, data) { + this.getCard = null; + this.transition_time = 2000; + this.linkSpouseText = null; + this.personSearch = null; + this.is_card_html = false; + this.beforeUpdate = null; + this.afterUpdate = null; + this.cont = setCont(cont); + const { svg } = htmlContSetup(this.cont); + this.svg = svg; + createNavCont(this.cont); + const main_id = data && data.length > 0 ? data[0].id : ''; + this.store = this.createStore(data, main_id); + this.setOnUpdate(); + this.editTreeInstance = null; + return this; + } + createStore(data, main_id) { + return createStore({ + data, + main_id, + node_separation: 250, + level_separation: 150, + single_parent_empty_card: true, + is_horizontal: false, + }); + } + setOnUpdate() { + this.store.setOnUpdate((props) => { + if (this.beforeUpdate) + this.beforeUpdate(props); + props = Object.assign({ transition_time: this.store.state.transition_time }, props || {}); + if (this.is_card_html) + props = Object.assign({}, props || {}, { cardHtml: true }); + view(this.store.getTree(), this.svg, this.getCard(), props || {}); + if (this.linkSpouseText) + linkSpouseText(this.svg, this.store.getTree(), Object.assign({}, props || {}, { linkSpouseText: this.linkSpouseText, node_separation: this.store.state.node_separation })); + if (this.afterUpdate) + this.afterUpdate(props); + }); + } + /** + * Update the tree + * @param props - The properties to update the tree with. + * @param props.initial - Whether to update the tree initially. + * @param props.tree_position - The position of the tree. + * - 'fit' to fit the tree to the container, + * - 'main_to_middle' to center the tree on the main person, + * - 'inherit' to inherit the position from the previous update. + * @param props.transition_time - The transition time. + * @returns The CreateChart instance + */ + updateTree(props = { initial: false }) { + this.store.updateTree(props); + return this; + } + /** + * Update the data + * @param data - The data to update the tree with. + * @returns The CreateChart instance + */ + updateData(data) { + this.store.updateData(data); + return this; + } + /** + * Set the card y spacing + * @param card_y_spacing - The card y spacing between the cards. Level separation. + * @returns The CreateChart instance + */ + setCardYSpacing(card_y_spacing) { + if (typeof card_y_spacing !== 'number') { + console.error('card_y_spacing must be a number'); + return this; + } + this.store.state.level_separation = card_y_spacing; + return this; + } + /** + * Set the card x spacing + * @param card_x_spacing - The card x spacing between the cards. Node separation. + * @returns The CreateChart instance + */ + setCardXSpacing(card_x_spacing) { + if (typeof card_x_spacing !== 'number') { + console.error('card_x_spacing must be a number'); + return this; + } + this.store.state.node_separation = card_x_spacing; + return this; + } + /** + * Set the orientation to vertical + * @returns The CreateChart instance + */ + setOrientationVertical() { + this.store.state.is_horizontal = false; + return this; + } + /** + * Set the orientation to horizontal + * @returns The CreateChart instance + */ + setOrientationHorizontal() { + this.store.state.is_horizontal = true; + return this; + } + /** + * Set whether to show the siblings of the main person + * @param show_siblings_of_main - Whether to show the siblings of the main person. + * @returns The CreateChart instance + */ + setShowSiblingsOfMain(show_siblings_of_main) { + this.store.state.show_siblings_of_main = show_siblings_of_main; + return this; + } + /** + * set function that will modify the tree hierarchy. it can be used to delete or add cards in the tree. + * @param modifyTreeHierarchy - function that will modify the tree hierarchy. + * @returns The CreateChart instance + */ + setModifyTreeHierarchy(modifyTreeHierarchy) { + this.store.state.modifyTreeHierarchy = modifyTreeHierarchy; + return this; + } + /** + * Set the private cards config + * @param private_cards_config - The private cards config. + * @param private_cards_config.condition - The condition to check if the card is private. + * - Example: (d: Datum) => d.data.living === true + * @returns The CreateChart instance + */ + setPrivateCardsConfig(private_cards_config) { + this.store.state.private_cards_config = private_cards_config; + return this; + } + /** + * Option to set text on spouse links + * @param linkSpouseText - The function to set the text on the spouse links. + * - Example: (sp1, sp2) => getMarriageDate(sp1, sp2) + * @returns The CreateChart instance + */ + setLinkSpouseText(linkSpouseText) { + this.linkSpouseText = linkSpouseText; + return this; + } + /** + * Set whether to show the single parent empty card + * @param single_parent_empty_card - Whether to show the single parent empty card. + * @param label - The label to display for the single parent empty card. + * @returns The CreateChart instance + */ + setSingleParentEmptyCard(single_parent_empty_card, { label = 'Unknown' } = {}) { + this.store.state.single_parent_empty_card = single_parent_empty_card; + this.store.state.single_parent_empty_card_label = label; + if (this.editTreeInstance && this.editTreeInstance.addRelativeInstance.is_active) + this.editTreeInstance.addRelativeInstance.onCancel(); + removeToAddFromData(this.store.getData() || []); + return this; + } + /** + * Set the Card creation function + * @param Card - The card function. + * @returns The CreateChart instance + */ + setCard(card) { + if (card === CardHtmlWrapper) + return this.setCardHtml(); + else if (card === CardSvgWrapper) + return this.setCardSvg(); + else + throw new Error('Card must be an instance of cardHtml or cardSvg'); + } + /** + * Set the Card HTML function + * @returns The CardHtml instance + */ + setCardHtml() { + const htmlSvg = this.cont.querySelector('#htmlSvg'); + if (!htmlSvg) + throw new Error('htmlSvg not found'); + this.is_card_html = true; + this.svg.querySelector('.cards_view').innerHTML = ''; + htmlSvg.style.display = 'block'; + const card = CardHtmlWrapper(this.cont, this.store); + this.getCard = () => card.getCard(); + return card; + } + /** + * Set the Card SVG function + * @returns The CardSvg instance + */ + setCardSvg() { + const htmlSvg = this.cont.querySelector('#htmlSvg'); + if (!htmlSvg) + throw new Error('htmlSvg not found'); + this.is_card_html = false; + this.svg.querySelector('.cards_view').innerHTML = ''; + htmlSvg.style.display = 'none'; + const card = CardSvgWrapper(this.cont, this.store); + this.getCard = () => card.getCard(); + return card; + } + /** + * Set the transition time + * @param transition_time - The transition time in milliseconds + * @returns The CreateChart instance + */ + setTransitionTime(transition_time) { + this.store.state.transition_time = transition_time; + return this; + } + /** + * Set the sort children function + * @param sortChildrenFunction - The sort children function. + * - Example: (a, b) => a.data.birth_date - b.data.birth_date + * @returns The CreateChart instance + */ + setSortChildrenFunction(sortChildrenFunction) { + this.store.state.sortChildrenFunction = sortChildrenFunction; + return this; + } + /** + * Set the sort spouses function + * @param sortSpousesFunction - The sort spouses function. + * - Example: + * (d, data) => { + * const spouses = d.data.rels.spouses || [] + * return spouses.sort((a, b) => { + * const sp1 = data.find(d0 => d0.id === a) + * const sp2 = data.find(d0 => d0.id === b) + * if (!sp1 || !sp2) return 0 + * return getMarriageDate(d, sp1) - getMarriageDate(d, sp2) + * }) + * }) + * } + * @returns The CreateChart instance + */ + setSortSpousesFunction(sortSpousesFunction) { + this.store.state.sortSpousesFunction = sortSpousesFunction; + return this; + } + /** + * Set how many generations to show in the ancestry + * @param ancestry_depth - The number of generations to show in the ancestry. + * @returns The CreateChart instance + */ + setAncestryDepth(ancestry_depth) { + this.store.state.ancestry_depth = ancestry_depth; + return this; + } + /** + * Set how many generations to show in the progeny + * @param progeny_depth - The number of generations to show in the progeny. + * @returns The CreateChart instance + */ + setProgenyDepth(progeny_depth) { + this.store.state.progeny_depth = progeny_depth; + return this; + } + /** + * Get the max depth of a person in the ancestry and progeny + * @param d_id - The id of the person to get the max depth of. + * @returns The max depth of the person in the ancestry and progeny. {ancestry: number, progeny: number} + */ + getMaxDepth(d_id) { + return getMaxDepth(d_id, this.store.getData()); + } + /** + * Calculate the kinships of a person + * @param d_id - The id of the person to calculate the kinships of. + * @param config - The config for the kinships. + * @param config.show_in_law - Whether to show in law relations. + * @returns The kinships of the person. + */ + calculateKinships(d_id, config = {}) { + return calculateKinships(d_id, this.store.getData(), config); + } + /** + * Get the kinships data stash with which we can create small family tree with relatives that connects 2 people + * @param main_id - The id of the main person. + * @param rel_id - The id of the person to get the kinships of. + * @returns The kinships data stash. + */ + getKinshipsDataStash(main_id, rel_id) { + return getKinshipsDataStash(main_id, rel_id, this.store.getData(), this.calculateKinships(main_id)); + } + /** + * Set whether to show toggable tree branches are duplicated + * @param duplicate_branch_toggle - Whether to show toggable tree branches are duplicated. + * @returns The CreateChart instance + */ + setDuplicateBranchToggle(duplicate_branch_toggle) { + this.store.state.duplicate_branch_toggle = duplicate_branch_toggle; + return this; + } + /** + * Initialize the edit tree + * @returns The edit tree instance. + */ + editTree() { + return this.editTreeInstance = editTree(this.cont, this.store); + } + /** + * Update the main person + * @param d - New main person. + * @returns The CreateChart instance + */ + updateMain(d) { + let d_id; + if (d.id) + d_id = d.id; + else + d_id = d.data.id; + this.store.updateMainId(d_id); + this.store.updateTree({}); + return this; + } + /** + * Update the main person + * @param id - New main person id. + * @returns The CreateChart instance + */ + updateMainId(id) { + this.store.updateMainId(id); + return this; + } + /** + * Get the main person + * @returns The main person. + */ + getMainDatum() { + return this.store.getMainDatum(); + } + /** + * Set the before update of the tree. + * @param fn - The function to call before the update. + * @returns The CreateChart instance + */ + setBeforeUpdate(fn) { + this.beforeUpdate = fn; + return this; + } + /** + * Set the after update of the tree. + * @param fn - The function to call after the update. + * @returns The CreateChart instance + */ + setAfterUpdate(fn) { + this.afterUpdate = fn; + return this; + } + /** + * Set the person dropdown + * @param getLabel - The function to get the label of the person to show in the dropdown. + * @param config - The config for the person dropdown. + * @param config.cont - The container to put the dropdown in. Default is the .f3-nav-cont element. + * @param config.onSelect - The function to call when a person is selected. Default is setting clicked person as main person and updating the tree. + * @param config.placeholder - The placeholder for the search input. Default is 'Search'. + * @returns The CreateChart instance + */ + setPersonDropdown(getLabel, { cont = this.cont.querySelector('.f3-nav-cont'), onSelect, placeholder = 'Search' } = {}) { + if (!onSelect) + onSelect = onSelectDefault.bind(this); + this.personSearch = autocomplete(cont, onSelect, { placeholder }); + this.personSearch.setOptionsGetterPerson(this.store.getData, getLabel); + function onSelectDefault(d_id) { + const datum = this.store.getDatum(d_id); + if (!datum) + throw new Error('Datum not found'); + if (this.editTreeInstance) + this.editTreeInstance.open(datum); + this.updateMainId(d_id); + this.updateTree({ initial: false }); + } + return this; + } + /** + * Unset the person dropdown + * @returns The CreateChart instance + */ + unSetPersonSearch() { + this.personSearch.destroy(); + this.personSearch = null; + return this; + } +} +function setCont(cont) { + if (typeof cont === "string") + cont = document.querySelector(cont); + if (!cont) + throw new Error('cont not found'); + return cont; +} +function createNavCont(cont) { + d3.select(cont).append('div').attr('class', 'f3-nav-cont'); +} + +function kinshipInfo(kinship_info_config, rel_id, data_stash) { + const { self_id, getLabel, title } = kinship_info_config; + const relationships = calculateKinships(self_id, data_stash, kinship_info_config); + const relationship = relationships[rel_id]; + if (!relationship) + return; + let label = relationship; + if (relationship === 'self') + label = 'You'; + else + label = capitalizeLabel(label); + const html = (` +
+
+ ${title} + + ${label} + ${infoSvgIcon()} + +
+
+ `); + const kinship_info_node = d3.create('div').html(html).select('div').node(); + let popup = null; + d3.select(kinship_info_node).select('.f3-kinship-info-icon').on('click', (e) => createPopup(e, kinship_info_node)); + return kinship_info_node; + function createPopup(e, cont) { + const width = 250; + const height = 400; + let left = e.clientX - width - 10; + let top = e.clientY - height - 10; + if (left + width > window.innerWidth) { + left = window.innerWidth - width - 10; + } + if (top < 0) { + top = 10; + } + if (popup && popup.active) { + popup.close(); + popup = null; + return; + } + popup = createInfoPopup(cont); + d3.select(popup.popup_cont) + .style('width', `${width}px`) + .style('height', `${height}px`) + .style('left', `${left}px`) + .style('top', `${top}px`); + const inner_cont = popup.popup_cont.querySelector('.f3-popup-content-inner'); + popup.activate(); + createSmallTree(self_id, rel_id, data_stash, relationships, inner_cont, getLabel); + } +} +function createSmallTree(self_id, rel_id, data_stash, relationships, parent_cont, getLabel) { + if (!d3.select(parent_cont).select('#SmallChart').node()) { + d3.select(parent_cont).append('div').attr('id', 'SmallChart').attr('class', 'f3'); + } + const small_chart = d3.select('#SmallChart'); + small_chart.selectAll('*').remove(); + const small_chart_data = getKinshipsDataStash(self_id, rel_id, data_stash, relationships); + let kinship_label_toggle = true; + const kinship_label_toggle_cont = small_chart.append('div'); + create(small_chart_data); + function create(data) { + const f3Chart = createChart('#SmallChart', data) + .setTransitionTime(500) + .setCardXSpacing(170) + .setCardYSpacing(70) + .setSingleParentEmptyCard(false); + const f3Card = f3Chart.setCardHtml() + .setStyle('rect') + .setCardInnerHtmlCreator((d) => { + return getCardInnerRect(d); + }) + .setOnCardUpdate(function (d) { + const card = d3.select(this).select('.card'); + card.classed('card-main', false); + }); + f3Card.onCardClick = ((e, d) => { }); + f3Chart.updateTree({ initial: true }); + setTimeout(() => setupSameZoom(0.65), 100); + createKinshipLabelToggle(); + function getCardInnerRect(d) { + let label = d.data.kinship === 'self' ? 'You' : d.data.kinship; + label = capitalizeLabel(label); + if (!kinship_label_toggle) + label = getLabel(d.data); + return (` +
+
${label}
+
+ `); + function getCardClass() { + if (d.data.kinship === 'self') { + return 'card-kinship-self' + (kinship_label_toggle ? '' : ' f3-real-label'); + } + else if (d.data.id === rel_id) { + return 'card-kinship-rel'; + } + else { + return 'card-kinship-default'; + } + } + } + function createKinshipLabelToggle() { + kinship_label_toggle_cont + .classed('f3-kinship-labels-toggle', true); + kinship_label_toggle_cont.append('label') + .text('Kinship labels') + .append('input') + .attr('type', 'checkbox') + .attr('checked', true) + .on('change', (e) => { + kinship_label_toggle = !kinship_label_toggle; + f3Chart.updateTree({ initial: false, tree_position: 'inherit' }); + }); + } + function setupSameZoom(zoom_level) { + const svg = f3Chart.cont.querySelector('svg.main_svg'); + const current_zoom = getCurrentZoom(svg); + if (current_zoom.k > zoom_level) { + zoomTo(svg, zoom_level); + } + } + } +} +function capitalizeLabel(label) { + label = label[0].toUpperCase() + label.slice(1); + if (label.includes('great-')) + label = label.replace('great-', 'Great-'); + return label; +} + +var elements = /*#__PURE__*/Object.freeze({ + __proto__: null, + Card: Card, + CardHtml: CardHtml$2, + CardSvg: CardSvg$2, + appendElement: appendElement, + infoPopup: createInfoPopup, + kinshipInfo: kinshipInfo +}); + +// export { default as calculateTree } from "./layout/calculate-tree" // handled in deprecated section +/** @deprecated Use cardSvg instead. This export will be removed in a future version. */ +const CardSvg = CardSvgWrapper; +/** @deprecated Use cardHtml instead. This export will be removed in a future version. */ +const CardHtml = CardHtmlWrapper; +const htmlHandlersWithDeprecated = Object.assign({}, htmlHandlers, { setupHtmlSvg, setupReactiveTreeData: _setupReactiveTreeData, getUniqueId }); + +var exports = /*#__PURE__*/Object.freeze({ + __proto__: null, + CalculateTree: CalculateTree, + Card: Card, + CardHtml: CardHtml, + CardHtmlClass: CardHtml$1, + CardSvg: CardSvg, + CardSvgClass: CardSvg$1, + calculateTree: calculateTreeWithV1Data, + cardHtml: CardHtmlWrapper, + cardSvg: CardSvgWrapper, + createChart: createChart, + createStore: createStore, + createSvg: createSvg, + elements: elements, + formatData: formatData, + formatDataForExport: formatDataForExport, + handlers: handlers, + htmlHandlers: htmlHandlersWithDeprecated, + icons: icons, + view: view +}); + +export { CalculateTree, Card, CardHtml, CardHtml$1 as CardHtmlClass, CardSvg, CardSvg$1 as CardSvgClass, calculateTreeWithV1Data as calculateTree, CardHtmlWrapper as cardHtml, CardSvgWrapper as cardSvg, createChart, createStore, createSvg, exports as default, elements, formatData, formatDataForExport, handlers, htmlHandlersWithDeprecated as htmlHandlers, icons, view }; diff --git a/dist/family-chart.js b/dist/family-chart.js new file mode 100644 index 00000000..cb20ecc8 --- /dev/null +++ b/dist/family-chart.js @@ -0,0 +1,5726 @@ +// https://donatso.github.io/family-chart/ v0.9.0 Copyright 2025 donatso +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports, require('d3')) : + typeof define === 'function' && define.amd ? define(['exports', 'd3'], factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.f3 = {}, global.d3)); +})(this, (function (exports, d3) { 'use strict'; + + function _interopNamespaceDefault(e) { + var n = Object.create(null); + if (e) { + Object.keys(e).forEach(function (k) { + if (k !== 'default') { + var d = Object.getOwnPropertyDescriptor(e, k); + Object.defineProperty(n, k, d.get ? d : { + enumerable: true, + get: function () { return e[k]; } + }); + } + }); + } + n.default = e; + return Object.freeze(n); + } + + var d3__namespace = /*#__PURE__*/_interopNamespaceDefault(d3); + + function sortChildrenWithSpouses(children, datum, data) { + if (!datum.rels.children) + return; + const spouses = datum.rels.spouses || []; + return children.sort((a, b) => { + const a_p2 = otherParent(a, datum, data); + const b_p2 = otherParent(b, datum, data); + const a_i = a_p2 ? spouses.indexOf(a_p2.id) : -1; + const b_i = b_p2 ? spouses.indexOf(b_p2.id) : -1; + if (datum.data.gender === "M") + return a_i - b_i; + else + return b_i - a_i; + }); + } + function sortAddNewChildren(children) { + return children.sort((a, b) => { + const a_new = a._new_rel_data; + const b_new = b._new_rel_data; + if (a_new && !b_new) + return 1; + if (!a_new && b_new) + return -1; + return 0; + }); + } + function otherParent(d, p1, data) { + return data.find(d0 => (d0.id !== p1.id) && (d.rels.parents.includes(d0.id))); + } + function calculateEnterAndExitPositions(d, entering, exiting) { + d.exiting = exiting; + if (entering) { + if (d.depth === 0 && !d.spouse) { + d._x = d.x; + d._y = d.y; + } + else if (d.spouse) { + d._x = d.spouse.x; + d._y = d.spouse.y; + } + else if (d.is_ancestry) { + if (!d.parent) + throw new Error('no parent'); + d._x = d.parent.x; + d._y = d.parent.y; + } + else { + d._x = d.psx; + d._y = d.psy; + } + } + else if (exiting) { + const x = d.x > 0 ? 1 : -1, y = d.y > 0 ? 1 : -1; + { + d._x = d.x + 400 * x; + d._y = d.y + 400 * y; + } + } + } + function setupSiblings({ tree, data_stash, node_separation, sortChildrenFunction }) { + const main = tree.find(d => d.data.main); + if (!main) + throw new Error('no main'); + const p1 = main.data.rels.parents[0]; + const p2 = main.data.rels.parents[1]; + const siblings = findSiblings(main); + if (siblings.length > 0 && !main.parents) { + if (main.data.parents) { + main.parents = main.data.parents; + } + else { + throw new Error('no parents'); + } + } + const siblings_added = addSiblingsToTree(main); + positionSiblings(main); + function findSiblings(main) { + return data_stash.filter(d => { + if (d.id === main.data.id) + return false; + if (p1 && d.rels.parents.includes(p1)) + return true; + if (p2 && d.rels.parents.includes(p2)) + return true; + return false; + }); + } + function addSiblingsToTree(main) { + const siblings_added = []; + for (let i = 0; i < siblings.length; i++) { + const sib = { + data: siblings[i], + sibling: true, + x: 0.0, // to be calculated in positionSiblings + y: main.y, + depth: main.depth - 1, + parents: [] + }; + const p1 = main.parents.find(d => d.data.id === sib.data.rels.parents[0]); + const p2 = main.parents.find(d => d.data.id === sib.data.rels.parents[1]); + if (p1) + sib.parents.push(p1); + if (p2) + sib.parents.push(p2); + tree.push(sib); + siblings_added.push(sib); + } + return siblings_added; + } + function positionSiblings(main) { + var _a, _b; + const sorted_siblings = [main, ...siblings_added]; + if (sortChildrenFunction) + sorted_siblings.sort((a, b) => sortChildrenFunction(a.data, b.data)); // first sort by custom function if provided + sorted_siblings.sort((a, b) => { + const a_p1 = main.parents.find(d => d.data.id === a.data.rels.parents[0]); + const a_p2 = main.parents.find(d => d.data.id === a.data.rels.parents[1]); + const b_p1 = main.parents.find(d => d.data.id === b.data.rels.parents[0]); + const b_p2 = main.parents.find(d => d.data.id === b.data.rels.parents[1]); + if (!a_p2 && b_p2) + return -1; + if (a_p2 && !b_p2) + return 1; + if (!a_p1 && b_p1) + return 1; + if (a_p1 && !b_p1) + return -1; + // If both have same parents or both missing same parent, maintain original order + return 0; + }); + const main_x = main.x; + const spouses_x = (main.spouses || []).map(d => d.x); + const x_range = d3__namespace.extent([main_x, ...spouses_x]); + const main_sorted_index = sorted_siblings.findIndex(d => d.data.id === main.data.id); + for (let i = 0; i < sorted_siblings.length; i++) { + if (i === main_sorted_index) + continue; + const sib = sorted_siblings[i]; + if (i < main_sorted_index) { + sib.x = ((_a = x_range[0]) !== null && _a !== void 0 ? _a : 0) - node_separation * (main_sorted_index - i); + } + else { + sib.x = ((_b = x_range[1]) !== null && _b !== void 0 ? _b : 0) + node_separation * (i - main_sorted_index); + } + } + } + } + function handlePrivateCards({ tree, data_stash, private_cards_config }) { + const private_persons = {}; + const condition = private_cards_config.condition; + if (!condition) + return console.error('private_cards_config.condition is not set'); + tree.forEach(d => { + if (d.data._new_rel_data) + return; + const is_private = isPrivate(d.data.id); + if (is_private) + d.is_private = is_private; + return; + }); + function isPrivate(d_id) { + const parents_and_spouses_checked = []; + let is_private = false; + checkParentsAndSpouses(d_id); + private_persons[d_id] = is_private; + return is_private; + function checkParentsAndSpouses(d_id) { + if (is_private) + return; + if (private_persons.hasOwnProperty(d_id)) { + is_private = private_persons[d_id]; + return is_private; + } + const d = data_stash.find(d0 => d0.id === d_id); + if (!d) + throw new Error('no d'); + if (d._new_rel_data) + return; + if (condition(d)) { + is_private = true; + return true; + } + const rels = d.rels; + [...rels.parents, ...(rels.spouses || [])].forEach(d0_id => { + if (!d0_id) + return; + if (parents_and_spouses_checked.includes(d0_id)) + return; + parents_and_spouses_checked.push(d0_id); + checkParentsAndSpouses(d0_id); + }); + } + } + } + function getMaxDepth(d_id, data_stash) { + const datum = data_stash.find(d => d.id === d_id); + if (!datum) + throw new Error('no datum'); + const root_ancestry = d3__namespace.hierarchy(datum, d => hierarchyGetterParents(d)); + const root_progeny = d3__namespace.hierarchy(datum, d => hierarchyGetterChildren(d)); + return { + ancestry: root_ancestry.height, + progeny: root_progeny.height + }; + function hierarchyGetterChildren(d) { + return [...(d.rels.children || [])] + .map(id => data_stash.find(d => d.id === id)) + .filter(d => d && !d._new_rel_data && !d.to_add); + } + function hierarchyGetterParents(d) { + return d.rels.parents + .filter(d => d) + .map(id => data_stash.find(d => d.id === id)) + .filter(d => d && !d._new_rel_data && !d.to_add); + } + } + + function createNewPerson({ data, rels }) { + return { + id: generateUUID(), + data: data || {}, + rels: Object.assign({ parents: [], children: [], spouses: [] }, (rels || {})) + }; + } + function createNewPersonWithGenderFromRel({ data, rel_type, rel_datum }) { + const gender = getGenderFromRelative(rel_datum, rel_type); + data = Object.assign(data || {}, { gender }); + return createNewPerson({ data }); + function getGenderFromRelative(rel_datum, rel_type) { + return (["daughter", "mother"].includes(rel_type) || rel_type === "spouse" && rel_datum.data.gender === "M") ? "F" : "M"; + } + } + function addNewPerson({ data_stash, datum }) { + data_stash.push(datum); + } + function generateUUID() { + var d = new Date().getTime(); + var d2 = (performance && performance.now && (performance.now() * 1000)) || 0; //Time in microseconds since page-load or 0 if unsupported + return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) { + var r = Math.random() * 16; + if (d > 0) { //Use timestamp until depleted + r = (d + r) % 16 | 0; + d = Math.floor(d / 16); + } + else { //Use microseconds since page-load if supported + r = (d2 + r) % 16 | 0; + d2 = Math.floor(d2 / 16); + } + return (c === 'x' ? r : (r & 0x3 | 0x8)).toString(16); + }); + } + + function isAllRelativeDisplayed(d, data) { + const r = d.data.rels; + const all_rels = [...r.parents, ...(r.spouses || []), ...(r.children || [])].filter(v => v); + return all_rels.every(rel_id => data.some(d => d.data.id === rel_id)); + } + function calculateDelay(tree, d, transition_time) { + const delay_level = transition_time * .4; + const ancestry_levels = Math.max(...tree.data.map(d => d.is_ancestry ? d.depth : 0)); + let delay = d.depth * delay_level; + if ((d.depth !== 0 || !!d.spouse) && !d.is_ancestry) { + delay += (ancestry_levels) * delay_level; // after ancestry + if (d.spouse) + delay += delay_level; // spouse after bloodline + delay += (d.depth) * delay_level; // double the delay for each level because of additional spouse delay + } + return delay; + } + + function handleDuplicateSpouseToggle(tree) { + tree.forEach(d => { + if (!d.spouse) return + const spouse = d.spouse; + if (d.duplicate && spouse.data._tgdp_sp) { + const parent_id = spouse.data.main ? 'main' : spouse.parent.data.id; + if (spouse.data._tgdp_sp[parent_id]?.hasOwnProperty(d.data.id)) { + d._toggle = spouse.data._tgdp_sp[parent_id][d.data.id]; + } + } + }); + } + + function handleDuplicateHierarchyProgeny(root, data_stash, on_toggle_one_close_others=true) { + const progeny_duplicates = []; + loopChildren(root); + setToggleIds(progeny_duplicates); + + function loopChildren(d) { + if (!d.children) return + const p1 = d.data; + const spouses = (d.data.rels.spouses || []).map(id => data_stash.find(d => d.id === id)); + + const children_by_spouse = getChildrenBySpouse(d); + spouses.forEach(p2 => { + if (progeny_duplicates.some(d => d.some(d => checkIfDuplicate([p1, p2], [d.p1, d.p2])))) { + return + } + const duplicates = findDuplicates(d, p1, p2); + if (duplicates.length > 0) { + const all_duplicates = [{d, p1, p2}, ...duplicates]; + progeny_duplicates.push(all_duplicates); + assignDuplicateValues(all_duplicates); + handleToggleOff(all_duplicates); + } else { + let parent_id = root === d ? 'main' : d.parent.data.id; + stashTgdpSpouse(d, parent_id, p2); + (children_by_spouse[p2.id] || []).forEach(child => { + loopChildren(child); + }); + } + }); + } + + function assignDuplicateValues(all_duplicates) { + all_duplicates.forEach(({d, p1, p2}, i) => { + if (!d.data._tgdp_sp) d.data._tgdp_sp = {}; + let parent_id = root === d ? 'main' : d.parent.data.id; + unstashTgdpSpouse(d, parent_id, p2); + if (!d.data._tgdp_sp[parent_id]) d.data._tgdp_sp[parent_id] = {}; + let val = 1; + if (!d.data._tgdp_sp[parent_id].hasOwnProperty(p2.id)) d.data._tgdp_sp[parent_id][p2.id] = val; + else val = d.data._tgdp_sp[parent_id][p2.id]; + all_duplicates[i].val = val; + }); + + if (on_toggle_one_close_others) { + if (all_duplicates.every(d => d.val < 0)) { + const first_duplicate = all_duplicates.sort((a, b) => b.val - a.val)[0]; + const {d, p1, p2} = first_duplicate; + const parent_id = root === d ? 'main' : d.parent.data.id; + d.data._tgdp_sp[parent_id][p2.id] = 1; + } + + if (all_duplicates.filter(d => d.val > 0).length > 1) { + const latest_duplicate = all_duplicates.sort((a, b) => b.val - a.val)[0]; + all_duplicates.forEach(dupl => { + if (dupl === latest_duplicate) return + const {d, p1, p2} = dupl; + const parent_id = root === d ? 'main' : d.parent.data.id; + d.data._tgdp_sp[parent_id][p2.id] = -1; + }); + } + } + } + + function handleToggleOff(all_duplicates) { + all_duplicates.forEach(({d, p1, p2}) => { + const parent_id = root === d ? 'main' : d.parent.data.id; + if (d.data._tgdp_sp[parent_id][p2.id] < 0) { + const children_by_spouse = getChildrenBySpouse(d); + if (children_by_spouse[p2.id]) { + d.children = d.children.filter(c => !children_by_spouse[p2.id].includes(c)); + if (d.children.length === 0) delete d.children; + } + } + }); + } + + function stashTgdpSpouse(d, parent_id, p2) { + if (d.data._tgdp_sp && d.data._tgdp_sp[parent_id] && d.data._tgdp_sp[parent_id].hasOwnProperty(p2.id)) { + if (!d.data.__tgdp_sp) d.data.__tgdp_sp = {}; + if (!d.data.__tgdp_sp[parent_id]) d.data.__tgdp_sp[parent_id] = {}; + d.data.__tgdp_sp[parent_id][p2.id] = d.data._tgdp_sp[parent_id][p2.id]; + delete d.data._tgdp_sp[parent_id][p2.id]; + } + } + + function unstashTgdpSpouse(d, parent_id, p2) { + if (d.data.__tgdp_sp && d.data.__tgdp_sp[parent_id] && d.data.__tgdp_sp[parent_id].hasOwnProperty(p2.id)) { + d.data._tgdp_sp[parent_id][p2.id] = d.data.__tgdp_sp[parent_id][p2.id]; + delete d.data.__tgdp_sp[parent_id][p2.id]; + } + } + + function findDuplicates(datum, partner1, partner2) { + const duplicates = []; + checkChildren(root); + return duplicates + + function checkChildren(d) { + if (d === datum) return + if (d.children) { + const p1 = d.data; + const spouses = (d.data.rels.spouses || []).map(id => data_stash.find(d => d.id === id)); + const children_by_spouse = getChildrenBySpouse(d); + spouses.forEach(p2 => { + if (checkIfDuplicate([partner1, partner2], [p1, p2])) { + duplicates.push({d, p1, p2}); + } else { + (children_by_spouse[p2.id] || []).forEach(child => { + checkChildren(child); + }); + } + }); + } + } + } + + function checkIfDuplicate(arr1, arr2) { + return arr1.every(d => arr2.some(d0 => d.id === d0.id)) + } + + function getChildrenBySpouse(d) { + const children_by_spouse = {}; + const p1 = d; + (d.children || []).forEach(child => { + const ch_rels = child.data.rels; + const p2_id = ch_rels.parents[0] === p1.data.id ? ch_rels.parents[1] : ch_rels.parents[0]; + if (!children_by_spouse[p2_id]) children_by_spouse[p2_id] = []; + children_by_spouse[p2_id].push(child); + }); + return children_by_spouse + } + + function setToggleIds(progeny_duplicates) { + let toggle_id = 0; + progeny_duplicates.forEach(dupl_arr => { + toggle_id = toggle_id+1; + dupl_arr.forEach(d => { + if (!d.d._toggle_id_sp) d.d._toggle_id_sp = {}; + d.d._toggle_id_sp[d.p2.id] = toggle_id; + }); + }); + } + } + + function handleDuplicateHierarchyAncestry(root, on_toggle_one_close_others=true) { + const ancestry_duplicates = []; + + loopChildren(root); + + setToggleIds(ancestry_duplicates); + + + function loopChildren(d) { + if (d.children) { + if (ancestry_duplicates.some(d0 => d0.includes(d))) { + return + } + const duplicates = findDuplicates(d.children); + if (duplicates.length > 0) { + const all_duplicates = [d, ...duplicates]; + ancestry_duplicates.push(all_duplicates); + assignDuplicateValues(all_duplicates); + handleToggleOff(all_duplicates); + } else { + d.children.forEach(child => { + loopChildren(child); + }); + } + } + } + + function assignDuplicateValues(all_duplicates) { + all_duplicates.forEach(d => { + if (!d.data._tgdp) d.data._tgdp = {}; + const parent_id = root === d ? 'main' : d.parent.data.id; + if (!d.data._tgdp[parent_id]) d.data._tgdp[parent_id] = -1; + d._toggle = d.data._tgdp[parent_id]; + }); + + if (on_toggle_one_close_others) { + if (all_duplicates.every(d => d._toggle < 0)) { + const first_duplicate = all_duplicates.sort((a, b) => b._toggle - a._toggle)[0]; + const d= first_duplicate; + const parent_id = root === d ? 'main' : d.parent.data.id; + d.data._tgdp[parent_id] = 1; + } + + if (all_duplicates.filter(d => d._toggle > 0).length > 1) { + const latest_duplicate = all_duplicates.sort((a, b) => b._toggle - a._toggle)[0]; + all_duplicates.forEach(dupl => { + if (dupl === latest_duplicate) return + const d = dupl; + const parent_id = root === d ? 'main' : d.parent.data.id; + d.data._tgdp[parent_id] = -1; + }); + } + } + } + + function handleToggleOff(all_duplicates) { + all_duplicates.forEach(d => { + const parent_id = root === d ? 'main' : d.parent.data.id; + if (d.data._tgdp[parent_id] < 0) delete d.children; + }); + } + + function findDuplicates(children_1) { + const duplicates = []; + checkChildren(root); + return duplicates + + function checkChildren(d) { + if (d.children) { + if (checkIfDuplicate(children_1, d.children)) { + duplicates.push(d); + } else { + d.children.forEach(child => { + checkChildren(child); + }); + } + } + } + } + + function checkIfDuplicate(arr1, arr2) { + return arr1 !== arr2 && arr1.every(d => arr2.some(d0 => d.data.id === d0.data.id)) + } + + function setToggleIds(ancestry_duplicates) { + let toggle_id = 0; + ancestry_duplicates.forEach(dupl_arr => { + toggle_id = toggle_id+1; + dupl_arr.forEach(d => { + d._toggle_id = toggle_id; + }); + }); + } + } + + function formatData(data) { + data.forEach((d) => { + if (!d.rels.parents) + d.rels.parents = []; + if (!d.rels.spouses) + d.rels.spouses = []; + if (!d.rels.children) + d.rels.children = []; + convertFatherMotherToParents(d); + }); + return data; + function convertFatherMotherToParents(d) { + if (!d.rels.parents) + d.rels.parents = []; + if (d.rels.father) + d.rels.parents.push(d.rels.father); + if (d.rels.mother) + d.rels.parents.push(d.rels.mother); + delete d.rels.father; + delete d.rels.mother; + } + } + function formatDataForExport(data, legacy_format = false) { + data.forEach(d => { + var _a; + if (legacy_format) { + let father; + let mother; + (_a = d.rels.parents) === null || _a === void 0 ? void 0 : _a.forEach(p => { + const parent = data.find(d => d.id === p); + if (!parent) + throw new Error('Parent not found'); + if (parent.data.gender === "M") { + if (!father) + father = parent.id; + else + mother = parent.id; // for same sex parents, we set some parent to father and some to mother + } + if (parent.data.gender === "F") { + if (!mother) + mother = parent.id; + else + father = parent.id; // for same sex parents, we set some parent to father and some to mother + } + }); + if (father) + d.rels.father = father; + if (mother) + d.rels.mother = mother; + delete d.rels.parents; + } + if (d.rels.parents && d.rels.parents.length === 0) + delete d.rels.parents; + if (d.rels.spouses && d.rels.spouses.length === 0) + delete d.rels.spouses; + if (d.rels.children && d.rels.children.length === 0) + delete d.rels.children; + }); + return data; + } + + function calculateTree(data, { main_id = null, node_separation = 250, level_separation = 150, single_parent_empty_card = true, is_horizontal = false, one_level_rels = false, sortChildrenFunction = undefined, sortSpousesFunction = undefined, ancestry_depth = undefined, progeny_depth = undefined, show_siblings_of_main = false, modifyTreeHierarchy = undefined, private_cards_config = undefined, duplicate_branch_toggle = false, on_toggle_one_close_others = true, }) { + if (!data || !data.length) + throw new Error('No data'); + if (is_horizontal) + [node_separation, level_separation] = [level_separation, node_separation]; + const data_stash = single_parent_empty_card ? createRelsToAdd(data) : data; + if (!main_id || !data_stash.find(d => d.id === main_id)) + main_id = data_stash[0].id; + const main = data_stash.find(d => d.id === main_id); + if (!main) + throw new Error('Main not found'); + const tree_children = calculateTreePositions(main, 'children', false); + const tree_parents = calculateTreePositions(main, 'parents', true); + data_stash.forEach(d => d.main = d === main); + levelOutEachSide(tree_parents, tree_children); + const tree = mergeSides(tree_parents, tree_children); + setupChildrenAndParents(tree); + setupSpouses(tree, node_separation); + if (show_siblings_of_main && !one_level_rels) + setupSiblings({ tree, data_stash, node_separation, sortChildrenFunction }); + setupProgenyParentsPos(tree); + nodePositioning(tree); + tree.forEach(d => d.all_rels_displayed = isAllRelativeDisplayed(d, tree)); + if (private_cards_config) + handlePrivateCards({ tree, data_stash, private_cards_config }); + setupTid(tree); + // setupFromTo(tree) + if (duplicate_branch_toggle) + handleDuplicateSpouseToggle(tree); + const dim = calculateTreeDim(tree, node_separation, level_separation); + return { data: tree, data_stash, dim, main_id: main.id, is_horizontal }; + function calculateTreePositions(datum, rt, is_ancestry) { + const hierarchyGetter = rt === "children" ? hierarchyGetterChildren : hierarchyGetterParents; + const d3_tree = d3__namespace.tree().nodeSize([node_separation, level_separation]).separation(separation); + const root = d3__namespace.hierarchy(datum, hierarchyGetter); + trimTree(root, is_ancestry); + if (duplicate_branch_toggle) + handleDuplicateHierarchy(root, data_stash, is_ancestry); + if (modifyTreeHierarchy) + modifyTreeHierarchy(root, is_ancestry); + d3_tree(root); + const tree = root.descendants(); + tree.forEach(d => { + if (d.x === undefined) + d.x = 0; + if (d.y === undefined) + d.y = 0; + }); + return tree; + function separation(a, b) { + let offset = 1; + if (!is_ancestry) { + if (!sameParent(a, b)) + offset += .25; + if (!one_level_rels) { + if (someSpouses(a, b)) + offset += offsetOnPartners(a, b); + } + if (sameParent(a, b) && !sameBothParents(a, b)) + offset += .125; + } + return offset; + } + function sameParent(a, b) { return a.parent == b.parent; } + function sameBothParents(a, b) { + const parentsA = [...a.data.rels.parents].sort(); + const parentsB = [...b.data.rels.parents].sort(); + return parentsA.length === parentsB.length && parentsA.every((p, i) => p === parentsB[i]); + } + function hasSpouses(d) { return d.data.rels.spouses && d.data.rels.spouses.length > 0; } + function someSpouses(a, b) { return hasSpouses(a) || hasSpouses(b); } + function hierarchyGetterChildren(d) { + const children = [...(d.rels.children || [])].map(id => data_stash.find(d => d.id === id)).filter(d => d !== undefined); + if (sortChildrenFunction) + children.sort(sortChildrenFunction); // first sort by custom function if provided + sortAddNewChildren(children); // then put new children at the end + if (sortSpousesFunction) + sortSpousesFunction(d, data_stash); + sortChildrenWithSpouses(children, d, data_stash); // then sort by order of spouses + return children; + } + function hierarchyGetterParents(d) { + let parents = [...d.rels.parents]; + const p1 = data_stash.find(d => d.id === parents[0]); + if (p1 && p1.data.gender === "F") + parents.reverse(); + return parents + .filter(d => d).map(id => data_stash.find(d => d.id === id)).filter(d => d !== undefined); + } + function offsetOnPartners(a, b) { + return ((a.data.rels.spouses || []).length + (b.data.rels.spouses || []).length) * .5; + } + } + function levelOutEachSide(parents, children) { + const mid_diff = (parents[0].x - children[0].x) / 2; + parents.forEach(d => d.x -= mid_diff); + children.forEach(d => d.x += mid_diff); + } + function mergeSides(parents, children) { + parents.forEach(d => { d.is_ancestry = true; }); + parents.forEach(d => d.depth === 1 ? d.parent = children[0] : null); + return [...children, ...parents.slice(1)]; + } + function nodePositioning(tree) { + tree.forEach(d => { + d.y *= (d.is_ancestry ? -1 : 1); + if (is_horizontal) { + const d_x = d.x; + d.x = d.y; + d.y = d_x; + } + }); + } + function setupSpouses(tree, node_separation) { + for (let i = tree.length; i--;) { + const d = tree[i]; + if (!d.is_ancestry) { + let spouses = d.data.rels.spouses || []; + if (d._ignore_spouses) + spouses = spouses.filter(sp_id => !d._ignore_spouses.includes(sp_id)); + if (spouses.length > 0) { + if (one_level_rels && d.depth > 0) + continue; + const side = d.data.data.gender === "M" ? -1 : 1; // female on right + d.x += spouses.length / 2 * node_separation * side; + spouses.forEach((sp_id, i) => { + const spouse = { + data: data_stash.find(d0 => d0.id === sp_id), + added: true, + depth: d.depth, + spouse: d, + x: d.x - (node_separation * (i + 1)) * side, + y: d.y, + tid: `${d.data.id}-spouse-${i}`, + }; + spouse.sx = i > 0 ? spouse.x : spouse.x + (node_separation / 2) * side; + spouse.sy = i > 0 ? spouse.y : spouse.y + (node_separation / 2) * side; + if (!d.spouses) + d.spouses = []; + d.spouses.push(spouse); + tree.push(spouse); + }); + } + } + if (d.parents && d.parents.length === 2) { + const p1 = d.parents[0]; + const p2 = d.parents[1]; + const midd = p1.x - (p1.x - p2.x) / 2; + const x = (d, sp) => midd + (node_separation / 2) * (d.x < sp.x ? 1 : -1); + p2.x = x(p1, p2); + p1.x = x(p2, p1); + } + } + } + function setupProgenyParentsPos(tree) { + tree.forEach(d => { + if (d.is_ancestry) + return; + if (d.depth === 0) + return; + if (d.added) + return; + if (d.sibling) + return; + const p1 = d.parent; + const p2 = ((p1 === null || p1 === void 0 ? void 0 : p1.spouses) || []).find((d0) => d.data.rels.parents.includes(d0.data.id)); + if (p1 && p2) { + if (!p1.added && !p2.added) + console.error('no added spouse', p1, p2); + const added_spouse = p1.added ? p1 : p2; + setupParentPos(d, added_spouse); + } + else if (p1 || p2) { + const parent = p1 || p2; + if (!parent) + throw new Error('no progeny parent'); + parent.sx = parent.x; + parent.sy = parent.y; + setupParentPos(d, parent); + } + function setupParentPos(d, p) { + d.psx = !is_horizontal ? p.sx : p.y; + d.psy = !is_horizontal ? p.y : p.sx; + } + }); + } + function setupChildrenAndParents(tree) { + tree.forEach(d0 => { + delete d0.children; + tree.forEach(d1 => { + if (d1.parent === d0) { + if (d1.is_ancestry) { + if (!d0.parents) + d0.parents = []; + d0.parents.push(d1); + } + else { + if (!d0.children) + d0.children = []; + d0.children.push(d1); + } + } + }); + if (d0.parents && d0.parents.length === 2) { + const p1 = d0.parents[0]; + const p2 = d0.parents[1]; + p1.coparent = p2; + p2.coparent = p1; + } + }); + } + function calculateTreeDim(tree, node_separation, level_separation) { + if (is_horizontal) + [node_separation, level_separation] = [level_separation, node_separation]; + const w_extent = d3__namespace.extent(tree, (d) => d.x); + const h_extent = d3__namespace.extent(tree, (d) => d.y); + if (w_extent[0] === undefined || w_extent[1] === undefined || h_extent[0] === undefined || h_extent[1] === undefined) + throw new Error('No extent'); + return { + width: w_extent[1] - w_extent[0] + node_separation, height: h_extent[1] - h_extent[0] + level_separation, x_off: -w_extent[0] + node_separation / 2, y_off: -h_extent[0] + level_separation / 2 + }; + } + function createRelsToAdd(data) { + const to_add_spouses = []; + for (let i = 0; i < data.length; i++) { + const d = data[i]; + if (d.rels.children && d.rels.children.length > 0) { + if (!d.rels.spouses) + d.rels.spouses = []; + let to_add_spouse; + d.rels.children.forEach(d0 => { + const child = data.find(d1 => d1.id === d0); + if (child.rels.parents.length === 2) + return; + if (!to_add_spouse) { + to_add_spouse = findOrCreateToAddSpouse(d); + } + if (!to_add_spouse.rels.children) + to_add_spouse.rels.children = []; + to_add_spouse.rels.children.push(child.id); + if (child.rels.parents.length !== 1) + throw new Error('child has more than 1 parent'); + child.rels.parents.push(to_add_spouse.id); + }); + } + } + to_add_spouses.forEach(d => data.push(d)); + return data; + function findOrCreateToAddSpouse(d) { + const spouses = (d.rels.spouses || []).map(sp_id => data.find(d0 => d0.id === sp_id)).filter(d => d !== undefined); + return spouses.find(sp => sp.to_add) || createToAddSpouse(d); + } + function createToAddSpouse(d) { + const spouse = createNewPerson({ + data: { gender: d.data.gender === "M" ? "F" : "M" }, + rels: { spouses: [d.id] } + }); + spouse.to_add = true; + to_add_spouses.push(spouse); + if (!d.rels.spouses) + d.rels.spouses = []; + d.rels.spouses.push(spouse.id); + return spouse; + } + } + function trimTree(root, is_ancestry) { + let max_depth = is_ancestry ? ancestry_depth : progeny_depth; + if (one_level_rels) + max_depth = 1; + if (!max_depth && max_depth !== 0) + return root; + trimNode(root, 0); + return root; + function trimNode(node, depth) { + if (depth === max_depth) { + if (node.children) + delete node.children; + } + else if (node.children) { + node.children.forEach(child => { + trimNode(child, depth + 1); + }); + } + } + } + function handleDuplicateHierarchy(root, data_stash, is_ancestry) { + if (is_ancestry) + handleDuplicateHierarchyAncestry(root, on_toggle_one_close_others); + else + handleDuplicateHierarchyProgeny(root, data_stash, on_toggle_one_close_others); + } + } + function setupTid(tree) { + const ids = []; + tree.forEach(d => { + if (ids.includes(d.data.id)) { + const duplicates = tree.filter(d0 => d0.data.id === d.data.id); + duplicates.forEach((d0, i) => { + d0.tid = `${d.data.id}--x${i + 1}`; + d0.duplicate = duplicates.length; + ids.push(d.data.id); + }); + } + else { + d.tid = d.data.id; + ids.push(d.data.id); + } + }); + } + /** + * Calculate the tree + * @param options - The options for the tree + * @param options.data - The data for the tree + * @returns The tree + * @deprecated Use f3.calculateTree instead + */ + function CalculateTree(options) { + return calculateTreeWithV1Data(options.data, options); + } + /** + * Calculate the tree with v1 data + * @param data - The data for the tree + * @param options - The options for the tree + * @returns The tree + */ + function calculateTreeWithV1Data(data, options) { + const formatted_data = formatData(data); + return calculateTree(formatted_data, options); + } + + function createStore(initial_state) { + let onUpdate; + const state = Object.assign({ transition_time: 1000 }, initial_state); + state.main_id_history = []; + if (state.data) { + checkIfFmFormat(state.data); + formatData(state.data); + } + const store = { + state, + updateTree: (props) => { + if (!state.data || state.data.length === 0) + return; + state.tree = calcTree(); + if (!state.main_id && state.tree) + updateMainId(state.tree.main_id); + if (onUpdate) + onUpdate(props); + }, + updateData: (data) => { + checkIfFmFormat(data); + formatData(data); + state.data = data; + validateMainId(); + }, + updateMainId, + getMainId: () => state.main_id, + getData: () => state.data, + getTree: () => state.tree, + setOnUpdate: (f) => onUpdate = f, + getMainDatum, + getDatum, + getTreeMainDatum, + getTreeDatum, + getLastAvailableMainDatum, + methods: {}, + }; + return store; + function calcTree() { + const args = { + main_id: state.main_id, + }; + if (state.node_separation !== undefined) + args.node_separation = state.node_separation; + if (state.level_separation !== undefined) + args.level_separation = state.level_separation; + if (state.single_parent_empty_card !== undefined) + args.single_parent_empty_card = state.single_parent_empty_card; + if (state.is_horizontal !== undefined) + args.is_horizontal = state.is_horizontal; + if (state.one_level_rels !== undefined) + args.one_level_rels = state.one_level_rels; + if (state.modifyTreeHierarchy !== undefined) + args.modifyTreeHierarchy = state.modifyTreeHierarchy; + if (state.sortChildrenFunction !== undefined) + args.sortChildrenFunction = state.sortChildrenFunction; + if (state.sortSpousesFunction !== undefined) + args.sortSpousesFunction = state.sortSpousesFunction; + if (state.ancestry_depth !== undefined) + args.ancestry_depth = state.ancestry_depth; + if (state.progeny_depth !== undefined) + args.progeny_depth = state.progeny_depth; + if (state.show_siblings_of_main !== undefined) + args.show_siblings_of_main = state.show_siblings_of_main; + if (state.private_cards_config !== undefined) + args.private_cards_config = state.private_cards_config; + if (state.duplicate_branch_toggle !== undefined) + args.duplicate_branch_toggle = state.duplicate_branch_toggle; + return calculateTree(state.data, args); + } + function getMainDatum() { + const datum = state.data.find(d => d.id === state.main_id); + if (!datum) + throw new Error("Main datum not found"); + return datum; + } + function getDatum(id) { + const datum = state.data.find(d => d.id === id); + if (!datum) + return undefined; + return datum; + } + function getTreeMainDatum() { + if (!state.tree) + throw new Error("No tree"); + const found = state.tree.data.find(d => d.data.id === state.main_id); + if (!found) + throw new Error("No tree main datum"); + return found; + } + function getTreeDatum(id) { + if (!state.tree) + throw new Error("No tree"); + const found = state.tree.data.find(d => d.data.id === id); + if (!found) + return undefined; + return found; + } + function updateMainId(id) { + if (id === state.main_id) + return; + state.main_id_history = state.main_id_history.filter(d => d !== id).slice(-10); + state.main_id_history.push(id); + state.main_id = id; + } + function validateMainId() { + if (state.main_id) { + const mainExists = state.data.find(d => d.id === state.main_id); + if (!mainExists && state.data.length > 0) { + // Set first datum as main if current main doesn't exist + updateMainId(state.data[0].id); + } + } + else { + if (state.data.length > 0) { + updateMainId(state.data[0].id); + } + } + } + // if main_id is deleted, get the last available main_id + function getLastAvailableMainDatum() { + let main_id = state.main_id_history.slice(0).reverse().find(id => getDatum(id)); + if (!main_id && state.data.length > 0) + main_id = state.data[0].id; + if (!main_id) + throw new Error("No main id"); + if (main_id !== state.main_id) + updateMainId(main_id); + const main_datum = getDatum(main_id); + if (!main_datum) + throw new Error("Main datum not found"); + return main_datum; + } + function checkIfFmFormat(data) { + if (state.legacy_format !== undefined) + return; // already checked + for (let d of data) { + if (d.rels.father || d.rels.mother) { + state.legacy_format = true; + return; + } + } + state.legacy_format = false; + } + } + + function positionTree({ t, svg, transition_time = 2000 }) { + const el_listener = getZoomListener(svg); + const zoom = el_listener.__zoomObj; + d3__namespace.select(el_listener).transition().duration(transition_time || 0).delay(transition_time ? 100 : 0) // delay 100 because of weird error of undefined something in d3 zoom + .call(zoom.transform, d3__namespace.zoomIdentity.scale(t.k).translate(t.x, t.y)); + } + function treeFit({ svg, svg_dim, tree_dim, transition_time }) { + const t = calculateTreeFit(svg_dim, tree_dim); + positionTree({ t, svg, transition_time }); + } + function calculateTreeFit(svg_dim, tree_dim) { + let k = Math.min(svg_dim.width / tree_dim.width, svg_dim.height / tree_dim.height); + if (k > 1) + k = 1; + const x = tree_dim.x_off + (svg_dim.width - tree_dim.width * k) / k / 2; + const y = tree_dim.y_off + (svg_dim.height - tree_dim.height * k) / k / 2; + return { k, x, y }; + } + function cardToMiddle({ datum, svg, svg_dim, scale, transition_time }) { + const k = scale || 1, x = svg_dim.width / 2 - datum.x * k, y = svg_dim.height / 2 - datum.y * k, t = { k, x: x / k, y: y / k }; + positionTree({ t, svg, transition_time }); + } + function manualZoom({ amount, svg, transition_time = 500 }) { + const el_listener = getZoomListener(svg); + const zoom = el_listener.__zoomObj; + if (!zoom) + throw new Error('Zoom object not found'); + d3__namespace.select(el_listener).transition().duration(transition_time || 0).delay(transition_time ? 100 : 0) // delay 100 because of weird error of undefined something in d3 zoom + .call(zoom.scaleBy, amount); + } + function getCurrentZoom(svg) { + const el_listener = getZoomListener(svg); + const currentTransform = d3__namespace.zoomTransform(el_listener); + return currentTransform; + } + function zoomTo(svg, zoom_level) { + const el_listener = getZoomListener(svg); + const currentTransform = d3__namespace.zoomTransform(el_listener); + manualZoom({ amount: zoom_level / currentTransform.k, svg }); + } + function getZoomListener(svg) { + const el_listener = svg.__zoomObj ? svg : svg.parentNode; + if (!el_listener.__zoomObj) + throw new Error('Zoom object not found'); + return el_listener; + } + function setupZoom(el, props = {}) { + if (el.__zoom) { + console.log('zoom already setup'); + return; + } + const view = el.querySelector('.view'); + const zoom = d3__namespace.zoom().on("zoom", (props.onZoom || zoomed)); + d3__namespace.select(el).call(zoom); + el.__zoomObj = zoom; + if (props.zoom_polite) + zoom.filter(zoomFilter); + function zoomed(e) { + d3__namespace.select(view).attr("transform", e.transform); + } + function zoomFilter(e) { + if (e.type === "wheel" && !e.ctrlKey) + return false; + else if (e.touches && e.touches.length < 2) + return false; + else + return true; + } + } + + function createLinks(d, is_horizontal = false) { + const links = []; + // d.spouses is always added to non-ancestry side for main blodline nodes + // d.coparent is added to ancestry side + if (d.spouses || d.coparent) + handleSpouse(d); + handleAncestrySide(d); + handleProgenySide(d); + return links; + function handleAncestrySide(d) { + if (!d.parents) + return; + const p1 = d.parents[0]; + const p2 = d.parents[1] || p1; + const p = { x: getMid(p1, p2, 'x'), y: getMid(p1, p2, 'y') }; + links.push({ + d: Link(d, p), + _d: () => { + const _d = { x: d.x, y: d.y }, _p = { x: d.x, y: d.y }; + return Link(_d, _p); + }, + curve: true, + id: linkId(d, p1, p2), + depth: d.depth + 1, + is_ancestry: true, + source: d, + target: [p1, p2] + }); + } + function handleProgenySide(d) { + if (!d.children || d.children.length === 0) + return; + d.children.forEach((child, i) => { + const other_parent = otherParent(child, d) || d; + const sx = other_parent.sx; + if (typeof sx !== 'number') + throw new Error('sx is not a number'); + const parent_pos = !is_horizontal ? { x: sx, y: d.y } : { x: d.x, y: sx }; + links.push({ + d: Link(child, parent_pos), + _d: () => Link(parent_pos, { x: _or(parent_pos, 'x'), y: _or(parent_pos, 'y') }), + curve: true, + id: linkId(child, d, other_parent), + depth: d.depth + 1, + is_ancestry: false, + source: [d, other_parent], + target: child + }); + }); + } + function handleSpouse(d) { + if (d.spouses) { + d.spouses.forEach(spouse => links.push(createSpouseLink(d, spouse))); + } + else if (d.coparent) { + links.push(createSpouseLink(d, d.coparent)); + } + function createSpouseLink(d, spouse) { + return { + d: [[d.x, d.y], [spouse.x, spouse.y]], + _d: () => [ + d.is_ancestry ? [_or(d, 'x') - .0001, _or(d, 'y')] : [d.x, d.y], // add -.0001 to line to have some length if d.x === spouse.x + d.is_ancestry ? [_or(spouse, 'x'), _or(spouse, 'y')] : [d.x - .0001, d.y] + ], + curve: false, + id: linkId(d, spouse), + depth: d.depth, + spouse: true, + is_ancestry: spouse.is_ancestry, + source: d, + target: spouse + }; + } + } + /// + function getMid(d1, d2, side, is_ = false) { + if (is_) + return _or(d1, side) - (_or(d1, side) - _or(d2, side)) / 2; + else + return d1[side] - (d1[side] - d2[side]) / 2; + } + function _or(d, side) { + const n = d.hasOwnProperty(`_${side}`) ? d[`_${side}`] : d[side]; + if (typeof n !== 'number') + throw new Error(`${side} is not a number`); + return n; + } + function Link(d, p) { + return is_horizontal ? LinkHorizontal(d, p) : LinkVertical(d, p); + } + function LinkVertical(d, p) { + const hy = (d.y + (p.y - d.y) / 2); + return [ + [d.x, d.y], + [d.x, hy], + [d.x, hy], + [p.x, hy], + [p.x, hy], + [p.x, p.y], + ]; + } + function LinkHorizontal(d, p) { + const hx = (d.x + (p.x - d.x) / 2); + return [ + [d.x, d.y], + [hx, d.y], + [hx, d.y], + [hx, p.y], + [hx, p.y], + [p.x, p.y], + ]; + } + function linkId(...args) { + return args.map(d => d.tid).sort().join(", "); // make unique id + } + function otherParent(child, p1) { + const p2 = (p1.spouses || []).find(d => child.data.rels.parents.includes(d.data.id)); + return p2; + } + } + + function updateLinks(svg, tree, props = {}) { + const links_data_dct = tree.data.reduce((acc, d) => { + createLinks(d, tree.is_horizontal).forEach(l => acc[l.id] = l); + return acc; + }, {}); + const links_data = Object.values(links_data_dct); + const link = d3__namespace + .select(svg) + .select(".links_view") + .selectAll("path.link") + .data(links_data, d => d.id); + if (props.transition_time === undefined) + throw new Error('transition_time is undefined'); + const link_exit = link.exit(); + const link_enter = link.enter().append("path").attr("class", "link"); + const link_update = link_enter.merge(link); + link_exit.each(linkExit); + link_enter.each(linkEnter); + link_update.each(linkUpdate); + function linkEnter(d) { + d3__namespace.select(this).attr("fill", "none").attr("stroke", "#fff").attr("stroke-width", 1).style("opacity", 0) + .attr("d", createPath(d, true)); + } + function linkUpdate(d) { + const path = d3__namespace.select(this); + const delay = props.initial ? calculateDelay(tree, d, props.transition_time) : 0; + path.transition('path').duration(props.transition_time).delay(delay).attr("d", createPath(d)).style("opacity", 1); + } + function linkExit(d) { + const path = d3__namespace.select(this); + path.transition('op').duration(800).style("opacity", 0); + path.transition('path').duration(props.transition_time).attr("d", createPath(d, true)) + .on("end", () => path.remove()); + } + } + function createPath(d, is_ = false) { + const line = d3__namespace.line().curve(d3__namespace.curveMonotoneY); + const lineCurve = d3__namespace.line().curve(d3__namespace.curveBasis); + const path_data = is_ ? d._d() : d.d; + if (!d.curve) + return line(path_data); + else + return lineCurve(path_data); + } + + function updateCardsSvg(svg, tree, Card, props = {}) { + const card = d3__namespace + .select(svg) + .select(".cards_view") + .selectAll("g.card_cont") + .data(tree.data, d => d.data.id); + const card_exit = card.exit(); + const card_enter = card.enter().append("g").attr("class", "card_cont"); + const card_update = card_enter.merge(card); + card_exit.each(d => calculateEnterAndExitPositions(d, false, true)); + card_enter.each(d => calculateEnterAndExitPositions(d, true, false)); + card_exit.each(cardExit); + card.each(cardUpdateNoEnter); + card_enter.each(cardEnter); + card_update.each(cardUpdate); + function cardEnter(d) { + d3__namespace.select(this) + .attr("transform", `translate(${d._x}, ${d._y})`) + .style("opacity", 0); + Card.call(this, d); + } + function cardUpdateNoEnter(d) { } + function cardUpdate(d) { + Card.call(this, d); + const delay = props.initial ? calculateDelay(tree, d, props.transition_time) : 0; + d3__namespace.select(this).transition().duration(props.transition_time).delay(delay).attr("transform", `translate(${d.x}, ${d.y})`).style("opacity", 1); + } + function cardExit(d) { + const tree_datum = d; + const pos = tree_datum ? [tree_datum._x, tree_datum._y] : [0, 0]; + const g = d3__namespace.select(this); + g.transition().duration(props.transition_time) + .style("opacity", 0) + .attr("transform", `translate(${pos[0]}, ${pos[1]})`) + .on("end", () => g.remove()); + } + } + + function updateCardsHtml(svg, tree, Card, props = {}) { + const div = getHtmlDiv(svg); + const card = d3__namespace.select(div).select(".cards_view").selectAll("div.card_cont").data(tree.data, d => d.tid); + const card_exit = card.exit(); + const card_enter = card.enter().append("div").attr("class", "card_cont").style('pointer-events', 'none'); + const card_update = card_enter.merge(card); + card_exit.each(d => calculateEnterAndExitPositions(d, false, true)); + card_enter.each(d => calculateEnterAndExitPositions(d, true, false)); + card_exit.each(cardExit); + card.each(cardUpdateNoEnter); + card_enter.each(cardEnter); + card_update.each(cardUpdate); + function cardEnter(d) { + d3__namespace.select(this) + .style('position', 'absolute') + .style('top', '0').style('left', '0') + .style("transform", `translate(${d._x}px, ${d._y}px)`) + .style("opacity", 0); + Card.call(this, d); + } + function cardUpdateNoEnter(d) { } + function cardUpdate(d) { + Card.call(this, d); + const delay = props.initial ? calculateDelay(tree, d, props.transition_time) : 0; + d3__namespace.select(this).transition().duration(props.transition_time).delay(delay).style("transform", `translate(${d.x}px, ${d.y}px)`).style("opacity", 1); + } + function cardExit(d) { + const tree_datum = d; + const pos = tree_datum ? [tree_datum._x, tree_datum._y] : [0, 0]; + const g = d3__namespace.select(this); + g.transition().duration(props.transition_time) + .style("opacity", 0) + .style("transform", `translate(${pos[0]}px, ${pos[1]}px)`) + .on("end", () => g.remove()); + } + function getHtmlDiv(svg) { + if (props.cardHtmlDiv) + return props.cardHtmlDiv; + const canvas = svg.closest('#f3Canvas'); + if (!canvas) + throw new Error('canvas not found'); + const htmlSvg = canvas.querySelector('#htmlSvg'); + if (!htmlSvg) + throw new Error('htmlSvg not found'); + return htmlSvg; + } + } + + function createSvg(cont, props = {}) { + const svg_dim = cont.getBoundingClientRect(); + const svg_html = (` + + + + + + + + + + + `); + const f3Canvas = getOrCreateF3Canvas(cont); + const temp_div = d3__namespace.create('div').node(); + temp_div.innerHTML = svg_html; + const svg = temp_div.querySelector('svg'); + f3Canvas.appendChild(svg); + cont.appendChild(f3Canvas); + setupZoom(f3Canvas, props); + return svg; + function getOrCreateF3Canvas(cont) { + let f3Canvas = cont.querySelector('#f3Canvas'); + if (!f3Canvas) { + f3Canvas = d3__namespace.create('div').attr('id', 'f3Canvas').attr('style', 'position: relative; overflow: hidden; width: 100%; height: 100%;').node(); + } + return f3Canvas; + } + } + + function htmlContSetup(cont) { + const getSvgView = () => cont.querySelector('svg .view'); + const getHtmlView = () => cont.querySelector('#htmlSvg .cards_view'); + createSvg(cont, { onZoom: onZoomSetup(getSvgView, getHtmlView) }); + createHtmlSvg(cont); + return { + svg: cont.querySelector('svg.main_svg'), + svgView: cont.querySelector('svg .view'), + htmlSvg: cont.querySelector('#htmlSvg'), + htmlView: cont.querySelector('#htmlSvg .cards_view') + }; + } + function createHtmlSvg(cont) { + const f3Canvas = d3__namespace.select(cont).select('#f3Canvas'); + const cardHtml = f3Canvas.append('div').attr('id', 'htmlSvg') + .attr('style', 'position: absolute; width: 100%; height: 100%; z-index: 2; top: 0; left: 0'); + cardHtml.append('div').attr('class', 'cards_view').style('transform-origin', '0 0'); + return cardHtml.node(); + } + function onZoomSetup(getSvgView, getHtmlView) { + return function onZoom(e) { + const t = e.transform; + d3__namespace.select(getSvgView()).style('transform', `translate(${t.x}px, ${t.y}px) scale(${t.k}) `); + d3__namespace.select(getHtmlView()).style('transform', `translate(${t.x}px, ${t.y}px) scale(${t.k}) `); + }; + } + + var htmlHandlers = /*#__PURE__*/Object.freeze({ + __proto__: null, + createHtmlSvg: createHtmlSvg, + default: htmlContSetup, + onZoomSetup: onZoomSetup + }); + + function cardComponentSetup(cont) { + const getSvgView = () => cont.querySelector('svg .view'); + const getHtmlSvg = () => cont.querySelector('#htmlSvg'); + const getHtmlView = () => cont.querySelector('#htmlSvg .cards_view'); + createSvg(cont, { onZoom: onZoomSetup(getSvgView, getHtmlView) }); + d3__namespace.select(getHtmlSvg()).append("div").attr("class", "cards_view_fake").style('display', 'none'); // important for handling data + return setupReactiveTreeData(getHtmlSvg); + } + function setupReactiveTreeData(getHtmlSvg) { + let tree_data = []; + return function getReactiveTreeData(new_tree_data) { + const tree_data_exit = getTreeDataExit(new_tree_data, tree_data); + tree_data = [...new_tree_data, ...tree_data_exit]; + assignUniqueIdToTreeData(getCardsViewFake(getHtmlSvg), tree_data); + return tree_data; + }; + function assignUniqueIdToTreeData(div, tree_data) { + const card = d3__namespace.select(div).selectAll("div.card_cont_2fake").data(tree_data, d => d.data.id); // how this doesn't break if there is multiple cards with the same id? + const card_exit = card.exit(); + const card_enter = card.enter().append("div").attr("class", "card_cont_2fake").style('display', 'none').attr("data-id", () => Math.random()); + const card_update = card_enter.merge(card); + card_exit.each(cardExit); + card_enter.each(cardEnter); + card_update.each(cardUpdate); + function cardEnter(d) { + d.unique_id = d3__namespace.select(this).attr("data-id"); + } + function cardUpdate(d) { + d.unique_id = d3__namespace.select(this).attr("data-id"); + } + function cardExit(d) { + if (!d) + return; + d.unique_id = d3__namespace.select(this).attr("data-id"); + d3__namespace.select(this).remove(); + } + } + function getTreeDataExit(new_tree_data, old_tree_data) { + if (old_tree_data.length > 0) { + return old_tree_data.filter(d => !new_tree_data.find(t => t.data.id === d.data.id)); + } + else { + return []; + } + } + } + function getCardsViewFake(getHtmlSvg) { + return d3__namespace.select(getHtmlSvg()).select("div.cards_view_fake").node(); + } + /** @deprecated This export will be removed in a future version. Use setupReactiveTreeData instead. */ + function setupHtmlSvg(getHtmlSvg) { + d3__namespace.select(getHtmlSvg()).append("div").attr("class", "cards_view_fake").style('display', 'none'); // important for handling data + } + /** @deprecated This export will be removed in a future version. Use setupReactiveTreeData instead. */ + const _setupReactiveTreeData = setupReactiveTreeData; + /** @deprecated This export will be removed in a future version. Use setupReactiveTreeData instead. */ + function getUniqueId(d) { + return d.unique_id; + } + + function updateCardsComponent(svg, tree, Card, props = {}) { + const div = props.cardHtmlDiv ? props.cardHtmlDiv : svg.closest('#f3Canvas').querySelector('#htmlSvg'); + const card = d3__namespace.select(getCardsViewFake(() => div)).selectAll("div.card_cont_fake").data(tree.data, d => d.data.id); + const card_exit = card.exit(); + const card_enter = card.enter().append("div").attr("class", "card_cont_fake").style('display', 'none'); + const card_update = card_enter.merge(card); + card_exit.each(d => calculateEnterAndExitPositions(d, false, true)); + card_enter.each(d => calculateEnterAndExitPositions(d, true, false)); + card_exit.each(cardExit); + card.each(cardUpdateNoEnter); + card_enter.each(cardEnter); + card_update.each(cardUpdate); + function cardEnter(d) { + const card_element = d3__namespace.select(Card(d)); + card_element + .style('position', 'absolute') + .style('top', '0').style('left', '0').style("opacity", 0) + .style("transform", `translate(${d._x}px, ${d._y}px)`); + } + function cardUpdateNoEnter(d) { } + function cardUpdate(d) { + const card_element = d3__namespace.select(Card(d)); + const delay = props.initial ? calculateDelay(tree, d, props.transition_time) : 0; + card_element.transition().duration(props.transition_time).delay(delay).style("transform", `translate(${d.x}px, ${d.y}px)`).style("opacity", 1); + } + function cardExit(d) { + const tree_datum = d; + const pos = tree_datum ? [tree_datum._x, tree_datum._y] : [0, 0]; + const card_element = d3__namespace.select(Card(d)); + const g = d3__namespace.select(this); + card_element.transition().duration(props.transition_time).style("opacity", 0).style("transform", `translate(${pos[0]}px, ${pos[1]}px)`) + .on("end", () => g.remove()); // remove the card_cont_fake + } + } + + function view (tree, svg, Card, props = {}) { + props.initial = props.hasOwnProperty('initial') ? props.initial : !d3__namespace.select(svg.parentNode).select('.card_cont').node(); + props.transition_time = props.hasOwnProperty('transition_time') ? props.transition_time : 1000; + if (props.cardComponent) + updateCardsComponent(svg, tree, Card, props); + else if (props.cardHtml) + updateCardsHtml(svg, tree, Card, props); + else + updateCardsSvg(svg, tree, Card, props); + updateLinks(svg, tree, props); + const tree_position = props.tree_position || 'fit'; + if (props.initial) + treeFit({ svg, svg_dim: svg.getBoundingClientRect(), tree_dim: tree.dim, transition_time: 0 }); + else if (tree_position === 'fit') + treeFit({ svg, svg_dim: svg.getBoundingClientRect(), tree_dim: tree.dim, transition_time: props.transition_time }); + else if (tree_position === 'main_to_middle') + cardToMiddle({ datum: tree.data[0], svg, svg_dim: svg.getBoundingClientRect(), scale: props.scale, transition_time: props.transition_time }); + else ; + return true; + } + + function cardChangeMain(store, { d }) { + store.updateMainId(d.data.id); + store.updateTree({}); + return true; + } + + function checkIfRelativesConnectedWithoutPerson(datum, data_stash) { + const r = datum.rels; + const r_ids = [...r.parents, ...(r.spouses || []), ...(r.children || [])].filter(r_id => !!r_id); + for (const r_id of r_ids) { + const person = data_stash.find(d => d.id === r_id); + if (!checkIfConnectedToFirstPerson(person, data_stash, [datum.id])) + return false; + } + return true; + } + function checkIfConnectedToFirstPerson(datum, data_stash, exclude_ids = []) { + const first_person = data_stash[0]; + if (datum.id === first_person.id) + return true; + const rels_checked = [...exclude_ids]; + let connected = false; + checkRels(datum); + return connected; + function checkRels(d0) { + if (connected) + return; + const r = d0.rels; + const r_ids = [...r.parents, ...(r.spouses || []), ...(r.children || [])].filter(r_id => !!r_id); + r_ids.forEach(r_id => { + if (rels_checked.includes(r_id)) + return; + rels_checked.push(r_id); + const person = data_stash.find(d => d.id === r_id); + if (person.id === first_person.id) + connected = true; + else + checkRels(person); + }); + } + } + + function submitFormData(datum, data_stash, form_data) { + form_data.forEach((v, k) => datum.data[k] = v); + syncRelReference(datum, data_stash); + if (datum.to_add) + delete datum.to_add; + if (datum.unknown) + delete datum.unknown; + } + function syncRelReference(datum, data_stash) { + Object.keys(datum.data).forEach(k => { + if (k.includes('__ref__')) { + const rel_id = k.split('__ref__')[1]; + const rel = data_stash.find(d => d.id === rel_id); + if (!rel) + return; + const ref_field_id = k.split('__ref__')[0] + '__ref__' + datum.id; + rel.data[ref_field_id] = datum.data[k]; + } + }); + } + function onDeleteSyncRelReference(datum, data_stash) { + Object.keys(datum.data).forEach(k => { + if (k.includes('__ref__')) { + const rel_id = k.split('__ref__')[1]; + const rel = data_stash.find(d => d.id === rel_id); + if (!rel) + return; + const ref_field_id = k.split('__ref__')[0] + '__ref__' + datum.id; + delete rel.data[ref_field_id]; + } + }); + } + function moveToAddToAdded(datum, data_stash) { + delete datum.to_add; + return datum; + } + function removeToAdd(datum, data_stash) { + deletePerson(datum, data_stash, false); + return false; + } + function deletePerson(datum, data_stash, clean_to_add = true) { + if (!checkIfRelativesConnectedWithoutPerson(datum, data_stash)) { + changeToUnknown(); + return { success: true }; + } + else { + executeDelete(); + if (clean_to_add) + removeToAddFromData(data_stash); + return { success: true }; + } + function executeDelete() { + data_stash.forEach(d => { + for (let k in d.rels) { + if (!d.rels.hasOwnProperty(k)) + continue; + const key = k; + if (Array.isArray(d.rels[key]) && d.rels[key].includes(datum.id)) { + d.rels[key].splice(d.rels[key].findIndex(did => did === datum.id), 1); + } + } + }); + onDeleteSyncRelReference(datum, data_stash); + data_stash.splice(data_stash.findIndex(d => d.id === datum.id), 1); + if (data_stash.length === 0) + data_stash.push(createNewPerson({ data: { gender: 'M' } })); + } + function changeToUnknown() { + onDeleteSyncRelReference(datum, data_stash); + datum.data = { + gender: datum.data.gender, + }; + datum.unknown = true; + } + } + function cleanupDataJson(data) { + removeToAddFromData(data); + data.forEach(d => { + delete d.main; + delete d._tgdp; + delete d._tgdp_sp; + delete d.__tgdp_sp; + }); + data.forEach(d => { + Object.keys(d).forEach(k => { + if (k[0] === '_') + console.error('key starts with _', k); + }); + }); + return data; + } + function removeToAddFromData(data) { + for (let i = data.length - 1; i >= 0; i--) { + if (data[i].to_add) + removeToAdd(data[i], data); + } + } + + function userIcon() { + return (` + + ${bgCircle()} + + + `); + } + function userEditIcon() { + return (` + + ${bgCircle()} + + + `); + } + function userPlusIcon() { + return (` + + ${bgCircle()} + + + `); + } + function userPlusCloseIcon() { + return (` + + ${bgCircle()} + + + + `); + } + function plusIcon() { + return (` + + ${bgCircle()} + + + `); + } + function pencilIcon() { + return (` + + ${bgCircle()} + + + `); + } + function pencilOffIcon() { + return (` + + ${bgCircle()} + + + `); + } + function trashIcon() { + return (` + + ${bgCircle()} + + + `); + } + function historyBackIcon() { + return (` + + ${bgCircle()} + + + `); + } + function historyForwardIcon() { + return (` + + ${bgCircle()} + + + `); + } + function personIcon() { + return (` + + + + `); + } + function miniTreeIcon() { + return (` + + + + + + + + + + + `); + } + function toggleIconOn() { + return (` + + ${bgCircle()} + + + + `); + } + function toggleIconOff() { + return (` + + ${bgCircle()} + + + + `); + } + function chevronDownIcon() { + return (` + + ${bgCircle()} + + + `); + } + function chevronUpIcon() { + return (` + + ${bgCircle()} + + + `); + } + function linkOffIcon() { + return (` + + ${bgCircle()} + + + `); + } + function infoIcon() { + return (` + + ${bgCircle()} + + + `); + } + function userSvgIcon() { return svgWrapper(userIcon()); } + function userEditSvgIcon() { return svgWrapper(userEditIcon()); } + function userPlusSvgIcon() { return svgWrapper(userPlusIcon()); } + function userPlusCloseSvgIcon() { return svgWrapper(userPlusCloseIcon()); } + function plusSvgIcon() { return svgWrapper(plusIcon()); } + function pencilSvgIcon() { return svgWrapper(pencilIcon()); } + function pencilOffSvgIcon() { return svgWrapper(pencilOffIcon()); } + function trashSvgIcon() { return svgWrapper(trashIcon()); } + function historyBackSvgIcon() { return svgWrapper(historyBackIcon()); } + function historyForwardSvgIcon() { return svgWrapper(historyForwardIcon()); } + function personSvgIcon() { return svgWrapper(personIcon(), '0 0 512 512'); } + function miniTreeSvgIcon() { return svgWrapper(miniTreeIcon(), '0 0 72 25'); } + function toggleSvgIconOn() { return svgWrapper(toggleIconOn()); } + function toggleSvgIconOff() { return svgWrapper(toggleIconOff()); } + function chevronDownSvgIcon() { return svgWrapper(chevronDownIcon()); } + function chevronUpSvgIcon() { return svgWrapper(chevronUpIcon()); } + function linkOffSvgIcon() { return svgWrapper(linkOffIcon()); } + function infoSvgIcon() { return svgWrapper(infoIcon()); } + function svgWrapper(icon, viewBox = '0 0 24 24') { + const match = icon.match(/data-icon="([^"]+)"/); + const dataIcon = match ? `data-icon="${match[1]}"` : ''; + return (` + + ${icon} + + `); + } + function bgCircle() { + return (` + + `); + } + + var icons = /*#__PURE__*/Object.freeze({ + __proto__: null, + chevronDownIcon: chevronDownIcon, + chevronDownSvgIcon: chevronDownSvgIcon, + chevronUpIcon: chevronUpIcon, + chevronUpSvgIcon: chevronUpSvgIcon, + historyBackIcon: historyBackIcon, + historyBackSvgIcon: historyBackSvgIcon, + historyForwardIcon: historyForwardIcon, + historyForwardSvgIcon: historyForwardSvgIcon, + infoIcon: infoIcon, + infoSvgIcon: infoSvgIcon, + linkOffIcon: linkOffIcon, + linkOffSvgIcon: linkOffSvgIcon, + miniTreeIcon: miniTreeIcon, + miniTreeSvgIcon: miniTreeSvgIcon, + pencilIcon: pencilIcon, + pencilOffIcon: pencilOffIcon, + pencilOffSvgIcon: pencilOffSvgIcon, + pencilSvgIcon: pencilSvgIcon, + personIcon: personIcon, + personSvgIcon: personSvgIcon, + plusIcon: plusIcon, + plusSvgIcon: plusSvgIcon, + toggleIconOff: toggleIconOff, + toggleIconOn: toggleIconOn, + toggleSvgIconOff: toggleSvgIconOff, + toggleSvgIconOn: toggleSvgIconOn, + trashIcon: trashIcon, + trashSvgIcon: trashSvgIcon, + userEditIcon: userEditIcon, + userEditSvgIcon: userEditSvgIcon, + userIcon: userIcon, + userPlusCloseIcon: userPlusCloseIcon, + userPlusCloseSvgIcon: userPlusCloseSvgIcon, + userPlusIcon: userPlusIcon, + userPlusSvgIcon: userPlusSvgIcon, + userSvgIcon: userSvgIcon + }); + + function getHtmlNew(form_creator) { + return (` +
+ ${closeBtn()} +

${form_creator.title}

+ ${genderRadio(form_creator)} + + ${fields(form_creator)} + +
+ + +
+ + ${form_creator.linkExistingRelative ? addLinkExistingRelative(form_creator) : ''} +
+ `); + } + function getHtmlEdit(form_creator) { + return (` +
+ ${closeBtn()} +
+ ${!form_creator.no_edit ? addRelativeBtn(form_creator) : ''} + ${form_creator.no_edit ? spaceDiv() : editBtn(form_creator)} +
+ + ${genderRadio(form_creator)} + + ${fields(form_creator)} + +
+ + +
+ + ${form_creator.linkExistingRelative ? addLinkExistingRelative(form_creator) : ''} + +
+ ${deleteBtn(form_creator)} + + ${removeRelativeBtn(form_creator)} +
+ `); + } + function deleteBtn(form_creator) { + return (` +
+ +
+ `); + } + function removeRelativeBtn(form_creator) { + return (` +
+ +
+ `); + } + function addRelativeBtn(form_creator) { + return (` + + ${form_creator.addRelativeActive ? userPlusCloseSvgIcon() : userPlusSvgIcon()} + + `); + } + function editBtn(form_creator) { + return (` + + ${form_creator.editable ? pencilOffSvgIcon() : pencilSvgIcon()} + + `); + } + function genderRadio(form_creator) { + if (!form_creator.editable) + return ''; + return (` +
+ ${form_creator.gender_field.options.map(option => (` + + `)).join('')} +
+ `); + } + function fields(form_creator) { + if (!form_creator.editable) + return infoField(); + let fields_html = ''; + form_creator.fields.forEach(field => { + if (field.type === 'text') { + fields_html += ` +
+ + +
`; + } + else if (field.type === 'textarea') { + fields_html += ` +
+ + +
`; + } + else if (field.type === 'select') { + const select_field = field; + fields_html += ` +
+ + +
`; + } + else if (field.type === 'rel_reference') { + fields_html += ` +
+ + +
`; + } + }); + return fields_html; + function infoField() { + let fields_html = ''; + form_creator.fields.forEach(field => { + var _a; + if (field.type === 'rel_reference') { + if (!field.initial_value) + return; + fields_html += ` +
+ ${field.label} - ${field.rel_label} + ${field.initial_value || ''} +
`; + } + else if (field.type === 'select') { + const select_field = field; + if (!field.initial_value) + return; + fields_html += ` +
+ ${select_field.label} + ${((_a = select_field.options.find(option => option.value === select_field.initial_value)) === null || _a === void 0 ? void 0 : _a.label) || ''} +
`; + } + else { + fields_html += ` +
+ ${field.label} + ${field.initial_value || ''} +
`; + } + }); + return fields_html; + } + } + function addLinkExistingRelative(form_creator) { + const title = form_creator.linkExistingRelative.hasOwnProperty('title') ? form_creator.linkExistingRelative.title : 'Profile already exists?'; + const select_placeholder = form_creator.linkExistingRelative.hasOwnProperty('select_placeholder') ? form_creator.linkExistingRelative.select_placeholder : 'Select profile'; + const options = form_creator.linkExistingRelative.options; + return (` +
+
+ +
+ `); + } + function closeBtn() { + return (` + + × + + `); + } + function spaceDiv() { + return `
`; + } + + function createFormNew(form_creator, closeCallback) { + return createForm(form_creator, closeCallback); + } + function createFormEdit(form_creator, closeCallback) { + return createForm(form_creator, closeCallback); + } + function createForm(form_creator, closeCallback) { + const is_new = isNewRelFormCreator(form_creator); + const formContainer = document.createElement('div'); + reload(); + return formContainer; + function reload() { + const formHtml = is_new ? getHtmlNew(form_creator) : getHtmlEdit(form_creator); + formContainer.innerHTML = formHtml; + setupEventListenersBase(formContainer, form_creator, closeCallback, reload); + if (is_new) + setupEventListenersNew(formContainer, form_creator); + else + setupEventListenersEdit(formContainer, form_creator, reload); + if (form_creator.onFormCreation) { + form_creator.onFormCreation({ + cont: formContainer, + form_creator: form_creator + }); + } + } + function isNewRelFormCreator(form_creator) { + return 'new_rel' in form_creator; + } + } + function setupEventListenersBase(formContainer, form_creator, closeCallback, reload) { + const form = formContainer.querySelector('form'); + form.addEventListener('submit', form_creator.onSubmit); + const cancel_btn = form.querySelector('.f3-cancel-btn'); + cancel_btn.addEventListener('click', onCancel); + const close_btn = form.querySelector('.f3-close-btn'); + close_btn.addEventListener('click', closeCallback); + function onCancel() { + form_creator.editable = false; + if (form_creator.onCancel) + form_creator.onCancel(); + reload(); + } + } + function setupEventListenersNew(formContainer, form_creator) { + const form = formContainer.querySelector('form'); + const link_existing_relative_select = form.querySelector('.f3-link-existing-relative select'); + if (link_existing_relative_select) { + link_existing_relative_select.addEventListener('change', form_creator.linkExistingRelative.onSelect); + } + } + function setupEventListenersEdit(formContainer, form_creator, reload) { + const form = formContainer.querySelector('form'); + const edit_btn = form.querySelector('.f3-edit-btn'); + if (edit_btn) + edit_btn.addEventListener('click', onEdit); + const delete_btn = form.querySelector('.f3-delete-btn'); + if (delete_btn && form_creator.onDelete) { + delete_btn.addEventListener('click', form_creator.onDelete); + } + const add_relative_btn = form.querySelector('.f3-add-relative-btn'); + if (add_relative_btn && form_creator.addRelative) { + add_relative_btn.addEventListener('click', () => { + if (form_creator.addRelativeActive) + form_creator.addRelativeCancel(); + else + form_creator.addRelative(); + form_creator.addRelativeActive = !form_creator.addRelativeActive; + reload(); + }); + } + const remove_relative_btn = form.querySelector('.f3-remove-relative-btn'); + if (remove_relative_btn && form_creator.removeRelative) { + remove_relative_btn.addEventListener('click', () => { + if (form_creator.removeRelativeActive) + form_creator.removeRelativeCancel(); + else + form_creator.removeRelative(); + form_creator.removeRelativeActive = !form_creator.removeRelativeActive; + reload(); + }); + } + const link_existing_relative_select = form.querySelector('.f3-link-existing-relative select'); + if (link_existing_relative_select) { + link_existing_relative_select.addEventListener('change', form_creator.linkExistingRelative.onSelect); + } + function onEdit() { + form_creator.editable = !form_creator.editable; + reload(); + } + } + + function createHistory(store, getStoreDataCopy, onUpdate) { + let history = []; + let history_index = -1; + return { + changed, + back, + forward, + canForward, + canBack + }; + function changed() { + if (history_index < history.length - 1) + history = history.slice(0, history_index + 1); + const clean_data = getStoreDataCopy(); + clean_data.main_id = store.getMainId(); + history.push(clean_data); + history_index++; + } + function back() { + if (!canBack()) + return; + history_index--; + updateData(history[history_index]); + } + function forward() { + if (!canForward()) + return; + history_index++; + updateData(history[history_index]); + } + function canForward() { + return history_index < history.length - 1; + } + function canBack() { + return history_index > 0; + } + function updateData(data) { + const current_main_id = store.getMainId(); + data = JSON.parse(JSON.stringify(data)); + if (!data.find(d => d.id === current_main_id)) + store.updateMainId(data.main_id); + store.updateData(data); + onUpdate(); + } + } + function createHistoryControls(cont, history) { + const history_controls = d3__namespace.select(cont).append("div").attr("class", "f3-history-controls"); + cont.insertBefore(history_controls.node(), cont.firstChild); + const back_btn = history_controls.append("button").attr("class", "f3-back-button").on("click", () => { + history.back(); + updateButtons(); + }); + const forward_btn = history_controls.append("button").attr("class", "f3-forward-button").on("click", () => { + history.forward(); + updateButtons(); + }); + back_btn.html(historyBackSvgIcon()); + forward_btn.html(historyForwardSvgIcon()); + return { + back_btn: back_btn.node(), + forward_btn: forward_btn.node(), + updateButtons, + destroy + }; + function updateButtons() { + back_btn.classed("disabled", !history.canBack()); + forward_btn.classed("disabled", !history.canForward()); + if (!history.canBack() && !history.canForward()) { + history_controls.style("opacity", 0).style("pointer-events", "none"); + } + else { + history_controls.style("opacity", 1).style("pointer-events", "auto"); + } + } + function destroy() { + d3__namespace.select(cont).select('.f3-history-controls').remove(); + } + } + + var handlers = /*#__PURE__*/Object.freeze({ + __proto__: null, + addNewPerson: addNewPerson, + calculateDelay: calculateDelay, + calculateTreeFit: calculateTreeFit, + cardChangeMain: cardChangeMain, + cardComponentSetup: cardComponentSetup, + cardToMiddle: cardToMiddle, + checkIfConnectedToFirstPerson: checkIfConnectedToFirstPerson, + checkIfRelativesConnectedWithoutPerson: checkIfRelativesConnectedWithoutPerson, + cleanupDataJson: cleanupDataJson, + createFormEdit: createFormEdit, + createFormNew: createFormNew, + createHistory: createHistory, + createHistoryControls: createHistoryControls, + createNewPerson: createNewPerson, + createNewPersonWithGenderFromRel: createNewPersonWithGenderFromRel, + deletePerson: deletePerson, + getCurrentZoom: getCurrentZoom, + htmlContSetup: htmlContSetup, + isAllRelativeDisplayed: isAllRelativeDisplayed, + manualZoom: manualZoom, + moveToAddToAdded: moveToAddToAdded, + onDeleteSyncRelReference: onDeleteSyncRelReference, + removeToAdd: removeToAdd, + removeToAddFromData: removeToAddFromData, + setupZoom: setupZoom, + submitFormData: submitFormData, + syncRelReference: syncRelReference, + treeFit: treeFit, + zoomTo: zoomTo + }); + + function CardBody({ d, card_dim, card_display }) { + return { template: (` + + + ${CardText({ d, card_dim, card_display }).template} + + `) + }; + } + function CardBodyAddNewRel({ d, card_dim, label }) { + return { template: (` + + + + ${label} + + + `) + }; + } + function CardText({ d, card_dim, card_display }) { + return { template: (` + + + + + ${Array.isArray(card_display) ? card_display.map(cd => `${cd(d.data)}`).join('\n') : card_display(d.data)} + + + + + + `) + }; + } + function CardBodyOutline({ d, card_dim, is_new }) { + return { template: (` + + `) + }; + } + function MiniTree({ d, card_dim }) { + return ({ template: (` + + + + + + + + + + + `) }); + } + function CardImage({ d, image, card_dim, maleIcon, femaleIcon }) { + return ({ template: (` + + ${image + ? `` + : (d.data.data.gender === "F" && false) ? femaleIcon({ card_dim }) + : (d.data.data.gender === "M" && false) ? maleIcon({ card_dim }) + : GenderlessIcon()} + + `) }); + function GenderlessIcon() { + return (` + + + + + + + `); + } + } + function appendTemplate(template, parent, is_first) { + const g = document.createElementNS("http://www.w3.org/2000/svg", 'g'); + g.innerHTML = template; + if (is_first) + parent.insertBefore(g, parent.firstChild); + else + parent.appendChild(g); + } + + const CardElements = { + miniTree, + cardBody, + cardImage + }; + function miniTree(d, props) { + if (d.data.to_add) + return; + const card_dim = props.card_dim; + if (d.all_rels_displayed) + return; + const g = d3__namespace.create('svg:g').html(MiniTree({ d, card_dim }).template); + g.on("click", function (e) { + e.stopPropagation(); + if (props.onMiniTreeClick) + props.onMiniTreeClick.call(this, e, d); + else + cardChangeMain(props.store, { d }); + }); + return g.node(); + } + function cardBody(d, props) { + const card_dim = props.card_dim; + const g = d3__namespace.create('svg:g').html(CardBody({ d, card_dim, card_display: props.card_display }).template); + g.on("click", function (e) { + e.stopPropagation(); + if (props.onCardClick) + props.onCardClick.call(this, e, d); + else + cardChangeMain(props.store, { d }); + }); + return g.node(); + } + function cardImage(d, props) { + if (d.data.to_add) + return; + const card_dim = props.card_dim; + const g = d3__namespace.create('svg:g').html(CardImage({ d, image: d.data.data.avatar || null, card_dim, maleIcon: undefined, femaleIcon: undefined }).template); + return g.node(); + } + function appendElement(el_maybe, parent, is_first = false) { + if (!el_maybe) + return; + if (is_first) + parent.insertBefore(el_maybe, parent.firstChild); + else + parent.appendChild(el_maybe); + } + + function handleCardDuplicateToggle(node, d, is_horizontal, updateTree) { + if (!d.hasOwnProperty('_toggle')) return + + const card = node.querySelector('.card'); + const card_inner = card.querySelector('.card-inner'); + const card_width = node.querySelector('.card').offsetWidth; + node.querySelector('.card').offsetHeight; + let toggle_is_off; + let toggle_id; + const pos = {}; + if (d.spouse) { + const spouse = d.spouse; + const parent_id = spouse.data.main ? 'main' : spouse.parent.data.id; + toggle_is_off = spouse.data._tgdp_sp[parent_id][d.data.id] < 0; + pos.top = 60; + pos.left = d.sx-d.x-30+card_width/2; + if (is_horizontal) { + pos.top = d.sy - d.x + 4; + pos.left = card_width/2 + 4; + if ((Math.abs(d.sx - d.y)) < 10) pos.left = card_width - 4; + } + toggle_id = spouse._toggle_id_sp ? spouse._toggle_id_sp[d.data.id] : -1; + if (toggle_id === -1) return + } else { + const parent_id = d.data.main ? 'main' : d.parent.data.id; + toggle_is_off = d.data._tgdp[parent_id] < 0; + pos.top = -65; + pos.left = -30+card_width/2; + if (is_horizontal) { + pos.top = 5; + pos.left = -55; + } + toggle_id = d._toggle_id; + } + + card_inner.style.zIndex = 1; + + const toggle_div = d3__namespace.select(card) + .append('div') + .attr('class', 'f3-toggle-div') + .attr('style', 'cursor: pointer; width: 60px; height: 60px;position: absolute; z-index: -1;') + .style('top', pos.top+'px') + .style('left', pos.left+'px') + .on('click', (e) => { + e.stopPropagation(); + if (d.spouse) { + const spouse = d.spouse; + const parent_id = spouse.data.main ? 'main' : spouse.parent.data.id; + if (!spouse.data._tgdp_sp[parent_id].hasOwnProperty(d.data.id)) console.error('no toggle', d, spouse); + let val = spouse.data._tgdp_sp[parent_id][d.data.id]; + if (val < 0) val = new Date().getTime(); + else val = -new Date().getTime(); + spouse.data._tgdp_sp[parent_id][d.data.id] = val; + } else { + const parent_id = d.data.main ? 'main' : d.parent.data.id; + let val = d.data._tgdp[parent_id]; + if (val < 0) val = new Date().getTime(); + else val = -new Date().getTime(); + d.data._tgdp[parent_id] = val; + } + + updateTree(); + }); + + toggle_div + .append('div') + .html(toggle_is_off ? toggleSvgIconOff() : toggleSvgIconOn()) + .select('svg') + .classed('f3-toggle-icon', true) + .style('color', toggle_is_off ? '#585656' : '#61bf52') + .style('padding', '0'); + + d3__namespace.select(card) + .select('.f3-toggle-icon .f3-small-circle') + .style('fill', '#fff'); + + d3__namespace.select(card) + .select('.f3-toggle-icon') + .append('text') + .attr('transform', toggle_is_off ? 'translate(10.6, 14.5)' : 'translate(4.1, 14.5)') + .attr('fill', toggle_is_off ? '#fff' : '#fff') + .attr('font-size', '7px') + .text('C'+toggle_id); + + + if (toggle_is_off) { + let transform; + if (d.is_ancestry) { + if (is_horizontal) transform = 'translate(5, -30)rotate(-90)'; + else transform = 'translate(0, -10)'; + } else { + if (is_horizontal) transform = 'translate(11, -22)rotate(90)'; + else transform = 'translate(-7, -32)rotate(180)'; + } + d3__namespace.select(card) + .select('.f3-toggle-div') + .insert('div') + .html(miniTreeSvgIcon()) + .select('svg') + .attr('style', 'position: absolute; z-index: -1;top: 0;left: 0;border-radius: 0;') + .style('width', '66px') + .style('height', '112px') + .attr('transform', transform) + .attr('viewBox', '0 0 72 125') + .select('line') + .attr('y1', d.is_ancestry ? '62' : '92'); + } + } + + function CardHtml$2(props) { + const cardInner = props.style === 'default' ? cardInnerDefault + : props.style === 'imageCircleRect' ? cardInnerImageCircleRect + : props.style === 'imageCircle' ? cardInnerImageCircle + : props.style === 'imageRect' ? cardInnerImageRect + : props.style === 'rect' ? cardInnerRect + : cardInnerDefault; + return function (d) { + this.innerHTML = (` +
+ ${props.mini_tree ? getMiniTree(d) : ''} + ${(props.cardInnerHtmlCreator && !d.data._new_rel_data) ? props.cardInnerHtmlCreator(d) : cardInner(d)} +
+ `); + this.querySelector('.card').addEventListener('click', (e) => props.onCardClick(e, d)); + if (props.onCardUpdate) + props.onCardUpdate.call(this, d); + if (props.onCardMouseenter) + d3__namespace.select(this).select('.card').on('mouseenter', e => props.onCardMouseenter(e, d)); + if (props.onCardMouseleave) + d3__namespace.select(this).select('.card').on('mouseleave', e => props.onCardMouseleave(e, d)); + if (d.duplicate) + handleCardDuplicateHover(this, d); + if (props.duplicate_branch_toggle) + handleCardDuplicateToggle(this, d, props.store.state.is_horizontal, props.store.updateTree); + if (location.origin.includes('localhost')) { + d.__node = this.querySelector('.card'); + d.__label = d.data.data['first name']; + if (d.data.to_add) { + const spouse = d.spouse || d.coparent || null; + if (spouse) + d3__namespace.select(this).select('.card').attr('data-to-add', spouse.data.data['first name']); + } + } + }; + function getCardInnerImageCircle(d) { + return (` +
+ ${d.data.data[props.cardImageField] ? `` : noImageIcon(d)} +
${textDisplay(d)}
+ ${d.duplicate ? getCardDuplicateTag(d) : ''} +
+ `); + } + function getCardInnerImageRect(d) { + return (` +
+ ${d.data.data[props.cardImageField] ? `` : noImageIcon(d)} +
${textDisplay(d)}
+ ${d.duplicate ? getCardDuplicateTag(d) : ''} +
+ `); + } + function getCardInnerRect(d) { + return (` +
+ ${textDisplay(d)} + ${d.duplicate ? getCardDuplicateTag(d) : ''} +
+ `); + } + function textDisplay(d) { + if (d.data._new_rel_data) + return newRelDataDisplay(d); + if (d.data.to_add) + return `
${props.empty_card_label || 'ADD'}
`; + if (d.data.unknown) + return `
${props.unknown_card_label || 'UNKNOWN'}
`; + return (` + ${props.card_display.map(display => `
${display(d.data)}
`).join('')} + `); + } + function newRelDataDisplay(d) { + const attr_list = []; + attr_list.push(`data-rel-type="${d.data._new_rel_data.rel_type}"`); + if (['son', 'daughter'].includes(d.data._new_rel_data.rel_type)) + attr_list.push(`data-other-parent-id="${d.data._new_rel_data.other_parent_id}"`); + return `
${d.data._new_rel_data.label}
`; + } + function getMiniTree(d) { + if (!props.mini_tree) + return ''; + if (d.data.to_add) + return ''; + if (d.data._new_rel_data) + return ''; + if (d.all_rels_displayed) + return ''; + return `
${miniTreeSvgIcon()}
`; + } + function cardInnerImageCircleRect(d) { + return d.data.data[props.cardImageField] ? cardInnerImageCircle(d) : cardInnerRect(d); + } + function cardInnerDefault(d) { + return getCardInnerImageRect(d); + } + function cardInnerImageCircle(d) { + return getCardInnerImageCircle(d); + } + function cardInnerImageRect(d) { + return getCardInnerImageRect(d); + } + function cardInnerRect(d) { + return getCardInnerRect(d); + } + function getClassList(d) { + const class_list = []; + if (d.data.data.gender === 'M') + class_list.push('card-male'); + else if (d.data.data.gender === 'F') + class_list.push('card-female'); + else + class_list.push('card-genderless'); + class_list.push(`card-depth-${d.is_ancestry ? -d.depth : d.depth}`); + if (d.data.main) + class_list.push('card-main'); + if (d.data._new_rel_data) + class_list.push('card-new-rel'); + if (d.data.to_add) + class_list.push('card-to-add'); + if (d.data.unknown) + class_list.push('card-unknown'); + return class_list; + } + function getCardStyle() { + let style = 'style="'; + if (props.card_dim.w || props.card_dim.h) { + style += `width: ${props.card_dim.w}px; min-height: ${props.card_dim.h}px;`; + if (props.card_dim.height_auto) + style += 'height: auto;'; + else + style += `height: ${props.card_dim.h}px;`; + } + else { + return ''; + } + style += '"'; + return style; + } + function getCardImageStyle() { + let style = 'style="position: relative;'; + if (props.card_dim.img_w || props.card_dim.img_h || props.card_dim.img_x || props.card_dim.img_y) { + style += `width: ${props.card_dim.img_w}px; height: ${props.card_dim.img_h}px;`; + style += `left: ${props.card_dim.img_x}px; top: ${props.card_dim.img_y}px;`; + } + else { + return ''; + } + style += '"'; + return style; + } + function noImageIcon(d) { + if (d.data._new_rel_data) + return `
${plusSvgIcon()}
`; + return `
${props.defaultPersonIcon ? props.defaultPersonIcon(d) : personSvgIcon()}
`; + } + function getCardDuplicateTag(d) { + return `
x${d.duplicate}
`; + } + function handleCardDuplicateHover(node, d) { + d3__namespace.select(node).on('mouseenter', e => { + d3__namespace.select(node.closest('.cards_view')).selectAll('.card_cont').select('.card').classed('f3-card-duplicate-hover', d0 => d0.data.id === d.data.id); + }); + d3__namespace.select(node).on('mouseleave', e => { + d3__namespace.select(node.closest('.cards_view')).selectAll('.card_cont').select('.card').classed('f3-card-duplicate-hover', false); + }); + } + } + + function setupCardSvgDefs(svg, card_dim) { + if (svg.querySelector("defs#f3CardDef")) + return; + svg.insertAdjacentHTML('afterbegin', (` + + + + + + + + + + + + + `)); + function curvedRectPath(dim, curve, no_curve_corners) { + const { w, h } = dim, c = curve, ncc = no_curve_corners || [], ncc_check = (corner) => ncc.includes(corner), lx = ncc_check('lx') ? `M0,0` : `M0,${c} Q 0,0 5,0`, rx = ncc_check('rx') ? `H${w}` : `H${w - c} Q ${w},0 ${w},5`, ry = ncc_check('ry') ? `V${h}` : `V${h - c} Q ${w},${h} ${w - c},${h}`, ly = ncc_check('ly') ? `H0` : `H${c} Q 0,${h} 0,${h - c}`; + return (`${lx} ${rx} ${ry} ${ly} z`); + } + } + function updateCardSvgDefs(svg, card_dim) { + if (svg.querySelector("defs#f3CardDef")) { + svg.querySelector("defs#f3CardDef").remove(); + } + setupCardSvgDefs(svg, card_dim); + } + + function CardSvg$2(props) { + props = setupProps(props); + setupCardSvgDefs(props.svg, props.card_dim); + return function (d) { + const gender_class = d.data.data.gender === 'M' ? 'card-male' : d.data.data.gender === 'F' ? 'card-female' : 'card-genderless'; + const card_dim = props.card_dim; + const card = d3__namespace.create('svg:g').attr('class', `card ${gender_class}`).attr('transform', `translate(${[-card_dim.w / 2, -card_dim.h / 2]})`); + card.append('g').attr('class', 'card-inner').attr('clip-path', 'url(#card_clip)'); + this.innerHTML = ''; + this.appendChild(card.node()); + card.on("click", function (e) { + e.stopPropagation(); + props.onCardClick.call(this, e, d); + }); + if (d.data._new_rel_data) { + appendTemplate(CardBodyOutline({ d, card_dim, is_new: d.data.to_add }).template, card.node(), true); + appendTemplate(CardBodyAddNewRel({ d, card_dim, label: d.data._new_rel_data.label }).template, this.querySelector('.card-inner'), true); + d3__namespace.select(this.querySelector('.card-inner')) + .append('g') + .attr('class', 'card-edit-icon') + .attr('fill', 'currentColor') + .attr('transform', `translate(-1,2)scale(${card_dim.img_h / 22})`) + .html(plusIcon()); + } + else { + appendTemplate(CardBodyOutline({ d, card_dim, is_new: d.data.to_add }).template, card.node(), true); + appendTemplate(CardBody({ d, card_dim, card_display: props.card_display }).template, this.querySelector('.card-inner'), false); + if (props.img) + appendElement(CardElements.cardImage(d, props), this.querySelector('.card')); + if (props.mini_tree) + appendElement(CardElements.miniTree(d, props), this.querySelector('.card'), true); + } + if (props.onCardUpdate) + props.onCardUpdate.call(this, d); + }; + function setupProps(props) { + const default_props = { + img: true, + mini_tree: true, + link_break: false, + card_dim: { w: 220, h: 70, text_x: 75, text_y: 15, img_w: 60, img_h: 60, img_x: 5, img_y: 5 } + }; + if (!props) + props = {}; + for (const k in default_props) { + if (typeof props[k] === 'undefined') + props[k] = default_props[k]; + } + return props; + } + } + /** + * @deprecated Use cardSvg instead. This export will be removed in a future version. + */ + function Card(props) { + if (props.onCardClick === undefined) + props.onCardClick = (e, d) => { + props.store.updateMainId(d.data.id); + props.store.updateTree({}); + }; + return CardSvg$2(props); + } + + function createInfoPopup (cont, onClose) { return new InfoPopup(cont, onClose); } + class InfoPopup { + constructor(cont, onClose) { + this.cont = cont; + this.active = false; + this.onClose = onClose; + this.popup_cont = d3__namespace.select(this.cont).append('div').attr('class', 'f3-popup').node(); + this.create(); + } + create() { + const popup = d3__namespace.select(this.popup_cont); + popup.html(` +
+ × +
+
+ `); + popup.select('.f3-popup-close').on('click', () => { + this.close(); + }); + popup.on('click', (event) => { + if (event.target == popup.node()) { + this.close(); + } + }); + } + activate(content) { + const popup_content_inner = d3__namespace.select(this.popup_cont).select('.f3-popup-content-inner').node(); + if (content) + popup_content_inner.appendChild(content); + this.open(); + } + open() { + this.active = true; + } + close() { + this.popup_cont.remove(); + this.active = false; + if (this.onClose) + this.onClose(); + } + } + + // https://support.ancestry.co.uk/s/article/Understanding-Kinship-Terms + function calculateKinships(d_id, data_stash, kinship_info_config) { + const main_datum = data_stash.find(d => d.id === d_id); + const kinships = {}; + loopCheck(main_datum.id, 'self', 0); + setupHalfKinships(kinships); + if (kinship_info_config.show_in_law) + setupInLawKinships(kinships, data_stash); + setupKinshipsGender(kinships); + return kinships; + function loopCheck(d_id, kinship, depth, prev_rel_id = undefined) { + if (!d_id) + return; + // if (kinships[d_id] && kinships[d_id] !== kinship) console.error('kinship mismatch, kinship 1: ', kinships[d_id], 'kinship 2: ', kinship) + if (kinships[d_id]) + return; + if (kinship) + kinships[d_id] = kinship; + const datum = data_stash.find(d => d.id === d_id); + const rels = datum.rels; + if (kinship === 'self') { + rels.parents.forEach(p_id => loopCheck(p_id, 'parent', depth - 1, d_id)); + (rels.spouses || []).forEach(id => loopCheck(id, 'spouse', depth)); + (rels.children || []).forEach(id => loopCheck(id, 'child', depth + 1)); + } + else if (kinship === 'parent') { + rels.parents.forEach(p_id => loopCheck(p_id, 'grandparent', depth - 1, d_id)); + (rels.children || []).forEach(id => { + if (prev_rel_id && prev_rel_id === id) + return; + loopCheck(id, 'sibling', depth + 1); + }); + } + else if (kinship === 'spouse') ; + else if (kinship === 'child') { + (rels.children || []).forEach(id => loopCheck(id, 'grandchild', depth + 1)); + } + else if (kinship === 'sibling') { + (rels.children || []).forEach(id => loopCheck(id, 'nephew', depth + 1)); + } + else if (kinship === 'grandparent') { + if (!prev_rel_id) + console.error(`${kinship} should have prev_rel_id`); + rels.parents.forEach(p_id => loopCheck(p_id, 'great-grandparent', depth - 1, d_id)); + (rels.children || []).forEach(id => { + if (prev_rel_id && prev_rel_id === id) + return; + loopCheck(id, 'uncle', depth + 1); + }); + } + else if (kinship.includes('grandchild')) { + (rels.children || []).forEach(id => loopCheck(id, getGreatKinship(kinship, depth + 1), depth + 1)); + } + else if (kinship.includes('great-grandparent')) { + if (!prev_rel_id) + console.error(`${kinship} should have prev_rel_id`); + rels.parents.forEach(p_id => loopCheck(p_id, getGreatKinship(kinship, depth - 1), depth - 1, d_id)); + (rels.children || []).forEach(id => { + if (prev_rel_id && prev_rel_id === id) + return; + const great_count = getGreatCount(depth + 1); + if (great_count === 0) + loopCheck(id, 'granduncle', depth + 1); + else if (great_count > 0) + loopCheck(id, getGreatKinship('granduncle', depth + 1), depth + 1); + else + console.error(`${kinship} should have great_count > -1`); + }); + } + else if (kinship === 'nephew') { + (rels.children || []).forEach(id => loopCheck(id, 'grandnephew', depth + 1)); + } + else if (kinship.includes('grandnephew')) { + (rels.children || []).forEach(id => loopCheck(id, getGreatKinship(kinship, depth + 1), depth + 1)); + } + else if (kinship === 'uncle') { + (rels.children || []).forEach(id => loopCheck(id, '1st Cousin', depth + 1)); + } + else if (kinship === 'granduncle') { + (rels.children || []).forEach(id => loopCheck(id, '1st Cousin 1x removed', depth + 1)); + } + else if (kinship.includes('great-granduncle')) { + const child_depth = depth + 1; + const removed_count = Math.abs(child_depth); + (rels.children || []).forEach(id => loopCheck(id, `1st Cousin ${removed_count}x removed`, child_depth)); + } + else if (kinship.slice(4).startsWith('Cousin')) { + (rels.children || []).forEach(id => { + const child_depth = depth + 1; + const removed_count = Math.abs(child_depth); + const cousin_count = +kinship[0]; + if (child_depth === 0) { + loopCheck(id, `${getOrdinal(cousin_count + 1)} Cousin`, child_depth); + } + else if (child_depth < 0) { + loopCheck(id, `${getOrdinal(cousin_count + 1)} Cousin ${removed_count}x removed`, child_depth); + } + else if (child_depth > 0) { + loopCheck(id, `${getOrdinal(cousin_count)} Cousin ${removed_count}x removed`, child_depth); + } + }); + } + else + console.error(`${kinship} not found`); + } + function setupHalfKinships(kinships) { + const half_kinships = []; + Object.keys(kinships).forEach(d_id => { + const kinship = kinships[d_id]; + if (kinship.includes('child')) + return; + if (kinship === 'spouse') + return; + const same_ancestors = findSameAncestor(main_datum.id, d_id, data_stash); + if (!same_ancestors) + return console.error(`${data_stash.find(d => d.id === d_id).data} not found in main_ancestry`); + if (same_ancestors.is_half_kin) + half_kinships.push(d_id); + }); + half_kinships.forEach(d_id => { + kinships[d_id] = `Half ${kinships[d_id]}`; + }); + } + function setupInLawKinships(kinships, data_stash) { + Object.keys(kinships).forEach(d_id => { + const kinship = kinships[d_id]; + const datum = data_stash.find(d => d.id === d_id); + if (kinship === 'spouse') { + const siblings = []; + datum.rels.parents.forEach(p_id => (getD(p_id).rels.children || []).forEach(d_id => siblings.push(d_id))); + siblings.forEach(sibling_id => { if (!kinships[sibling_id]) + kinships[sibling_id] = 'sibling-in-law'; }); // gender label is added in setupKinshipsGender + } + if (kinship === 'sibling') { + (datum.rels.spouses || []).forEach(spouse_id => { + if (!kinships[spouse_id]) + kinships[spouse_id] = 'sibling-in-law'; + }); + } + if (kinship === 'child') { + (datum.rels.spouses || []).forEach(spouse_id => { if (!kinships[spouse_id]) + kinships[spouse_id] = 'child-in-law'; }); // gender label is added in setupKinshipsGender + } + if (kinship === 'uncle') { + (datum.rels.spouses || []).forEach(spouse_id => { if (!kinships[spouse_id]) + kinships[spouse_id] = 'uncle-in-law'; }); // gender label is added in setupKinshipsGender + } + if (kinship.includes('Cousin')) { + (datum.rels.spouses || []).forEach(spouse_id => { if (!kinships[spouse_id]) + kinships[spouse_id] = `${kinship} in-law`; }); // gender label is added in setupKinshipsGender + } + }); + } + function setupKinshipsGender(kinships) { + Object.keys(kinships).forEach(d_id => { + const kinship = kinships[d_id]; + const datum = data_stash.find(d => d.id === d_id); + const gender = datum.data.gender; + if (kinship.includes('parent')) { + const rel_type_general = 'parent'; + const rel_type = gender === 'M' ? 'father' : gender === 'F' ? 'mother' : rel_type_general; + kinships[d_id] = kinships[d_id].replace('parent', rel_type); + } + else if (kinship.includes('sibling')) { + const rel_type_general = 'sibling'; + const rel_type = gender === 'M' ? 'brother' : gender === 'F' ? 'sister' : rel_type_general; + kinships[d_id] = kinships[d_id].replace('sibling', rel_type); + } + else if (kinship.includes('child')) { + const rel_type_general = 'child'; + const rel_type = gender === 'M' ? 'son' : gender === 'F' ? 'daughter' : rel_type_general; + kinships[d_id] = kinships[d_id].replace('child', rel_type); + } + else if (kinship.includes('uncle')) { + const rel_type_general = 'aunt/uncle'; + const rel_type = gender === 'M' ? 'uncle' : gender === 'F' ? 'aunt' : rel_type_general; + kinships[d_id] = kinships[d_id].replace('uncle', rel_type); + } + else if (kinship.includes('nephew')) { + const rel_type_general = 'neice/nephew'; + const rel_type = gender === 'M' ? 'nephew' : gender === 'F' ? 'niece' : rel_type_general; + kinships[d_id] = kinships[d_id].replace('nephew', rel_type); + } + }); + } + function getD(d_id) { + return data_stash.find(d => d.id === d_id); + } + } + function findSameAncestor(main_id, rel_id, data_stash) { + const main_ancestry = getAncestry(main_id); + let found; + let is_ancestor; + let is_half_kin; + checkIfRel(rel_id); + checkIfSpouse(rel_id); + loopCheck(rel_id); + if (!found) + return null; + return { found, is_ancestor, is_half_kin }; + function loopCheck(rel_id) { + if (found) + return; + if (rel_id === main_id) { + is_ancestor = true; + found = rel_id; + is_half_kin = false; + return; + } + const d = data_stash.find(d => d.id === rel_id); + const rels = d.rels; + const parents = getParents(rels); + const found_parent = main_ancestry.find(p => (p[0] && parents[0] && p[0] === parents[0]) || (p[1] && parents[1] && p[1] === parents[1])); + if (found_parent) { + found = parents.filter((p, i) => p === found_parent[i]); + is_half_kin = checkIfHalfKin(parents, found_parent); + return; + } + rels.parents.forEach(p_id => loopCheck(p_id)); + } + function getAncestry(rel_id) { + const ancestry = []; + loopAdd(rel_id); + return ancestry; + function loopAdd(rel_id) { + const d = data_stash.find(d => d.id === rel_id); + const rels = d.rels; + ancestry.push(getParents(rels)); + rels.parents.forEach(p_id => loopAdd(p_id)); + } + } + function getParents(rels) { + return rels.parents; + } + function checkIfRel(rel_id) { + const d = data_stash.find(d => d.id === rel_id); + const found_parent = main_ancestry.find(p => p[0] === d.id || p[1] === d.id); + if (found_parent) { + is_ancestor = true; + found = rel_id; + is_half_kin = false; + } + } + function checkIfSpouse(rel_id) { + const main_datum = data_stash.find(d => d.id === main_id); + if ((main_datum.rels.spouses || []).includes(rel_id)) { + found = [main_id, rel_id]; + } + } + function checkIfHalfKin(ancestors1, ancestors2) { + return ancestors1.some((p, i) => p !== ancestors2[i]) || ancestors2.some((p, i) => p !== ancestors1[i]); + } + } + function getOrdinal(n) { + const s = ['st', 'nd', 'rd']; + return s[n - 1] ? n + s[n - 1] : n + 'th'; + } + function getGreatCount(depth) { + const depth_abs = Math.abs(depth); + return depth_abs - 2; + } + function getGreatKinship(kinship, depth) { + const great_count = getGreatCount(depth); + if (kinship.includes('great-')) + kinship = kinship.split('great-')[1]; + if (great_count === 1) { + return `great-${kinship}`; + } + else if (great_count > 1) { + return `${great_count}x-great-${kinship}`; + } + else { + console.error(`${kinship} should have great_count > 1`); + return kinship; + } + } + + function getKinshipsDataStash(main_id, rel_id, data_stash, kinships) { + var _a; + let in_law_id; + const kinship = kinships[rel_id].toLowerCase(); + if (kinship.includes('in-law')) { + in_law_id = rel_id; + const datum = data_stash.find(d => d.id === in_law_id); + if (kinship.includes('sister') || kinship.includes('brother')) { + rel_id = main_id; + } + else { + rel_id = (_a = datum.rels.spouses) === null || _a === void 0 ? void 0 : _a.find(d_id => kinships[d_id] && !kinships[d_id].includes('in-law')); + } + } + const same_ancestors = findSameAncestor(main_id, rel_id, data_stash); + if (!same_ancestors) + return console.error(`${rel_id} not found in main_ancestry`); + const same_ancestor_id = same_ancestors.is_ancestor ? same_ancestors.found : same_ancestors.found[0]; + const same_ancestor = data_stash.find(d => d.id === same_ancestor_id); + const root = d3__namespace.hierarchy(same_ancestor, hierarchyGetterChildren); + const same_ancestor_progeny = root.descendants().map(d => d.data.id); + const main_ancestry = getCleanAncestry(main_id, same_ancestor_progeny); + const rel_ancestry = getCleanAncestry(rel_id, same_ancestor_progeny); + loopClean(root); + const kinship_data_stash = root.descendants().map(d => { + const datum = { + id: d.data.id, + data: JSON.parse(JSON.stringify(d.data.data)), + kinship: kinships[d.data.id], + rels: { + parents: [], + spouses: [], + children: [] + } + }; + if (d.children && d.children.length > 0) + datum.rels.children = d.children.map(c => c.data.id); + return datum; + }); + if (kinship_data_stash.length > 0 && !same_ancestors.is_ancestor && !same_ancestors.is_half_kin) + addRootSpouse(kinship_data_stash); + if (in_law_id) + addInLawConnection(kinship_data_stash); + return kinship_data_stash; + function loopClean(tree_datum) { + tree_datum.children = (tree_datum.children || []).filter(child => { + if (main_ancestry.includes(child.data.id)) + return true; + if (rel_ancestry.includes(child.data.id)) + return true; + return false; + }); + tree_datum.children.forEach(child => loopClean(child)); + if (tree_datum.children.length === 0) + delete tree_datum.children; + } + function hierarchyGetterChildren(d) { + const children = [...(d.rels.children || [])].map(id => data_stash.find(d => d.id === id)).filter(d => d); + return children; + } + function getCleanAncestry(d_id, same_ancestor_progeny) { + const ancestry = [d_id]; + loopAdd(d_id); + return ancestry; + function loopAdd(d_id) { + const d = data_stash.find(d => d.id === d_id); + const rels = d.rels; + rels.parents.forEach(p_id => { + if (same_ancestor_progeny.includes(p_id)) { + ancestry.push(p_id); + loopAdd(p_id); + } + }); + } + } + function addRootSpouse(kinship_data_stash) { + const datum = kinship_data_stash[0]; + if (!same_ancestors) + return console.error(`${rel_id} not found in main_ancestry`); + const spouse_id = same_ancestor_id === same_ancestors.found[0] ? same_ancestors.found[1] : same_ancestors.found[0]; + datum.rels.spouses = [spouse_id]; + const spouse = data_stash.find(d => d.id === spouse_id); + const spouse_datum = { + id: spouse.id, + data: JSON.parse(JSON.stringify(spouse.data)), + kinship: kinships[spouse.id], + rels: { + spouses: [datum.id], + children: datum.rels.children, + parents: [] + } + }; + kinship_data_stash.push(spouse_datum); + (datum.rels.children || []).forEach(child_id => { + const child = data_stash.find(d => d.id === child_id); + const kinship_child = kinship_data_stash.find(d => d.id === child_id); + kinship_child.rels.parents = [...child.rels.parents]; + }); + } + function addInLawConnection(kinship_data_stash) { + if (kinship.includes('sister') || kinship.includes('brother')) { + addInLawSibling(kinship_data_stash); + } + else { + addInLawSpouse(kinship_data_stash); + } + } + function addInLawSpouse(kinship_data_stash) { + const datum = kinship_data_stash.find(d => d.id === rel_id); + const spouse_id = in_law_id; + datum.rels.spouses = [spouse_id]; + const spouse = data_stash.find(d => d.id === spouse_id); + const spouse_datum = { + id: spouse.id, + data: JSON.parse(JSON.stringify(spouse.data)), + kinship: kinships[spouse.id], + rels: { + spouses: [datum.id], + children: [], + parents: [] + } + }; + kinship_data_stash.push(spouse_datum); + } + function addInLawSibling(kinship_data_stash) { + var _a; + const datum = kinship_data_stash.find(d => d.id === rel_id); + const in_law_datum = getD(in_law_id); + kinship_data_stash.push({ + id: in_law_id, + data: JSON.parse(JSON.stringify(in_law_datum.data)), + kinship: kinships[in_law_id], + rels: { + spouses: [], + children: [], + parents: [] + } + }); + const siblings = []; + in_law_datum.rels.parents.forEach(p_id => (getD(p_id).rels.children || []).forEach(d_id => siblings.push(d_id))); + const spouse_id = (_a = getD(rel_id).rels.spouses) === null || _a === void 0 ? void 0 : _a.find(d_id => siblings.includes(d_id)); + datum.rels.spouses = [spouse_id]; + const spouse = getD(spouse_id); + const spouse_datum = { + id: spouse.id, + data: JSON.parse(JSON.stringify(spouse.data)), + kinship: kinships[spouse.id], + rels: { + spouses: [datum.id], + children: [], + parents: [] + } + }; + kinship_data_stash.push(spouse_datum); + in_law_datum.rels.parents.forEach(p_id => { + const parent = getD(p_id); + const kinship_label = parent.data.gender === 'M' ? 'Father-in-law' : parent.data.gender === 'F' ? 'Mother-in-law' : 'Parent-in-law'; + const parent_datum = { + id: parent.id, + data: JSON.parse(JSON.stringify(parent.data)), + kinship: kinship_label, + rels: { + spouses: [], + children: [spouse_id, in_law_id], + parents: [] + } + }; + const p2_id = in_law_datum.rels.parents.find(p_id => p_id !== p_id); + if (p2_id) + parent_datum.rels.parents.push(p2_id); + kinship_data_stash.unshift(parent_datum); + }); + } + function getD(d_id) { + return data_stash.find(d => d.id === d_id); + } + } + + function handleLinkRel(updated_datum, link_rel_id, store_data) { + const new_rel_id = updated_datum.id; + store_data.forEach(d => { + if (d.rels.parents.includes(new_rel_id)) { + d.rels.parents[d.rels.parents.indexOf(new_rel_id)] = link_rel_id; + } + if (d.rels.spouses && d.rels.spouses.includes(new_rel_id)) { + d.rels.spouses = d.rels.spouses.filter(id => id !== new_rel_id); + if (!d.rels.spouses.includes(link_rel_id)) + d.rels.spouses.push(link_rel_id); + } + if (d.rels.children && d.rels.children.includes(new_rel_id)) { + d.rels.children = d.rels.children.filter(id => id !== new_rel_id); + if (!d.rels.children.includes(link_rel_id)) + d.rels.children.push(link_rel_id); + } + }); + const link_rel = store_data.find(d => d.id === link_rel_id); + const new_rel = store_data.find(d => d.id === new_rel_id); + if (!new_rel) + throw new Error('New rel not found'); + if (!link_rel) + throw new Error('Link rel not found'); + (new_rel.rels.children || []).forEach(child_id => { + if (!link_rel.rels.children) + link_rel.rels.children = []; + if (!link_rel.rels.children.includes(child_id)) + link_rel.rels.children.push(child_id); + }); + (new_rel.rels.spouses || []).forEach(spouse_id => { + if (!link_rel.rels.spouses) + link_rel.rels.spouses = []; + if (!link_rel.rels.spouses.includes(spouse_id)) + link_rel.rels.spouses.push(spouse_id); + }); + if (link_rel.rels.parents.length === 0) { + link_rel.rels.parents = [...new_rel.rels.parents]; + } + else { + const link_rel_father = link_rel.rels.parents.find(id => { var _a; return ((_a = store_data.find(d => d.id === id)) === null || _a === void 0 ? void 0 : _a.data.gender) === "M"; }); + const link_rel_mother = link_rel.rels.parents.find(id => { var _a; return ((_a = store_data.find(d => d.id === id)) === null || _a === void 0 ? void 0 : _a.data.gender) === "F"; }); + const new_rel_father = new_rel.rels.parents.find(id => { var _a; return ((_a = store_data.find(d => d.id === id)) === null || _a === void 0 ? void 0 : _a.data.gender) === "M"; }); + const new_rel_mother = new_rel.rels.parents.find(id => { var _a; return ((_a = store_data.find(d => d.id === id)) === null || _a === void 0 ? void 0 : _a.data.gender) === "F"; }); + if (new_rel_father) { + if (link_rel_father) { + console.error('link rel already has father'); + link_rel.rels.parents[link_rel.rels.parents.indexOf(link_rel_father)] = new_rel_father; + } + else + link_rel.rels.parents.push(new_rel_father); + } + if (new_rel_mother) { + if (link_rel_mother) { + console.error('link rel already has mother'); + link_rel.rels.parents[link_rel.rels.parents.indexOf(link_rel_mother)] = new_rel_mother; + } + else + link_rel.rels.parents.push(new_rel_mother); + } + } + store_data.splice(store_data.findIndex(d => d.id === new_rel_id), 1); + } + function getLinkRelOptions(datum, data) { + const rel_datum = datum._new_rel_data ? data.find(d => d.id === datum._new_rel_data.rel_id) : null; + const ancestry_ids = getAncestry(datum, data); + const progeny_ids = getProgeny(datum, data); + if (datum._new_rel_data && ['son', 'daughter'].includes(datum._new_rel_data.rel_type)) { + if (!rel_datum) + throw new Error('Rel datum not found'); + progeny_ids.push(...getProgeny(rel_datum, data)); + } + return data.filter(d => d.id !== datum.id && d.id !== (rel_datum === null || rel_datum === void 0 ? void 0 : rel_datum.id) && !d._new_rel_data && !d.to_add && !d.unknown) + .filter(d => !ancestry_ids.includes(d.id)) + .filter(d => !progeny_ids.includes(d.id)) + .filter(d => !(d.rels.spouses || []).includes(datum.id)); + function getAncestry(datum, data_stash) { + const ancestry_ids = []; + loopCheck(datum); + return ancestry_ids; + function loopCheck(d) { + d.rels.parents.forEach(p_id => { + if (p_id) { + ancestry_ids.push(p_id); + const parent = data_stash.find(d => d.id === p_id); + if (!parent) + throw new Error('Parent not found'); + loopCheck(parent); + } + }); + } + } + function getProgeny(datum, data_stash) { + const progeny_ids = []; + loopCheck(datum); + return progeny_ids; + function loopCheck(d) { + const children = d.rels.children ? [...d.rels.children] : []; + children.forEach(c_id => { + progeny_ids.push(c_id); + const child = data_stash.find(d => d.id === c_id); + if (!child) + throw new Error('Child not found'); + loopCheck(child); + }); + } + } + } + + function formCreatorSetup({ datum, store, fields, postSubmitHandler, addRelative, removeRelative, deletePerson, onCancel, editFirst, link_existing_rel_config, onFormCreation, no_edit, onSubmit, onDelete, canEdit, canDelete, }) { + let can_delete = canDelete ? canDelete(datum) : true; + const can_edit = canEdit ? canEdit(datum) : true; + if (!can_edit) { + no_edit = true; + can_delete = false; + } + let form_creator; + const base_form_creator = { + datum_id: datum.id, + fields: [], + onSubmit: submitFormChanges, + onCancel: onCancel, + onFormCreation: onFormCreation, + no_edit: no_edit, + gender_field: getGenderField(), + }; + // Existing datum form creator + if (!datum._new_rel_data) { + if (!addRelative) + throw new Error('addRelative is required'); + if (!removeRelative) + throw new Error('removeRelative is required'); + form_creator = Object.assign(Object.assign({}, base_form_creator), { onDelete: deletePersonWithPostSubmit, addRelative: () => addRelative.activate(datum), addRelativeCancel: () => addRelative.onCancel(), addRelativeActive: addRelative.is_active, removeRelative: () => removeRelative.activate(datum), removeRelativeCancel: () => removeRelative.onCancel(), removeRelativeActive: removeRelative.is_active, editable: false, can_delete: can_delete }); + } + // New rel form creator + else { + form_creator = Object.assign(Object.assign({}, base_form_creator), { title: datum._new_rel_data.label, new_rel: true, editable: true }); + } + if (datum._new_rel_data || datum.to_add || datum.unknown) { + if (link_existing_rel_config) + form_creator.linkExistingRelative = createLinkExistingRelative(datum, store.getData(), link_existing_rel_config); + } + if (no_edit) + form_creator.editable = false; + else if (editFirst) + form_creator.editable = true; + fields.forEach(field => { + if (field.type === 'rel_reference') + addRelReferenceField(field); + else if (field.type === 'select') + addSelectField(field); + else + form_creator.fields.push({ + id: field.id, + type: field.type, + label: field.label, + initial_value: datum.data[field.id], + }); + }); + return form_creator; + function getGenderField() { + return { + id: 'gender', + type: 'switch', + label: 'Gender', + initial_value: datum.data.gender, + disabled: false, + options: [{ value: 'M', label: 'Male' }, { value: 'F', label: 'Female' }] + }; + } + function addRelReferenceField(field) { + if (!field.getRelLabel) + console.error('getRelLabel is not set'); + if (field.rel_type === 'spouse') { + (datum.rels.spouses || []).forEach(spouse_id => { + const spouse = store.getDatum(spouse_id); + if (!spouse) + throw new Error('Spouse not found'); + const marriage_date_id = `${field.id}__ref__${spouse_id}`; + const rel_reference_field = { + id: marriage_date_id, + type: 'rel_reference', + label: field.label, + rel_id: spouse_id, + rel_label: field.getRelLabel(spouse), + initial_value: datum.data[marriage_date_id], + rel_type: field.rel_type, + }; + form_creator.fields.push(rel_reference_field); + }); + } + } + function addSelectField(field) { + if (!field.options && !field.optionCreator) + return console.error('optionCreator or options is not set for field', field); + const select_field = { + id: field.id, + type: field.type, + label: field.label, + initial_value: datum.data[field.id], + placeholder: field.placeholder, + options: field.options || field.optionCreator(datum), + }; + form_creator.fields.push(select_field); + } + function createLinkExistingRelative(datum, data, link_existing_rel_config) { + if (!link_existing_rel_config) + throw new Error('link_existing_rel_config is required'); + const obj = { + title: link_existing_rel_config.title, + select_placeholder: link_existing_rel_config.select_placeholder, + options: getLinkRelOptions(datum, data) + .map((d) => ({ value: d.id, label: link_existing_rel_config.linkRelLabel(d) })) + .sort((a, b) => { + if (typeof a.label === 'string' && typeof b.label === 'string') + return a.label.localeCompare(b.label); + else + return a.label < b.label ? -1 : 1; + }), + onSelect: submitLinkExistingRelative + }; + return obj; + } + function submitFormChanges(e) { + if (onSubmit) { + onSubmit(e, datum, applyChanges, () => postSubmitHandler({})); + } + else { + e.preventDefault(); + applyChanges(); + postSubmitHandler({}); + } + function applyChanges() { + const form_data = new FormData(e.target); + submitFormData(datum, store.getData(), form_data); + } + } + function submitLinkExistingRelative(e) { + const link_rel_id = e.target.value; + postSubmitHandler({ link_rel_id: link_rel_id }); + } + function deletePersonWithPostSubmit() { + if (onDelete) { + onDelete(datum, () => deletePerson(), () => postSubmitHandler({ delete: true })); + } + else { + deletePerson(); + postSubmitHandler({ delete: true }); + } + } + } + + function updateGendersForNewRelatives(updated_datum, data) { + // if gender on main datum is changed, we need to switch mother/father ids for new children + data.forEach(d => { + const rd = d._new_rel_data; + if (!rd) + return; + if (rd.rel_type === 'spouse') + d.data.gender = d.data.gender === 'M' ? 'F' : 'M'; + }); + } + function cleanUp(data) { + for (let i = data.length - 1; i >= 0; i--) { + const d = data[i]; + if (d._new_rel_data) { + data.forEach(d2 => { + if (d2.rels.parents.includes(d.id)) + d2.rels.parents.splice(d2.rels.parents.indexOf(d.id), 1); + if (d2.rels.children && d2.rels.children.includes(d.id)) + d2.rels.children.splice(d2.rels.children.indexOf(d.id), 1); + if (d2.rels.spouses && d2.rels.spouses.includes(d.id)) + d2.rels.spouses.splice(d2.rels.spouses.indexOf(d.id), 1); + }); + data.splice(i, 1); + } + } + } + function addDatumRelsPlaceholders(datum, store_data, addRelLabels, canAdd) { + let can_add = { parent: true, spouse: true, child: true }; + if (canAdd) + can_add = Object.assign(can_add, canAdd(datum)); + if (!datum.rels.spouses) + datum.rels.spouses = []; + if (!datum.rels.children) + datum.rels.children = []; + if (can_add.parent) + addParents(); + if (can_add.spouse) { + addSpouseForSingleParentChildren(); + addSpouse(); + } + if (can_add.child) + addChildren(); + function addParents() { + const parents = datum.rels.parents; + const father = parents.find(d_id => { var _a; return ((_a = store_data.find(d => d.id === d_id)) === null || _a === void 0 ? void 0 : _a.data.gender) === "M"; }); + const mother = parents.find(d_id => { var _a; return ((_a = store_data.find(d => d.id === d_id)) === null || _a === void 0 ? void 0 : _a.data.gender) === "F"; }); + if (parents.length < 2 && !father) { + const father = createNewPerson({ data: { gender: "M" }, rels: { children: [datum.id] } }); + father._new_rel_data = { rel_type: "father", label: addRelLabels.father, rel_id: datum.id }; + datum.rels.parents.push(father.id); + store_data.push(father); + } + if (parents.length < 2 && !mother) { + const mother = createNewPerson({ data: { gender: "F" }, rels: { children: [datum.id] } }); + mother._new_rel_data = { rel_type: "mother", label: addRelLabels.mother, rel_id: datum.id }; + datum.rels.parents.push(mother.id); + store_data.push(mother); + } + const p1 = store_data.find(d => d.id === datum.rels.parents[0]); + const p2 = store_data.find(d => d.id === datum.rels.parents[1]); + if (!p1.rels.spouses) + p1.rels.spouses = []; + if (!p2.rels.spouses) + p2.rels.spouses = []; + if (!p1.rels.spouses.includes(p2.id)) + p1.rels.spouses.push(p2.id); + if (!p2.rels.spouses.includes(p1.id)) + p2.rels.spouses.push(p1.id); + if (!p1.rels.children) + p1.rels.children = []; + if (!p2.rels.children) + p2.rels.children = []; + if (!p1.rels.children.includes(datum.id)) + p1.rels.children.push(datum.id); + if (!p2.rels.children.includes(datum.id)) + p2.rels.children.push(datum.id); + } + function addSpouseForSingleParentChildren() { + if (!datum.rels.spouses) + datum.rels.spouses = []; + if (datum.rels.children) { + let new_spouse; + datum.rels.children.forEach(child_id => { + const child = store_data.find(d => d.id === child_id); + if (child.rels.parents.length === 1) { + const p1 = store_data.find(d => d.id === child.rels.parents[0]); + const new_spouse_gender = p1.data.gender === "M" ? "F" : "M"; + if (!new_spouse) + new_spouse = createNewPerson({ data: { gender: new_spouse_gender }, rels: { spouses: [datum.id] } }); + new_spouse._new_rel_data = { rel_type: "spouse", label: addRelLabels.spouse, rel_id: datum.id }; + new_spouse.rels.children.push(child.id); + datum.rels.spouses.push(new_spouse.id); + child.rels.parents.push(new_spouse.id); + store_data.push(new_spouse); + } + }); + } + } + function addSpouse() { + if (!datum.rels.spouses) + datum.rels.spouses = []; + const spouse_gender = datum.data.gender === "M" ? "F" : "M"; + const new_spouse = createNewPerson({ data: { gender: spouse_gender }, rels: { spouses: [datum.id] } }); + new_spouse._new_rel_data = { rel_type: "spouse", label: addRelLabels.spouse, rel_id: datum.id }; + datum.rels.spouses.push(new_spouse.id); + store_data.push(new_spouse); + } + function addChildren() { + if (!datum.rels.children) + datum.rels.children = []; + if (!datum.rels.spouses) + datum.rels.spouses = []; + datum.rels.spouses.forEach(spouse_id => { + const spouse = store_data.find(d => d.id === spouse_id); + if (!spouse.rels.children) + spouse.rels.children = []; + const new_son = createNewPerson({ data: { gender: "M" }, rels: { parents: [datum.id, spouse.id] } }); + new_son._new_rel_data = { rel_type: "son", label: addRelLabels.son, other_parent_id: spouse.id, rel_id: datum.id }; + spouse.rels.children.push(new_son.id); + datum.rels.children.push(new_son.id); + store_data.push(new_son); + const new_daughter = createNewPerson({ data: { gender: "F" }, rels: { parents: [datum.id, spouse.id] } }); + new_daughter._new_rel_data = { rel_type: "daughter", label: addRelLabels.daughter, other_parent_id: spouse.id, rel_id: datum.id }; + spouse.rels.children.push(new_daughter.id); + datum.rels.children.push(new_daughter.id); + store_data.push(new_daughter); + }); + } + return store_data; + } + + var addRelative = (store, onActivate, cancelCallback) => { return new AddRelative(store, onActivate, cancelCallback); }; + class AddRelative { + constructor(store, onActivate, cancelCallback) { + this.store = store; + this.onActivate = onActivate; + this.cancelCallback = cancelCallback; + this.datum = null; + this.onChange = null; + this.onCancel = null; + this.is_active = false; + this.addRelLabels = this.addRelLabelsDefault(); + return this; + } + activate(datum) { + if (this.is_active) + this.onCancel(); + this.onActivate(); + this.is_active = true; + this.store.state.one_level_rels = true; + const store = this.store; + this.datum = datum; + let gender_stash = this.datum.data.gender; + addDatumRelsPlaceholders(datum, this.getStoreData(), this.addRelLabels, this.canAdd); + store.updateTree({}); + this.onChange = onChange; + this.onCancel = () => onCancel(this); + function onChange(updated_datum, props) { + if (updated_datum === null || updated_datum === void 0 ? void 0 : updated_datum._new_rel_data) { + if (props === null || props === void 0 ? void 0 : props.link_rel_id) + handleLinkRel(updated_datum, props.link_rel_id, store.getData()); + else + delete updated_datum._new_rel_data; + } + else if (updated_datum.id === datum.id) { + if (updated_datum.data.gender !== gender_stash) { + gender_stash = updated_datum.data.gender; + updateGendersForNewRelatives(updated_datum, store.getData()); + } + } + else { + console.error('Something went wrong'); + } + } + function onCancel(self) { + if (!self.is_active) + return; + self.is_active = false; + self.store.state.one_level_rels = false; + self.cleanUp(); + self.cancelCallback(self.datum); + self.datum = null; + self.onChange = null; + self.onCancel = null; + } + } + setAddRelLabels(add_rel_labels) { + if (typeof add_rel_labels !== 'object') { + console.error('add_rel_labels must be an object'); + return; + } + for (const key in add_rel_labels) { + const key_str = key; + this.addRelLabels[key_str] = add_rel_labels[key_str]; + } + return this; + } + setCanAdd(canAdd) { + this.canAdd = canAdd; + return this; + } + addRelLabelsDefault() { + return { + father: 'Add Father', + mother: 'Add Mother', + spouse: 'Add Spouse', + son: 'Add Son', + daughter: 'Add Daughter' + }; + } + getStoreData() { + return this.store.getData(); + } + cleanUp(data) { + if (!data) + data = this.store.getData(); + cleanUp(data); + return data; + } + } + + var removeRelative = (store, onActivate, cancelCallback, modal) => { return new RemoveRelative(store, onActivate, cancelCallback, modal); }; + class RemoveRelative { + constructor(store, onActivate, cancelCallback, modal) { + this.store = store; + this.onActivate = onActivate; + this.cancelCallback = cancelCallback; + this.modal = modal; + this.datum = null; + this.onChange = null; + this.onCancel = null; + this.is_active = false; + return this; + } + activate(datum) { + if (this.is_active) + this.onCancel(); + this.onActivate(); + this.is_active = true; + this.store.state.one_level_rels = true; + const store = this.store; + store.updateTree({}); + this.datum = datum; + this.onChange = onChange.bind(this); + this.onCancel = onCancel.bind(this); + function onChange(rel_tree_datum, onAccept) { + const rel_type = findRelType(rel_tree_datum); + const rels = datum.rels; + if (rel_type === 'parent') + handleParentRemoval.call(this); + else if (rel_type === 'spouse') + handleSpouseRemoval.call(this); + else if (rel_type === 'children') + handleChildrenRemoval.call(this); + function handleParentRemoval() { + const rel_id = rel_tree_datum.data.id; + const parent = store.getDatum(rel_id); + if (!parent) + throw new Error('Parent not found'); + if (!parent.rels.children) + throw new Error('Parent has no children'); + parent.rels.children = parent.rels.children.filter(id => id !== datum.id); + rels.parents = rels.parents.filter(id => id !== rel_id); + onAccept(); + } + function handleSpouseRemoval() { + const spouse = rel_tree_datum.data; + if (checkIfChildrenWithSpouse()) + openModal.call(this); + else + remove.call(this, true); + function checkIfChildrenWithSpouse() { + const children = spouse.rels.children || []; + return children.some(ch_id => { + const child = store.getDatum(ch_id); + if (!child) + throw new Error('Child not found'); + if (child.rels.parents.includes(spouse.id)) + return true; + return false; + }); + } + function openModal() { + const current_gender_class = datum.data.gender === 'M' ? 'f3-male-bg' : datum.data.gender === 'F' ? 'f3-female-bg' : null; + const spouse_gender_class = spouse.data.gender === 'M' ? 'f3-male-bg' : spouse.data.gender === 'F' ? 'f3-female-bg' : null; + const div = d3__namespace.create('div').html(` +

You are removing a spouse relationship. Since there are shared children, please choose which parent should keep them in the family tree.

+
+ + +
+ `); + div.selectAll('[data-option="assign-to-current"]').on('click', () => { + remove(true); + this.modal.close(); + }); + div.selectAll('[data-option="assign-to-spouse"]').on('click', () => { + remove(false); + this.modal.close(); + }); + this.modal.activate(div.node()); + } + function remove(to_current) { + rel_tree_datum.data.rels.spouses = rel_tree_datum.data.rels.spouses.filter(id => id !== datum.id); + rels.spouses = rels.spouses.filter(id => id !== rel_tree_datum.data.id); + const childrens_parent = to_current ? datum : rel_tree_datum.data; + const other_parent = to_current ? rel_tree_datum.data : datum; + (rels.children || []).forEach(id => { + const child = store.getDatum(id); + if (!child) + throw new Error('Child not found'); + if (child.rels.parents.includes(other_parent.id)) + child.rels.parents = child.rels.parents.filter(id => id !== other_parent.id); + }); + if (other_parent.rels.children) { + other_parent.rels.children = other_parent.rels.children.filter(ch_id => !(childrens_parent.rels.children || []).includes(ch_id)); + } + onAccept(); + } + } + function handleChildrenRemoval() { + if (!rels.children) + throw new Error('Children not found'); + rels.children = rels.children.filter(id => id !== rel_tree_datum.data.id); + rel_tree_datum.data.rels.parents = rel_tree_datum.data.rels.parents.filter(id => id !== datum.id); + onAccept(); + } + function findRelType(d) { + if (d.is_ancestry) { + if (datum.rels.parents.includes(d.data.id)) + return 'parent'; + } + else if (d.spouse) { + if (!datum.rels.spouses) + throw new Error('Spouses not found'); + if (datum.rels.spouses.includes(d.data.id)) + return 'spouse'; + } + else { + if (!datum.rels.children) + throw new Error('Children not found'); + if (datum.rels.children.includes(d.data.id)) + return 'children'; + } + return null; + } + } + function onCancel() { + if (!this.is_active) + return; + this.is_active = false; + this.store.state.one_level_rels = false; + if (!this.datum) + throw new Error('Datum not found'); + this.cancelCallback(this.datum); + this.datum = null; + this.onChange = null; + this.onCancel = null; + } + } + } + + function modal (cont) { return new Modal(cont); } + class Modal { + constructor(cont) { + this.cont = cont; + this.active = false; + this.onClose = null; + this.modal_cont = d3__namespace.select(this.cont).append('div').attr('class', 'f3-modal').node(); + d3__namespace.select(this.modal_cont).style('display', 'none'); + this.create(); + } + create() { + const modal = d3__namespace.select(this.modal_cont); + modal.html(` +
+ × +
+
+
+ `); + modal.select('.f3-modal-close').on('click', () => { + this.close(); + }); + modal.on('click', (event) => { + if (event.target == modal.node()) { + this.close(); + } + }); + } + activate(content, { boolean, onAccept, onCancel } = {}) { + this.reset(); + const modal_content_inner = d3__namespace.select(this.modal_cont).select('.f3-modal-content-inner').node(); + if (typeof content === 'string') { + modal_content_inner.innerHTML = content; + } + else { + modal_content_inner.appendChild(content); + } + if (boolean) { + if (!onAccept) + throw new Error('onAccept is required'); + if (!onCancel) + throw new Error('onCancel is required'); + d3__namespace.select(this.modal_cont).select('.f3-modal-content-bottom').html(` + + + `); + d3__namespace.select(this.modal_cont).select('.f3-modal-accept').on('click', () => { onAccept(); this.reset(); this.close(); }); + d3__namespace.select(this.modal_cont).select('.f3-modal-cancel').on('click', () => { this.close(); }); + this.onClose = onCancel; + } + this.open(); + } + reset() { + this.onClose = null; + d3__namespace.select(this.modal_cont).select('.f3-modal-content-inner').html(''); + d3__namespace.select(this.modal_cont).select('.f3-modal-content-bottom').html(''); + } + open() { + this.modal_cont.style.display = 'block'; + this.active = true; + } + close() { + this.modal_cont.style.display = 'none'; + this.active = false; + if (this.onClose) + this.onClose(); + } + } + + var editTree = (cont, store) => new EditTree(cont, store); + /** + * EditTree class - Provides comprehensive editing capabilities for family tree data. + * + * This class handles all editing operations for family tree data, including: + * - Adding new family members and relationships + * - Editing existing person information + * - Removing family members and relationships + * - Form management and validation + * - History tracking and undo/redo functionality + * - Modal dialogs and user interactions + * + * @example + * ```typescript + * import * as f3 from 'family-chart' + * const f3Chart = f3.createChart('#FamilyChart', data) + * const f3EditTree = f3Chart.editTree() // returns an EditTree instance + * .setFields(["first name","last name","birthday"]) + * .setOnChange(() => { + * const updated_data = f3EditTree.exportData() + * // do something with the updated data + * }) + * ``` + */ + class EditTree { + constructor(cont, store) { + this.cont = cont; + this.store = store; + this.fields = [ + { type: 'text', label: 'first name', id: 'first name' }, + { type: 'text', label: 'last name', id: 'last name' }, + { type: 'text', label: 'birthday', id: 'birthday' }, + { type: 'text', label: 'avatar', id: 'avatar' } + ]; + this.is_fixed = true; + this.no_edit = false; + this.onChange = null; + this.editFirst = false; + this.postSubmit = null; + this.onFormCreation = null; + this.createFormEdit = null; + this.createFormNew = null; + this.formCont = this.getFormContDefault(); + this.modal = this.setupModal(); + this.addRelativeInstance = this.setupAddRelative(); + this.removeRelativeInstance = this.setupRemoveRelative(); + this.history = this.createHistory(); + return this; + } + /** + * Open the edit form + * @param datum - The datum to edit + */ + open(datum) { + if (!datum.rels) + datum = datum.data; // if TreeDatum is used, it will be converted to Datum. will be removed in a future version. + if (this.addRelativeInstance.is_active) + handleAddRelative(this); + else if (this.removeRelativeInstance.is_active) + handleRemoveRelative(this, this.store.getTreeDatum(datum.id)); + else { + this.cardEditForm(datum); + } + function handleAddRelative(self) { + if (datum._new_rel_data) { + self.cardEditForm(datum); + } + else { + self.addRelativeInstance.onCancel(); + self.cardEditForm(datum); + self.store.updateMainId(datum.id); + self.store.updateTree({}); + } + } + function handleRemoveRelative(self, tree_datum) { + if (!tree_datum) + throw new Error('Tree datum not found'); + if (!self.removeRelativeInstance.datum) + throw new Error('Remove relative datum not found'); + if (!self.removeRelativeInstance.onCancel) + throw new Error('Remove relative onCancel not found'); + if (!self.removeRelativeInstance.onChange) + throw new Error('Remove relative onChange not found'); + if (datum.id === self.removeRelativeInstance.datum.id) { + self.removeRelativeInstance.onCancel(); + self.cardEditForm(datum); + } + else { + self.removeRelativeInstance.onChange(tree_datum, onAccept.bind(self)); + function onAccept() { + self.removeRelativeInstance.onCancel(); + self.updateHistory(); + self.store.updateTree({}); + } + } + } + } + setupAddRelative() { + return addRelative(this.store, () => onActivate(this), (datum) => cancelCallback(this, datum)); + function onActivate(self) { + if (self.removeRelativeInstance.is_active) + self.removeRelativeInstance.onCancel(); + } + function cancelCallback(self, datum) { + self.store.updateMainId(datum.id); + self.store.updateTree({}); + self.openFormWithId(datum.id); + } + } + setupRemoveRelative() { + return removeRelative(this.store, onActivate.bind(this), cancelCallback.bind(this), this.modal); + function onActivate() { + if (this.addRelativeInstance.is_active) + this.addRelativeInstance.onCancel(); + setClass(this.cont, true); + } + function cancelCallback(datum) { + setClass(this.cont, false); + this.store.updateMainId(datum.id); + this.store.updateTree({}); + this.openFormWithId(datum.id); + } + function setClass(cont, add) { + d3__namespace.select(cont).select('#f3Canvas').classed('f3-remove-relative-active', add); + } + } + createHistory() { + const history = createHistory(this.store, this._getStoreDataCopy.bind(this), historyUpdateTree.bind(this)); + const nav_cont = this.cont.querySelector('.f3-nav-cont'); + if (!nav_cont) + throw new Error("Nav cont not found"); + const controls = createHistoryControls(nav_cont, history); + history.changed(); + controls.updateButtons(); + return Object.assign(Object.assign({}, history), { controls }); + function historyUpdateTree() { + var _a; + console.log('historyUpdateTree'); + if (this.addRelativeInstance.is_active) + this.addRelativeInstance.onCancel(); + if (this.removeRelativeInstance.is_active) + this.removeRelativeInstance.onCancel(); + this.store.updateTree({ initial: false }); + this.history.controls.updateButtons(); + this.openFormWithId((_a = this.store.getMainDatum()) === null || _a === void 0 ? void 0 : _a.id); + if (this.onChange) + this.onChange(); + } + } + /** + * Open the edit form without canceling the add relative or remove relative view + * @param datum - The datum to edit + */ + openWithoutRelCancel(datum) { + this.cardEditForm(datum); + } + getFormContDefault() { + let form_cont = d3__namespace.select(this.cont).select('div.f3-form-cont').node(); + if (!form_cont) + form_cont = d3__namespace.select(this.cont).append('div').classed('f3-form-cont', true).node(); + return { + el: form_cont, + populate(form_element) { + form_cont.innerHTML = ''; + form_cont.appendChild(form_element); + }, + open() { + d3__namespace.select(form_cont).classed('opened', true); + }, + close() { + d3__namespace.select(form_cont).classed('opened', false).html(''); + }, + }; + } + setFormCont(formCont) { + this.formCont = formCont; + return this; + } + cardEditForm(datum) { + const props = {}; + const is_new_rel = datum === null || datum === void 0 ? void 0 : datum._new_rel_data; + if (is_new_rel) { + props.onCancel = () => this.addRelativeInstance.onCancel(); + } + else { + props.addRelative = this.addRelativeInstance; + props.removeRelative = this.removeRelativeInstance; + props.deletePerson = () => { + deletePerson(datum, this.store.getData()); + this.openFormWithId(this.store.getLastAvailableMainDatum().id); + this.store.updateTree({}); + }; + } + const form_creator = formCreatorSetup(Object.assign({ store: this.store, datum, postSubmitHandler: (props) => postSubmitHandler(this, props), fields: this.fields, onCancel: () => { }, editFirst: this.editFirst, no_edit: this.no_edit, link_existing_rel_config: this.link_existing_rel_config, onFormCreation: this.onFormCreation, onSubmit: this.onSubmit, onDelete: this.onDelete, canEdit: this.canEdit, canDelete: this.canDelete }, props)); + const form_cont = is_new_rel + ? (this.createFormNew || createFormNew)(form_creator, this.closeForm.bind(this)) + : (this.createFormEdit || createFormEdit)(form_creator, this.closeForm.bind(this)); + this.formCont.populate(form_cont); + this.openForm(); + function postSubmitHandler(self, props) { + if (self.addRelativeInstance.is_active) { + self.addRelativeInstance.onChange(datum, props); + if (self.postSubmit) + self.postSubmit(datum, self.store.getData()); + const active_datum = self.addRelativeInstance.datum; + if (!active_datum) + throw new Error('Active datum not found'); + self.store.updateMainId(active_datum.id); + self.openWithoutRelCancel(active_datum); + } + else if ((datum.to_add || datum.unknown) && (props === null || props === void 0 ? void 0 : props.link_rel_id)) { + handleLinkRel(datum, props.link_rel_id, self.store.getData()); + self.store.updateMainId(props.link_rel_id); + self.openFormWithId(props.link_rel_id); + } + else if (!(props === null || props === void 0 ? void 0 : props.delete)) { + if (self.postSubmit) + self.postSubmit(datum, self.store.getData()); + self.openFormWithId(datum.id); + } + if (!self.is_fixed) + self.closeForm(); + self.store.updateTree({}); + self.updateHistory(); + } + } + openForm() { + this.formCont.open(); + } + closeForm() { + this.formCont.close(); + this.store.updateTree({}); + } + fixed() { + this.is_fixed = true; + if (this.formCont.el) + d3__namespace.select(this.formCont.el).style('position', 'relative'); + return this; + } + absolute() { + this.is_fixed = false; + if (this.formCont.el) + d3__namespace.select(this.formCont.el).style('position', 'absolute'); + return this; + } + setCardClickOpen(card) { + card.setOnCardClick((e, d) => { + if (this.isAddingRelative()) { + this.open(d.data); + } + else if (this.isRemovingRelative()) { + this.open(d.data); + } + else { + this.open(d.data); + card.onCardClickDefault(e, d); + } + }); + return this; + } + openFormWithId(d_id) { + if (d_id) { + const d = this.store.getDatum(d_id); + if (!d) + throw new Error('Datum not found'); + this.openWithoutRelCancel(d); + } + else { + const d = this.store.getMainDatum(); + if (!d) + throw new Error('Main datum not found'); + this.openWithoutRelCancel(d); + } + } + setNoEdit() { + this.no_edit = true; + return this; + } + setEdit() { + this.no_edit = false; + return this; + } + setFields(fields) { + const new_fields = []; + if (!Array.isArray(fields)) { + console.error('fields must be an array'); + return this; + } + for (const field of fields) { + if (typeof field === 'string') { + new_fields.push({ type: 'text', label: field, id: field }); + } + else if (typeof field === 'object') { + if (!field.id) { + console.error('fields must be an array of objects with id property'); + } + else { + new_fields.push(field); + } + } + else { + console.error('fields must be an array of strings or objects'); + } + } + this.fields = new_fields; + return this; + } + /** + * Set the onChange function to be called when the data changes via editing, adding, or removing a relative + * @param fn - The onChange function + */ + setOnChange(fn) { + this.onChange = fn; + return this; + } + setCanEdit(canEdit) { + this.canEdit = canEdit; + return this; + } + setCanDelete(canDelete) { + this.canDelete = canDelete; + return this; + } + setCanAdd(canAdd) { + this.addRelativeInstance.setCanAdd(canAdd); + return this; + } + addRelative(datum) { + if (!datum) + datum = this.store.getMainDatum(); + this.addRelativeInstance.activate(datum); + return this; + } + setupModal() { + return modal(this.cont); + } + setEditFirst(editFirst) { + this.editFirst = editFirst; + return this; + } + isAddingRelative() { + return this.addRelativeInstance.is_active; + } + isRemovingRelative() { + return this.removeRelativeInstance.is_active; + } + setAddRelLabels(add_rel_labels) { + this.addRelativeInstance.setAddRelLabels(add_rel_labels); + return this; + } + setLinkExistingRelConfig(link_existing_rel_config) { + this.link_existing_rel_config = link_existing_rel_config; + return this; + } + setOnFormCreation(onFormCreation) { + this.onFormCreation = onFormCreation; + return this; + } + setCreateFormEdit(createFormEdit) { + this.createFormEdit = createFormEdit; + return this; + } + setCreateFormNew(createFormNew) { + this.createFormNew = createFormNew; + return this; + } + _getStoreDataCopy() { + let data = JSON.parse(JSON.stringify(this.store.getData())); // important to make a deep copy of the data + if (this.addRelativeInstance.is_active) + data = this.addRelativeInstance.cleanUp(data); + data = cleanupDataJson(data); + return data; + } + /** + * deprecated: use exportData instead. This function will be removed in a future version. + * Export the data + * @returns family chart data + */ + getStoreDataCopy() { + return this.exportData(); + } + /** + * @returns family chart data + */ + exportData() { + let data = this._getStoreDataCopy(); + data = formatDataForExport(data, this.store.state.legacy_format); + return data; + } + getDataJson() { + return JSON.stringify(this.exportData(), null, 2); + } + updateHistory() { + if (this.history) { + this.history.changed(); + this.history.controls.updateButtons(); + } + if (this.onChange) + this.onChange(); + } + setPostSubmit(postSubmit) { + this.postSubmit = postSubmit; + return this; + } + setOnSubmit(onSubmit) { + this.onSubmit = onSubmit; + return this; + } + setOnDelete(onDelete) { + this.onDelete = onDelete; + return this; + } + destroy() { + this.history.controls.destroy(); + this.history = null; + if (this.formCont.el) + d3__namespace.select(this.formCont.el).remove(); + if (this.addRelativeInstance.onCancel) + this.addRelativeInstance.onCancel(); + this.store.updateTree({}); + return this; + } + } + + function linkSpouseText(svg, tree, props) { + const links_data = []; + tree.data.forEach(d => { + if (d.coparent && d.data.data.gender === 'F') + links_data.push({ nodes: [d, d.coparent], id: `${d.data.id}--${d.coparent.data.id}` }); + if (d.spouses) + d.spouses.forEach(sp => links_data.push({ nodes: [sp, d], id: `${sp.data.id}--${d.data.id}` })); + }); + const link = d3__namespace.select(svg) + .select(".links_view") + .selectAll("g.link-text") + .data(links_data, (d) => d.id); + const link_exit = link.exit(); + const link_enter = link.enter().append("g").attr("class", "link-text"); + const link_update = link_enter.merge(link); + const spouseLineX = (sp1, sp2) => { + if (sp1.spouse && sp1.data.data.gender === 'F') + return sp1.x - props.node_separation / 2; + else if (sp2.spouse && sp2.data.data.gender === 'M') + return sp2.x + props.node_separation / 2; + else + return Math.min(sp1.x, sp2.x) + props.node_separation / 2; + }; + link_exit.each(linkExit); + link_enter.each(linkEnter); + link_update.each(linkUpdate); + function linkEnter(d) { + const [sp1, sp2] = d.nodes; + const text_g = d3__namespace.select(this); + text_g + .attr('transform', `translate(${spouseLineX(sp1, sp2)}, ${sp1.y - 3})`) + .style('opacity', 0); + text_g.append("text").style('font-size', '12px').style('fill', '#fff').style('text-anchor', 'middle'); + } + function linkUpdate(d) { + const [sp1, sp2] = d.nodes; + const text_g = d3__namespace.select(this); + const delay = props.initial ? calculateDelay(tree, sp1, props.transition_time) : 0; + text_g.select('text').text(props.linkSpouseText(sp1, sp2)); + text_g.transition('text').duration(props.transition_time).delay(delay) + .attr('transform', `translate(${spouseLineX(sp1, sp2)}, ${sp1.y - 3})`); + text_g.transition('text-op').duration(100).delay(delay + props.transition_time).style('opacity', 1); + } + function linkExit(d) { + const text_g = d3__namespace.select(this); + text_g.transition('text').duration(100).style('opacity', 0) + .on("end", () => text_g.remove()); + } + } + + function autocomplete (cont, onSelect, config = {}) { return new Autocomplete(cont, onSelect, config); } + class Autocomplete { + constructor(cont, onSelect, config = {}) { + this.cont = cont; + this.options = []; + this.onSelect = onSelect; + this.config = config; + this.autocomplete_cont = d3__namespace.select(this.cont).append('div').attr('class', 'f3-autocomplete-cont').node(); + this.create(); + } + create() { + var _a; + const self = this; + d3__namespace.select(this.autocomplete_cont).html(` +
+
+ + ${chevronDownSvgIcon()} +
+
+
+ `); + const search_cont = d3__namespace.select(this.autocomplete_cont).select(".f3-autocomplete"); + const search_input = search_cont.select("input"); + const dropdown = search_cont.select(".f3-autocomplete-items"); + search_cont.on("focusout", () => { + setTimeout(() => { + const search_cont_node = search_cont.node(); + if (!search_cont_node.contains(document.activeElement)) { + closeDropdown(); + } + }, 200); + }); + search_input + .on("focus", () => { + updateOptions(); + activateDropdown(); + }) + .on("input", activateDropdown) + .on("keydown", handleArrowKeys); + dropdown.on("wheel", e => e.stopPropagation()); + search_cont.select(".f3-autocomplete-toggle") + .on("click", (e) => { + e.stopPropagation(); + const is_active = search_cont.classed("active"); + search_cont.classed("active", !is_active); + if (is_active) { + closeDropdown(); + } + else { + const search_input_node = search_input.node(); + search_input_node.focus(); + activateDropdown(); + } + }); + function activateDropdown() { + search_cont.classed("active", true); + const search_input_value = search_input.property("value"); + const filtered_options = self.options.filter(d => d.label.toLowerCase().includes(search_input_value.toLowerCase())); + filtered_options.forEach(setHtmlLabel); + filtered_options.sort(sortByLabel); + updateDropdown(filtered_options); + function setHtmlLabel(d) { + const index = d.label.toLowerCase().indexOf(search_input_value.toLowerCase()); + if (index !== -1) + d.label_html = itemLabel(); + else + d.label_html = d.label; + function itemLabel() { + return d.label.substring(0, index) + + '' + d.label.substring(index, index + search_input_value.length) + + '' + d.label.substring(index + search_input_value.length); + } + } + function sortByLabel(a, b) { + if (a.label < b.label) + return -1; + else if (a.label > b.label) + return 1; + else + return 0; + } + } + function closeDropdown() { + search_cont.classed("active", false); + updateDropdown([]); + } + function updateDropdown(filtered_options) { + dropdown.selectAll("div.f3-autocomplete-item") + .data(filtered_options, d => d === null || d === void 0 ? void 0 : d.value).join("div") + .attr("class", "f3-autocomplete-item") + .on("click", (e, d) => { + self.onSelect(d.value); + }) + .html(d => d.optionHtml ? d.optionHtml(d) : itemHtml(d)); + function itemHtml(d) { + return `
${d.label_html}
`; + } + } + function updateOptions() { + self.options = self.getOptions(); + } + function handleArrowKeys(e) { + const items = dropdown.selectAll("div.f3-autocomplete-item").nodes(); + const currentIndex = items.findIndex(item => d3__namespace.select(item).classed("f3-selected")); + if (e.key === "ArrowDown") { + e.preventDefault(); + const nextIndex = currentIndex < items.length - 1 ? currentIndex + 1 : 0; + selectItem(items, nextIndex); + } + else if (e.key === "ArrowUp") { + e.preventDefault(); + const prevIndex = currentIndex > 0 ? currentIndex - 1 : items.length - 1; + selectItem(items, prevIndex); + } + else if (e.key === "Enter" && currentIndex !== -1) { + e.preventDefault(); + const d = d3__namespace.select(items[currentIndex]).datum(); + if (d) { + self.onSelect(d.value); + } + } + function selectItem(items, index) { + items.forEach(item => d3__namespace.select(item).classed("f3-selected", false)); + if (items[index]) { + d3__namespace.select(items[index]).classed("f3-selected", true); + items[index].scrollIntoView({ block: "nearest" }); + } + } + } + } + setOptionsGetter(getOptions) { + this.getOptions = getOptions; + return this; + } + setOptionsGetterPerson(getData, getLabel) { + this.getOptions = () => { + const options = []; + const data = getData(); + data.forEach(d => { + if (d.to_add || d.unknown || d._new_rel_data) + return; + if (options.find(d0 => d0.value === d.id)) + return; + options.push({ + label: getLabel(d), + value: d.id, + optionHtml: optionHtml(d) + }); + }); + return options; + }; + return this; + function optionHtml(d) { + const link_off = !checkIfConnectedToFirstPerson(d, getData()); + return (option) => (` +
+ ${personSvgIcon()} + ${option.label_html} + ${link_off ? `${linkOffSvgIcon()}` : ''} +
+ `); + } + function getPersonGender(d) { + if (d.data.gender === "M") + return "male"; + else if (d.data.gender === "F") + return "female"; + else + return "genderless"; + } + } + destroy() { + this.autocomplete_cont.remove(); + } + } + + function processCardDisplay(card_display) { + const card_display_arr = []; + if (Array.isArray(card_display)) { + card_display.forEach(d => { + if (typeof d === 'function') { + card_display_arr.push(d); + } + else if (typeof d === 'string') { + card_display_arr.push((d1) => d1.data[d]); + } + else if (Array.isArray(d)) { + card_display_arr.push((d1) => d.map(key => d1.data[key]).join(' ')); + } + }); + } + else if (typeof card_display === 'function') { + card_display_arr.push(card_display); + } + else if (typeof card_display === 'string') { + card_display_arr.push((d1) => d1.data[card_display]); + } + return card_display_arr; + } + + function pathToMain(cards, links, datum, main_datum) { + const is_ancestry = datum.is_ancestry; + const links_data = links.data(); + let links_node_to_main = []; + let cards_node_to_main = []; + if (is_ancestry) { + const links_to_main = []; + let parent = datum; + let itteration1 = 0; + while (parent !== main_datum && itteration1 < 100) { + itteration1++; // to prevent infinite loop + const spouse_link = links_data.find(d => d.spouse === true && (d.source === parent || d.target === parent)); + if (spouse_link) { + const child_links = links_data.filter(d => Array.isArray(d.target) && d.target.includes(spouse_link.source) && d.target.includes(spouse_link.target)); + const child_link = getChildLinkFromAncestrySide(child_links, main_datum); + if (!child_link) + break; + links_to_main.push(spouse_link); + links_to_main.push(child_link); + parent = child_link.source; + } + else { + // single parent + const child_links = links_data.filter(d => Array.isArray(d.target) && d.target.includes(parent)); + const child_link = getChildLinkFromAncestrySide(child_links, main_datum); + if (!child_link) + break; + links_to_main.push(child_link); + parent = child_link.source; + } + } + links.each(function (d) { + if (links_to_main.includes(d)) { + links_node_to_main.push({ link: d, node: this }); + } + }); + const cards_to_main = getCardsToMain(datum, links_to_main); + cards.each(function (d) { + if (cards_to_main.includes(d)) { + cards_node_to_main.push({ card: d, node: this }); + } + }); + } + else if (datum.spouse && datum.spouse.data === main_datum.data) { + links.each(function (d) { + if (d.target === datum) + links_node_to_main.push({ link: d, node: this }); + }); + const cards_to_main = [main_datum, datum]; + cards.each(function (d) { + if (cards_to_main.includes(d)) { + cards_node_to_main.push({ card: d, node: this }); + } + }); + } + else if (datum.sibling) { + links.each(function (d) { + if (!Array.isArray(datum.parents)) + throw new Error('datum.parents is not an array'); + if (d.source === datum) + links_node_to_main.push({ link: d, node: this }); + if (d.source === main_datum && Array.isArray(d.target) && d.target.length === 2) + links_node_to_main.push({ link: d, node: this }); + if (datum.parents.includes(d.source) && !Array.isArray(d.target) && datum.parents.includes(d.target)) + links_node_to_main.push({ link: d, node: this }); + }); + const cards_to_main = [main_datum, datum, ...(datum.parents || [])]; + cards.each(function (d) { + if (cards_to_main.includes(d)) { + cards_node_to_main.push({ card: d, node: this }); + } + }); + } + else { + let links_to_main = []; + let child = datum; + let itteration1 = 0; + while (child !== main_datum && itteration1 < 100) { + itteration1++; // to prevent infinite loop + const child_link = links_data.find(d => d.target === child && Array.isArray(d.source)); + if (child_link) { + const spouse_link = links_data.find(d => d.spouse === true && sameArray([d.source, d.target], child_link.source)); + links_to_main.push(child_link); + links_to_main.push(spouse_link); + if (spouse_link) + child = spouse_link.source; + else + child = child_link.source[0]; + } + else { + const spouse_link = links_data.find(d => d.target === child && !Array.isArray(d.source)); // spouse link + if (!spouse_link) + break; + links_to_main.push(spouse_link); + child = spouse_link.source; + } + } + links.each(function (d) { + if (links_to_main.includes(d)) { + links_node_to_main.push({ link: d, node: this }); + } + }); + const cards_to_main = getCardsToMain(main_datum, links_to_main); + cards.each(function (d) { + if (cards_to_main.includes(d)) { + cards_node_to_main.push({ card: d, node: this }); + } + }); + } + return { cards_node_to_main, links_node_to_main }; + function sameArray(arr1, arr2) { + return arr1.every(d1 => arr2.some(d2 => d1 === d2)); + } + function getCardsToMain(first_parent, links_to_main) { + const all_cards = links_to_main.filter(d => d).reduce((acc, d) => { + if (Array.isArray(d.target)) + acc.push(...d.target); + else + acc.push(d.target); + if (Array.isArray(d.source)) + acc.push(...d.source); + else + acc.push(d.source); + return acc; + }, []); + const cards_to_main = [main_datum, datum]; + getChildren(first_parent); + return cards_to_main; + function getChildren(d) { + if (d.data.rels.children) { + d.data.rels.children.forEach(child_id => { + const child = all_cards.find(d0 => d0.data.id === child_id); + if (child) { + cards_to_main.push(child); + getChildren(child); + } + }); + } + } + } + function getChildLinkFromAncestrySide(child_links, main_datum) { + if (child_links.length === 0) + return null; + else if (child_links.length === 1) + return child_links[0]; + else { + // siblings of main + // should be last level where we go to the main and not its siblings + return child_links.find(d => d.source === main_datum); + } + } + } + + function CardHtmlWrapper(cont, store) { return new CardHtml$1(cont, store); } + /** + * CardHtml class - Handles HTML-based card rendering and customization for family tree nodes. + * + * @example + * ```typescript + * import * as f3 from 'family-chart' + * const f3Chart = f3.createChart('#FamilyChart', data) + * const f3Card = f3Chart.setCardHtml() // returns a CardHtml instance + * .setCardDisplay([["first name","last name"],["birthday"]]); + * ``` + */ + let CardHtml$1 = class CardHtml { + constructor(cont, store) { + this.cont = cont; + this.svg = this.cont.querySelector('svg.main_svg'); + this.store = store; + this.card_display = [(d) => `${d.data["first name"]} ${d.data["last name"]}`]; + this.cardImageField = 'avatar'; + this.onCardClick = this.onCardClickDefault; + this.style = 'default'; + this.mini_tree = false; + this.card_dim = {}; + return this; + } + getCard() { + return CardHtml$2({ + store: this.store, + card_display: this.card_display, + cardImageField: this.cardImageField, + defaultPersonIcon: this.defaultPersonIcon, + onCardClick: this.onCardClick, + style: this.style, + mini_tree: this.mini_tree, + onCardUpdate: this.onCardUpdate, + card_dim: this.card_dim, + empty_card_label: this.store.state.single_parent_empty_card_label || '', + unknown_card_label: this.store.state.unknown_card_label || '', + cardInnerHtmlCreator: this.cardInnerHtmlCreator, + duplicate_branch_toggle: this.store.state.duplicate_branch_toggle, + onCardMouseenter: this.onCardMouseenter ? this.onCardMouseenter.bind(this) : undefined, + onCardMouseleave: this.onCardMouseleave ? this.onCardMouseleave.bind(this) : undefined + }); + } + setCardDisplay(card_display) { + this.card_display = processCardDisplay(card_display); + return this; + } + setCardImageField(cardImageField) { + this.cardImageField = cardImageField; + return this; + } + setDefaultPersonIcon(defaultPersonIcon) { + this.defaultPersonIcon = defaultPersonIcon; + return this; + } + setOnCardClick(onCardClick) { + this.onCardClick = onCardClick; + return this; + } + onCardClickDefault(e, d) { + this.store.updateMainId(d.data.id); + this.store.updateTree({}); + } + setStyle(style) { + this.style = style; + return this; + } + setMiniTree(mini_tree) { + this.mini_tree = mini_tree; + return this; + } + setOnCardUpdate(onCardUpdate) { + this.onCardUpdate = onCardUpdate; + return this; + } + setCardDim(card_dim) { + if (typeof card_dim !== 'object') { + console.error('card_dim must be an object'); + return this; + } + for (let key in card_dim) { + const val = card_dim[key]; + if (typeof val !== 'number' && typeof val !== 'boolean') { + console.error(`card_dim.${key} must be a number or boolean`); + return this; + } + if (key === 'width') + key = 'w'; + if (key === 'height') + key = 'h'; + if (key === 'img_width') + key = 'img_w'; + if (key === 'img_height') + key = 'img_h'; + if (key === 'img_x') + key = 'img_x'; + if (key === 'img_y') + key = 'img_y'; + this.card_dim[key] = val; + } + return this; + } + resetCardDim() { + this.card_dim = {}; + return this; + } + setCardInnerHtmlCreator(cardInnerHtmlCreator) { + this.cardInnerHtmlCreator = cardInnerHtmlCreator; + return this; + } + setOnHoverPathToMain() { + this.onCardMouseenter = this.onEnterPathToMain.bind(this); + this.onCardMouseleave = this.onLeavePathToMain.bind(this); + return this; + } + unsetOnHoverPathToMain() { + this.onCardMouseenter = undefined; + this.onCardMouseleave = undefined; + return this; + } + onEnterPathToMain(e, datum) { + this.to_transition = datum.data.id; + const main_datum = this.store.getTreeMainDatum(); + const cards = d3__namespace.select(this.cont).select('div.cards_view').selectAll('.card_cont'); + const links = d3__namespace.select(this.cont).select('svg.main_svg .links_view').selectAll('.link'); + const { cards_node_to_main, links_node_to_main } = pathToMain(cards, links, datum, main_datum); + cards_node_to_main.forEach(d => { + const delay = Math.abs(datum.depth - d.card.depth) * 200; + d3__namespace.select(d.node.querySelector('div.card-inner')) + .transition().duration(0).delay(delay) + .on('end', () => this.to_transition === datum.data.id && d3__namespace.select(d.node.querySelector('div.card-inner')).classed('f3-path-to-main', true)); + }); + links_node_to_main.forEach(d => { + const delay = Math.abs(datum.depth - d.link.depth) * 200; + d3__namespace.select(d.node) + .transition().duration(0).delay(delay) + .on('end', () => this.to_transition === datum.data.id && d3__namespace.select(d.node).classed('f3-path-to-main', true)); + }); + return this; + } + onLeavePathToMain(e, d) { + this.to_transition = false; + d3__namespace.select(this.cont).select('div.cards_view').selectAll('div.card-inner').classed('f3-path-to-main', false); + d3__namespace.select(this.cont).select('svg.main_svg .links_view').selectAll('.link').classed('f3-path-to-main', false); + return this; + } + }; + + function CardSvgWrapper(cont, store) { return new CardSvg$1(cont, store); } + let CardSvg$1 = class CardSvg { + constructor(cont, store) { + this.cont = cont; + this.store = store; + this.svg = this.cont.querySelector('svg.main_svg'); + this.card_dim = { w: 220, h: 70, text_x: 75, text_y: 15, img_w: 60, img_h: 60, img_x: 5, img_y: 5 }; + this.card_display = []; + this.mini_tree = true; + this.link_break = false; + this.onCardClick = this.onCardClickDefault.bind(this); + return this; + } + getCard() { + return CardSvg$2({ + store: this.store, + svg: this.svg, + card_dim: this.card_dim, + card_display: this.card_display, + mini_tree: this.mini_tree, + link_break: this.link_break, + onCardClick: this.onCardClick, + onCardUpdate: this.onCardUpdate + }); + } + setCardDisplay(card_display) { + this.card_display = processCardDisplay(card_display); + return this; + } + setCardDim(card_dim) { + if (typeof card_dim !== 'object') { + console.error('card_dim must be an object'); + return this; + } + for (let key in card_dim) { + const val = card_dim[key]; + if (typeof val !== 'number' && typeof val !== 'boolean') { + console.error(`card_dim.${key} must be a number or boolean`); + return this; + } + if (key === 'width') + key = 'w'; + if (key === 'height') + key = 'h'; + if (key === 'img_width') + key = 'img_w'; + if (key === 'img_height') + key = 'img_h'; + if (key === 'img_x') + key = 'img_x'; + if (key === 'img_y') + key = 'img_y'; + this.card_dim[key] = val; + } + updateCardSvgDefs(this.svg, this.card_dim); + return this; + } + setOnCardUpdate(onCardUpdate) { + this.onCardUpdate = onCardUpdate; + return this; + } + setMiniTree(mini_tree) { + this.mini_tree = mini_tree; + return this; + } + setLinkBreak(link_break) { + this.link_break = link_break; + return this; + } + onCardClickDefault(e, d) { + this.store.updateMainId(d.data.id); + this.store.updateTree({}); + } + setOnCardClick(onCardClick) { + this.onCardClick = onCardClick; + return this; + } + }; + + function createChart(cont, data) { + return new Chart(cont, data); + } + /** + * Main Chart class - The primary class for creating and managing family tree visualizations. + * + * This is the main entry point for the Family Chart library. Use this class to: + * - Create and configure family tree visualizations + * - Set up data, styling, and interaction options + * - Control tree layout, orientation, and display settings + * - Manage user interactions and updates + * + * @example + * ```typescript + * const f3Chart = createChart('#FamilyChart', data) // returns a Chart instance; + * ``` + */ + class Chart { + constructor(cont, data) { + this.getCard = null; + this.transition_time = 2000; + this.linkSpouseText = null; + this.personSearch = null; + this.is_card_html = false; + this.beforeUpdate = null; + this.afterUpdate = null; + this.cont = setCont(cont); + const { svg } = htmlContSetup(this.cont); + this.svg = svg; + createNavCont(this.cont); + const main_id = data && data.length > 0 ? data[0].id : ''; + this.store = this.createStore(data, main_id); + this.setOnUpdate(); + this.editTreeInstance = null; + return this; + } + createStore(data, main_id) { + return createStore({ + data, + main_id, + node_separation: 250, + level_separation: 150, + single_parent_empty_card: true, + is_horizontal: false, + }); + } + setOnUpdate() { + this.store.setOnUpdate((props) => { + if (this.beforeUpdate) + this.beforeUpdate(props); + props = Object.assign({ transition_time: this.store.state.transition_time }, props || {}); + if (this.is_card_html) + props = Object.assign({}, props || {}, { cardHtml: true }); + view(this.store.getTree(), this.svg, this.getCard(), props || {}); + if (this.linkSpouseText) + linkSpouseText(this.svg, this.store.getTree(), Object.assign({}, props || {}, { linkSpouseText: this.linkSpouseText, node_separation: this.store.state.node_separation })); + if (this.afterUpdate) + this.afterUpdate(props); + }); + } + /** + * Update the tree + * @param props - The properties to update the tree with. + * @param props.initial - Whether to update the tree initially. + * @param props.tree_position - The position of the tree. + * - 'fit' to fit the tree to the container, + * - 'main_to_middle' to center the tree on the main person, + * - 'inherit' to inherit the position from the previous update. + * @param props.transition_time - The transition time. + * @returns The CreateChart instance + */ + updateTree(props = { initial: false }) { + this.store.updateTree(props); + return this; + } + /** + * Update the data + * @param data - The data to update the tree with. + * @returns The CreateChart instance + */ + updateData(data) { + this.store.updateData(data); + return this; + } + /** + * Set the card y spacing + * @param card_y_spacing - The card y spacing between the cards. Level separation. + * @returns The CreateChart instance + */ + setCardYSpacing(card_y_spacing) { + if (typeof card_y_spacing !== 'number') { + console.error('card_y_spacing must be a number'); + return this; + } + this.store.state.level_separation = card_y_spacing; + return this; + } + /** + * Set the card x spacing + * @param card_x_spacing - The card x spacing between the cards. Node separation. + * @returns The CreateChart instance + */ + setCardXSpacing(card_x_spacing) { + if (typeof card_x_spacing !== 'number') { + console.error('card_x_spacing must be a number'); + return this; + } + this.store.state.node_separation = card_x_spacing; + return this; + } + /** + * Set the orientation to vertical + * @returns The CreateChart instance + */ + setOrientationVertical() { + this.store.state.is_horizontal = false; + return this; + } + /** + * Set the orientation to horizontal + * @returns The CreateChart instance + */ + setOrientationHorizontal() { + this.store.state.is_horizontal = true; + return this; + } + /** + * Set whether to show the siblings of the main person + * @param show_siblings_of_main - Whether to show the siblings of the main person. + * @returns The CreateChart instance + */ + setShowSiblingsOfMain(show_siblings_of_main) { + this.store.state.show_siblings_of_main = show_siblings_of_main; + return this; + } + /** + * set function that will modify the tree hierarchy. it can be used to delete or add cards in the tree. + * @param modifyTreeHierarchy - function that will modify the tree hierarchy. + * @returns The CreateChart instance + */ + setModifyTreeHierarchy(modifyTreeHierarchy) { + this.store.state.modifyTreeHierarchy = modifyTreeHierarchy; + return this; + } + /** + * Set the private cards config + * @param private_cards_config - The private cards config. + * @param private_cards_config.condition - The condition to check if the card is private. + * - Example: (d: Datum) => d.data.living === true + * @returns The CreateChart instance + */ + setPrivateCardsConfig(private_cards_config) { + this.store.state.private_cards_config = private_cards_config; + return this; + } + /** + * Option to set text on spouse links + * @param linkSpouseText - The function to set the text on the spouse links. + * - Example: (sp1, sp2) => getMarriageDate(sp1, sp2) + * @returns The CreateChart instance + */ + setLinkSpouseText(linkSpouseText) { + this.linkSpouseText = linkSpouseText; + return this; + } + /** + * Set whether to show the single parent empty card + * @param single_parent_empty_card - Whether to show the single parent empty card. + * @param label - The label to display for the single parent empty card. + * @returns The CreateChart instance + */ + setSingleParentEmptyCard(single_parent_empty_card, { label = 'Unknown' } = {}) { + this.store.state.single_parent_empty_card = single_parent_empty_card; + this.store.state.single_parent_empty_card_label = label; + if (this.editTreeInstance && this.editTreeInstance.addRelativeInstance.is_active) + this.editTreeInstance.addRelativeInstance.onCancel(); + removeToAddFromData(this.store.getData() || []); + return this; + } + /** + * Set the Card creation function + * @param Card - The card function. + * @returns The CreateChart instance + */ + setCard(card) { + if (card === CardHtmlWrapper) + return this.setCardHtml(); + else if (card === CardSvgWrapper) + return this.setCardSvg(); + else + throw new Error('Card must be an instance of cardHtml or cardSvg'); + } + /** + * Set the Card HTML function + * @returns The CardHtml instance + */ + setCardHtml() { + const htmlSvg = this.cont.querySelector('#htmlSvg'); + if (!htmlSvg) + throw new Error('htmlSvg not found'); + this.is_card_html = true; + this.svg.querySelector('.cards_view').innerHTML = ''; + htmlSvg.style.display = 'block'; + const card = CardHtmlWrapper(this.cont, this.store); + this.getCard = () => card.getCard(); + return card; + } + /** + * Set the Card SVG function + * @returns The CardSvg instance + */ + setCardSvg() { + const htmlSvg = this.cont.querySelector('#htmlSvg'); + if (!htmlSvg) + throw new Error('htmlSvg not found'); + this.is_card_html = false; + this.svg.querySelector('.cards_view').innerHTML = ''; + htmlSvg.style.display = 'none'; + const card = CardSvgWrapper(this.cont, this.store); + this.getCard = () => card.getCard(); + return card; + } + /** + * Set the transition time + * @param transition_time - The transition time in milliseconds + * @returns The CreateChart instance + */ + setTransitionTime(transition_time) { + this.store.state.transition_time = transition_time; + return this; + } + /** + * Set the sort children function + * @param sortChildrenFunction - The sort children function. + * - Example: (a, b) => a.data.birth_date - b.data.birth_date + * @returns The CreateChart instance + */ + setSortChildrenFunction(sortChildrenFunction) { + this.store.state.sortChildrenFunction = sortChildrenFunction; + return this; + } + /** + * Set the sort spouses function + * @param sortSpousesFunction - The sort spouses function. + * - Example: + * (d, data) => { + * const spouses = d.data.rels.spouses || [] + * return spouses.sort((a, b) => { + * const sp1 = data.find(d0 => d0.id === a) + * const sp2 = data.find(d0 => d0.id === b) + * if (!sp1 || !sp2) return 0 + * return getMarriageDate(d, sp1) - getMarriageDate(d, sp2) + * }) + * }) + * } + * @returns The CreateChart instance + */ + setSortSpousesFunction(sortSpousesFunction) { + this.store.state.sortSpousesFunction = sortSpousesFunction; + return this; + } + /** + * Set how many generations to show in the ancestry + * @param ancestry_depth - The number of generations to show in the ancestry. + * @returns The CreateChart instance + */ + setAncestryDepth(ancestry_depth) { + this.store.state.ancestry_depth = ancestry_depth; + return this; + } + /** + * Set how many generations to show in the progeny + * @param progeny_depth - The number of generations to show in the progeny. + * @returns The CreateChart instance + */ + setProgenyDepth(progeny_depth) { + this.store.state.progeny_depth = progeny_depth; + return this; + } + /** + * Get the max depth of a person in the ancestry and progeny + * @param d_id - The id of the person to get the max depth of. + * @returns The max depth of the person in the ancestry and progeny. {ancestry: number, progeny: number} + */ + getMaxDepth(d_id) { + return getMaxDepth(d_id, this.store.getData()); + } + /** + * Calculate the kinships of a person + * @param d_id - The id of the person to calculate the kinships of. + * @param config - The config for the kinships. + * @param config.show_in_law - Whether to show in law relations. + * @returns The kinships of the person. + */ + calculateKinships(d_id, config = {}) { + return calculateKinships(d_id, this.store.getData(), config); + } + /** + * Get the kinships data stash with which we can create small family tree with relatives that connects 2 people + * @param main_id - The id of the main person. + * @param rel_id - The id of the person to get the kinships of. + * @returns The kinships data stash. + */ + getKinshipsDataStash(main_id, rel_id) { + return getKinshipsDataStash(main_id, rel_id, this.store.getData(), this.calculateKinships(main_id)); + } + /** + * Set whether to show toggable tree branches are duplicated + * @param duplicate_branch_toggle - Whether to show toggable tree branches are duplicated. + * @returns The CreateChart instance + */ + setDuplicateBranchToggle(duplicate_branch_toggle) { + this.store.state.duplicate_branch_toggle = duplicate_branch_toggle; + return this; + } + /** + * Initialize the edit tree + * @returns The edit tree instance. + */ + editTree() { + return this.editTreeInstance = editTree(this.cont, this.store); + } + /** + * Update the main person + * @param d - New main person. + * @returns The CreateChart instance + */ + updateMain(d) { + let d_id; + if (d.id) + d_id = d.id; + else + d_id = d.data.id; + this.store.updateMainId(d_id); + this.store.updateTree({}); + return this; + } + /** + * Update the main person + * @param id - New main person id. + * @returns The CreateChart instance + */ + updateMainId(id) { + this.store.updateMainId(id); + return this; + } + /** + * Get the main person + * @returns The main person. + */ + getMainDatum() { + return this.store.getMainDatum(); + } + /** + * Set the before update of the tree. + * @param fn - The function to call before the update. + * @returns The CreateChart instance + */ + setBeforeUpdate(fn) { + this.beforeUpdate = fn; + return this; + } + /** + * Set the after update of the tree. + * @param fn - The function to call after the update. + * @returns The CreateChart instance + */ + setAfterUpdate(fn) { + this.afterUpdate = fn; + return this; + } + /** + * Set the person dropdown + * @param getLabel - The function to get the label of the person to show in the dropdown. + * @param config - The config for the person dropdown. + * @param config.cont - The container to put the dropdown in. Default is the .f3-nav-cont element. + * @param config.onSelect - The function to call when a person is selected. Default is setting clicked person as main person and updating the tree. + * @param config.placeholder - The placeholder for the search input. Default is 'Search'. + * @returns The CreateChart instance + */ + setPersonDropdown(getLabel, { cont = this.cont.querySelector('.f3-nav-cont'), onSelect, placeholder = 'Search' } = {}) { + if (!onSelect) + onSelect = onSelectDefault.bind(this); + this.personSearch = autocomplete(cont, onSelect, { placeholder }); + this.personSearch.setOptionsGetterPerson(this.store.getData, getLabel); + function onSelectDefault(d_id) { + const datum = this.store.getDatum(d_id); + if (!datum) + throw new Error('Datum not found'); + if (this.editTreeInstance) + this.editTreeInstance.open(datum); + this.updateMainId(d_id); + this.updateTree({ initial: false }); + } + return this; + } + /** + * Unset the person dropdown + * @returns The CreateChart instance + */ + unSetPersonSearch() { + this.personSearch.destroy(); + this.personSearch = null; + return this; + } + } + function setCont(cont) { + if (typeof cont === "string") + cont = document.querySelector(cont); + if (!cont) + throw new Error('cont not found'); + return cont; + } + function createNavCont(cont) { + d3__namespace.select(cont).append('div').attr('class', 'f3-nav-cont'); + } + + function kinshipInfo(kinship_info_config, rel_id, data_stash) { + const { self_id, getLabel, title } = kinship_info_config; + const relationships = calculateKinships(self_id, data_stash, kinship_info_config); + const relationship = relationships[rel_id]; + if (!relationship) + return; + let label = relationship; + if (relationship === 'self') + label = 'You'; + else + label = capitalizeLabel(label); + const html = (` +
+
+ ${title} + + ${label} + ${infoSvgIcon()} + +
+
+ `); + const kinship_info_node = d3__namespace.create('div').html(html).select('div').node(); + let popup = null; + d3__namespace.select(kinship_info_node).select('.f3-kinship-info-icon').on('click', (e) => createPopup(e, kinship_info_node)); + return kinship_info_node; + function createPopup(e, cont) { + const width = 250; + const height = 400; + let left = e.clientX - width - 10; + let top = e.clientY - height - 10; + if (left + width > window.innerWidth) { + left = window.innerWidth - width - 10; + } + if (top < 0) { + top = 10; + } + if (popup && popup.active) { + popup.close(); + popup = null; + return; + } + popup = createInfoPopup(cont); + d3__namespace.select(popup.popup_cont) + .style('width', `${width}px`) + .style('height', `${height}px`) + .style('left', `${left}px`) + .style('top', `${top}px`); + const inner_cont = popup.popup_cont.querySelector('.f3-popup-content-inner'); + popup.activate(); + createSmallTree(self_id, rel_id, data_stash, relationships, inner_cont, getLabel); + } + } + function createSmallTree(self_id, rel_id, data_stash, relationships, parent_cont, getLabel) { + if (!d3__namespace.select(parent_cont).select('#SmallChart').node()) { + d3__namespace.select(parent_cont).append('div').attr('id', 'SmallChart').attr('class', 'f3'); + } + const small_chart = d3__namespace.select('#SmallChart'); + small_chart.selectAll('*').remove(); + const small_chart_data = getKinshipsDataStash(self_id, rel_id, data_stash, relationships); + let kinship_label_toggle = true; + const kinship_label_toggle_cont = small_chart.append('div'); + create(small_chart_data); + function create(data) { + const f3Chart = createChart('#SmallChart', data) + .setTransitionTime(500) + .setCardXSpacing(170) + .setCardYSpacing(70) + .setSingleParentEmptyCard(false); + const f3Card = f3Chart.setCardHtml() + .setStyle('rect') + .setCardInnerHtmlCreator((d) => { + return getCardInnerRect(d); + }) + .setOnCardUpdate(function (d) { + const card = d3__namespace.select(this).select('.card'); + card.classed('card-main', false); + }); + f3Card.onCardClick = ((e, d) => { }); + f3Chart.updateTree({ initial: true }); + setTimeout(() => setupSameZoom(0.65), 100); + createKinshipLabelToggle(); + function getCardInnerRect(d) { + let label = d.data.kinship === 'self' ? 'You' : d.data.kinship; + label = capitalizeLabel(label); + if (!kinship_label_toggle) + label = getLabel(d.data); + return (` +
+
${label}
+
+ `); + function getCardClass() { + if (d.data.kinship === 'self') { + return 'card-kinship-self' + (kinship_label_toggle ? '' : ' f3-real-label'); + } + else if (d.data.id === rel_id) { + return 'card-kinship-rel'; + } + else { + return 'card-kinship-default'; + } + } + } + function createKinshipLabelToggle() { + kinship_label_toggle_cont + .classed('f3-kinship-labels-toggle', true); + kinship_label_toggle_cont.append('label') + .text('Kinship labels') + .append('input') + .attr('type', 'checkbox') + .attr('checked', true) + .on('change', (e) => { + kinship_label_toggle = !kinship_label_toggle; + f3Chart.updateTree({ initial: false, tree_position: 'inherit' }); + }); + } + function setupSameZoom(zoom_level) { + const svg = f3Chart.cont.querySelector('svg.main_svg'); + const current_zoom = getCurrentZoom(svg); + if (current_zoom.k > zoom_level) { + zoomTo(svg, zoom_level); + } + } + } + } + function capitalizeLabel(label) { + label = label[0].toUpperCase() + label.slice(1); + if (label.includes('great-')) + label = label.replace('great-', 'Great-'); + return label; + } + + var elements = /*#__PURE__*/Object.freeze({ + __proto__: null, + Card: Card, + CardHtml: CardHtml$2, + CardSvg: CardSvg$2, + appendElement: appendElement, + infoPopup: createInfoPopup, + kinshipInfo: kinshipInfo + }); + + // export { default as calculateTree } from "./layout/calculate-tree" // handled in deprecated section + /** @deprecated Use cardSvg instead. This export will be removed in a future version. */ + const CardSvg = CardSvgWrapper; + /** @deprecated Use cardHtml instead. This export will be removed in a future version. */ + const CardHtml = CardHtmlWrapper; + const htmlHandlersWithDeprecated = Object.assign({}, htmlHandlers, { setupHtmlSvg, setupReactiveTreeData: _setupReactiveTreeData, getUniqueId }); + + var exports$1 = /*#__PURE__*/Object.freeze({ + __proto__: null, + CalculateTree: CalculateTree, + Card: Card, + CardHtml: CardHtml, + CardHtmlClass: CardHtml$1, + CardSvg: CardSvg, + CardSvgClass: CardSvg$1, + calculateTree: calculateTreeWithV1Data, + cardHtml: CardHtmlWrapper, + cardSvg: CardSvgWrapper, + createChart: createChart, + createStore: createStore, + createSvg: createSvg, + elements: elements, + formatData: formatData, + formatDataForExport: formatDataForExport, + handlers: handlers, + htmlHandlers: htmlHandlersWithDeprecated, + icons: icons, + view: view + }); + + exports.CalculateTree = CalculateTree; + exports.Card = Card; + exports.CardHtml = CardHtml; + exports.CardHtmlClass = CardHtml$1; + exports.CardSvg = CardSvg; + exports.CardSvgClass = CardSvg$1; + exports.calculateTree = calculateTreeWithV1Data; + exports.cardHtml = CardHtmlWrapper; + exports.cardSvg = CardSvgWrapper; + exports.createChart = createChart; + exports.createStore = createStore; + exports.createSvg = createSvg; + exports.default = exports$1; + exports.elements = elements; + exports.formatData = formatData; + exports.formatDataForExport = formatDataForExport; + exports.handlers = handlers; + exports.htmlHandlers = htmlHandlersWithDeprecated; + exports.icons = icons; + exports.view = view; + + Object.defineProperty(exports, '__esModule', { value: true }); + +})); diff --git a/dist/family-chart.min.js b/dist/family-chart.min.js new file mode 100644 index 00000000..2e3b2566 --- /dev/null +++ b/dist/family-chart.min.js @@ -0,0 +1,2 @@ +// https://donatso.github.io/family-chart/ v0.9.0 Copyright 2025 donatso +!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?t(exports,require("d3")):"function"==typeof define&&define.amd?define(["exports","d3"],t):t((e="undefined"!=typeof globalThis?globalThis:e||self).f3={},e.d3)}(this,function(e,t){"use strict";function n(e){var t=Object.create(null);return e&&Object.keys(e).forEach(function(n){if("default"!==n){var i=Object.getOwnPropertyDescriptor(e,n);Object.defineProperty(t,n,i.get?i:{enumerable:!0,get:function(){return e[n]}})}}),t.default=e,Object.freeze(t)}var i=n(t);function r(e,t,n){return n.find(n=>n.id!==t.id&&e.rels.parents.includes(n.id))}function s(e,t,n){if(e.exiting=n,t)if(0!==e.depth||e.spouse)if(e.spouse)e._x=e.spouse.x,e._y=e.spouse.y;else if(e.is_ancestry){if(!e.parent)throw new Error("no parent");e._x=e.parent.x,e._y=e.parent.y}else e._x=e.psx,e._y=e.psy;else e._x=e.x,e._y=e.y;else if(n){const t=e.x>0?1:-1,n=e.y>0?1:-1;e._x=e.x+400*t,e._y=e.y+400*n}}function a({tree:e,data_stash:t,private_cards_config:n}){const i={},r=n.condition;if(!r)return console.error("private_cards_config.condition is not set");e.forEach(e=>{if(e.data._new_rel_data)return;const n=function(e){const n=[];let s=!1;return a(e),i[e]=s,s;function a(e){if(s)return;if(i.hasOwnProperty(e))return s=i[e],s;const o=t.find(t=>t.id===e);if(!o)throw new Error("no d");if(o._new_rel_data)return;if(r(o))return s=!0,!0;const d=o.rels;[...d.parents,...d.spouses||[]].forEach(e=>{e&&(n.includes(e)||(n.push(e),a(e)))})}}(e.data.id);n&&(e.is_private=n)})}function o(e,t){const n=t.find(t=>t.id===e);if(!n)throw new Error("no datum");const r=i.hierarchy(n,e=>function(e){return e.rels.parents.filter(e=>e).map(e=>t.find(t=>t.id===e)).filter(e=>e&&!e._new_rel_data&&!e.to_add)}(e)),s=i.hierarchy(n,e=>function(e){return[...e.rels.children||[]].map(e=>t.find(t=>t.id===e)).filter(e=>e&&!e._new_rel_data&&!e.to_add)}(e));return{ancestry:r.height,progeny:s.height}}function d({data:e,rels:t}){return{id:(n=(new Date).getTime(),i=performance&&performance.now&&1e3*performance.now()||0,"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,function(e){var t=16*Math.random();return n>0?(t=(n+t)%16|0,n=Math.floor(n/16)):(t=(i+t)%16|0,i=Math.floor(i/16)),("x"===e?t:3&t|8).toString(16)})),data:e||{},rels:Object.assign({parents:[],children:[],spouses:[]},t||{})};var n,i}function l(e,t){const n=e.data.rels;return[...n.parents,...n.spouses||[],...n.children||[]].filter(e=>e).every(e=>t.some(t=>t.data.id===e))}function c(e,t,n){const i=.4*n,r=Math.max(...e.data.map(e=>e.is_ancestry?e.depth:0));let s=t.depth*i;return 0===t.depth&&!t.spouse||t.is_ancestry||(s+=r*i,t.spouse&&(s+=i),s+=t.depth*i),s}function u(e,t,n=!0){const i=[];function r(e,t){return e.every(e=>t.some(t=>e.id===t.id))}function s(e){const t={},n=e;return(e.children||[]).forEach(e=>{const i=e.data.rels,r=i.parents[0]===n.data.id?i.parents[1]:i.parents[0];t[r]||(t[r]=[]),t[r].push(e)}),t}!function a(o){if(!o.children)return;const d=o.data,l=(o.data.rels.spouses||[]).map(e=>t.find(t=>t.id===e)),c=s(o);l.forEach(l=>{if(i.some(e=>e.some(e=>r([d,l],[e.p1,e.p2]))))return;const u=function(n,i,a){const o=[];return d(e),o;function d(e){if(e!==n&&e.children){const n=e.data,l=(e.data.rels.spouses||[]).map(e=>t.find(t=>t.id===e)),c=s(e);l.forEach(t=>{r([i,a],[n,t])?o.push({d:e,p1:n,p2:t}):(c[t.id]||[]).forEach(e=>{d(e)})})}}}(o,d,l);if(u.length>0){const t=[{d:o,p1:d,p2:l},...u];i.push(t),function(t){if(t.forEach(({d:n,p1:i,p2:r},s)=>{n.data._tgdp_sp||(n.data._tgdp_sp={});let a=e===n?"main":n.parent.data.id;!function(e,t,n){e.data.__tgdp_sp&&e.data.__tgdp_sp[t]&&e.data.__tgdp_sp[t].hasOwnProperty(n.id)&&(e.data._tgdp_sp[t][n.id]=e.data.__tgdp_sp[t][n.id],delete e.data.__tgdp_sp[t][n.id])}(n,a,r),n.data._tgdp_sp[a]||(n.data._tgdp_sp[a]={});let o=1;n.data._tgdp_sp[a].hasOwnProperty(r.id)?o=n.data._tgdp_sp[a][r.id]:n.data._tgdp_sp[a][r.id]=o,t[s].val=o}),n){if(t.every(e=>e.val<0)){const n=t.sort((e,t)=>t.val-e.val)[0],{d:i,p1:r,p2:s}=n,a=e===i?"main":i.parent.data.id;i.data._tgdp_sp[a][s.id]=1}if(t.filter(e=>e.val>0).length>1){const n=t.sort((e,t)=>t.val-e.val)[0];t.forEach(t=>{if(t===n)return;const{d:i,p1:r,p2:s}=t,a=e===i?"main":i.parent.data.id;i.data._tgdp_sp[a][s.id]=-1})}}}(t),function(t){t.forEach(({d:t,p1:n,p2:i})=>{const r=e===t?"main":t.parent.data.id;if(t.data._tgdp_sp[r][i.id]<0){const e=s(t);e[i.id]&&(t.children=t.children.filter(t=>!e[i.id].includes(t)),0===t.children.length&&delete t.children)}})}(t)}else{let t=e===o?"main":o.parent.data.id;!function(e,t,n){e.data._tgdp_sp&&e.data._tgdp_sp[t]&&e.data._tgdp_sp[t].hasOwnProperty(n.id)&&(e.data.__tgdp_sp||(e.data.__tgdp_sp={}),e.data.__tgdp_sp[t]||(e.data.__tgdp_sp[t]={}),e.data.__tgdp_sp[t][n.id]=e.data._tgdp_sp[t][n.id],delete e.data._tgdp_sp[t][n.id])}(o,t,l),(c[l.id]||[]).forEach(e=>{a(e)})}})}(e),function(e){let t=0;e.forEach(e=>{t+=1,e.forEach(e=>{e.d._toggle_id_sp||(e.d._toggle_id_sp={}),e.d._toggle_id_sp[e.p2.id]=t})})}(i)}function h(e,t=!0){const n=[];!function i(r){if(r.children){if(n.some(e=>e.includes(r)))return;const s=function(t){const n=[];return i(e),n;function i(e){var r,s;e.children&&(r=t,s=e.children,r!==s&&r.every(e=>s.some(t=>e.data.id===t.data.id))?n.push(e):e.children.forEach(e=>{i(e)}))}}(r.children);if(s.length>0){const i=[r,...s];n.push(i),function(n){if(n.forEach(t=>{t.data._tgdp||(t.data._tgdp={});const n=e===t?"main":t.parent.data.id;t.data._tgdp[n]||(t.data._tgdp[n]=-1),t._toggle=t.data._tgdp[n]}),t){if(n.every(e=>e._toggle<0)){const t=n.sort((e,t)=>t._toggle-e._toggle)[0],i=e===t?"main":t.parent.data.id;t.data._tgdp[i]=1}if(n.filter(e=>e._toggle>0).length>1){const t=n.sort((e,t)=>t._toggle-e._toggle)[0];n.forEach(n=>{if(n===t)return;const i=n,r=e===i?"main":i.parent.data.id;i.data._tgdp[r]=-1})}}}(i),function(t){t.forEach(t=>{const n=e===t?"main":t.parent.data.id;t.data._tgdp[n]<0&&delete t.children})}(i)}else r.children.forEach(e=>{i(e)})}}(e),function(e){let t=0;e.forEach(e=>{t+=1,e.forEach(e=>{e._toggle_id=t})})}(n)}function p(e){return e.forEach(e=>{e.rels.parents||(e.rels.parents=[]),e.rels.spouses||(e.rels.spouses=[]),e.rels.children||(e.rels.children=[]),function(e){e.rels.parents||(e.rels.parents=[]);e.rels.father&&e.rels.parents.push(e.rels.father);e.rels.mother&&e.rels.parents.push(e.rels.mother);delete e.rels.father,delete e.rels.mother}(e)}),e}function f(e,t=!1){return e.forEach(n=>{var i;if(t){let t,r;null===(i=n.rels.parents)||void 0===i||i.forEach(n=>{const i=e.find(e=>e.id===n);if(!i)throw new Error("Parent not found");"M"===i.data.gender&&(t?r=i.id:t=i.id),"F"===i.data.gender&&(r?t=i.id:r=i.id)}),t&&(n.rels.father=t),r&&(n.rels.mother=r),delete n.rels.parents}n.rels.parents&&0===n.rels.parents.length&&delete n.rels.parents,n.rels.spouses&&0===n.rels.spouses.length&&delete n.rels.spouses,n.rels.children&&0===n.rels.children.length&&delete n.rels.children}),e}function _(e,{main_id:t=null,node_separation:n=250,level_separation:s=150,single_parent_empty_card:o=!0,is_horizontal:c=!1,one_level_rels:p=!1,sortChildrenFunction:f,sortSpousesFunction:_,ancestry_depth:g,progeny_depth:m,show_siblings_of_main:v=!1,modifyTreeHierarchy:y,private_cards_config:w,duplicate_branch_toggle:b=!1,on_toggle_one_close_others:C=!0}){if(!e||!e.length)throw new Error("No data");c&&([n,s]=[s,n]);const x=o?function(e){const t=[];for(let t=0;t0){let t;i.rels.spouses||(i.rels.spouses=[]),i.rels.children.forEach(r=>{const s=e.find(e=>e.id===r);if(2!==s.rels.parents.length){if(t||(t=n(i)),t.rels.children||(t.rels.children=[]),t.rels.children.push(s.id),1!==s.rels.parents.length)throw new Error("child has more than 1 parent");s.rels.parents.push(t.id)}})}}return t.forEach(t=>e.push(t)),e;function n(n){return(n.rels.spouses||[]).map(t=>e.find(e=>e.id===t)).filter(e=>void 0!==e).find(e=>e.to_add)||function(e){const n=d({data:{gender:"M"===e.data.gender?"F":"M"},rels:{spouses:[e.id]}});n.to_add=!0,t.push(n),e.rels.spouses||(e.rels.spouses=[]);return e.rels.spouses.push(n.id),n}(n)}}(e):e;t&&x.find(e=>e.id===t)||(t=x[0].id);const $=x.find(e=>e.id===t);if(!$)throw new Error("Main not found");const k=R($,"children",!1),E=R($,"parents",!0);x.forEach(e=>e.main=e===$),function(e,t){const n=(e[0].x-t[0].x)/2;e.forEach(e=>e.x-=n),t.forEach(e=>e.x+=n)}(E,k);const S=(I=k,(M=E).forEach(e=>{e.is_ancestry=!0}),M.forEach(e=>1===e.depth?e.parent=I[0]:null),[...I,...M.slice(1)]);var M,I;!function(e){e.forEach(t=>{if(delete t.children,e.forEach(e=>{e.parent===t&&(e.is_ancestry?(t.parents||(t.parents=[]),t.parents.push(e)):(t.children||(t.children=[]),t.children.push(e)))}),t.parents&&2===t.parents.length){const e=t.parents[0],n=t.parents[1];e.coparent=n,n.coparent=e}})}(S),function(e,t){for(let n=e.length;n--;){const i=e[n];if(!i.is_ancestry){let n=i.data.rels.spouses||[];if(i._ignore_spouses&&(n=n.filter(e=>!i._ignore_spouses.includes(e))),n.length>0){if(p&&i.depth>0)continue;const r="M"===i.data.data.gender?-1:1;i.x+=n.length/2*t*r,n.forEach((n,s)=>{const a={data:x.find(e=>e.id===n),added:!0,depth:i.depth,spouse:i,x:i.x-t*(s+1)*r,y:i.y,tid:`${i.data.id}-spouse-${s}`};a.sx=s>0?a.x:a.x+t/2*r,a.sy=s>0?a.y:a.y+t/2*r,i.spouses||(i.spouses=[]),i.spouses.push(a),e.push(a)})}}if(i.parents&&2===i.parents.length){const e=i.parents[0],n=i.parents[1],r=e.x-(e.x-n.x)/2,s=(e,n)=>r+t/2*(e.xe.data.main);if(!s)throw new Error("no main");const a=s.data.rels.parents[0],o=s.data.rels.parents[1],d=function(e){return t.filter(t=>!!(t.id!==e.data.id&&(a&&t.rels.parents.includes(a)||o&&t.rels.parents.includes(o))))}(s);if(d.length>0&&!s.parents){if(!s.data.parents)throw new Error("no parents");s.parents=s.data.parents}const l=function(t){const n=[];for(let i=0;ie.data.id===r.data.rels.parents[0]),a=t.parents.find(e=>e.data.id===r.data.rels.parents[1]);s&&r.parents.push(s),a&&r.parents.push(a),e.push(r),n.push(r)}return n}(s);!function(e){var t,s;const a=[e,...l];r&&a.sort((e,t)=>r(e.data,t.data)),a.sort((t,n)=>{const i=e.parents.find(e=>e.data.id===t.data.rels.parents[0]),r=e.parents.find(e=>e.data.id===t.data.rels.parents[1]),s=e.parents.find(e=>e.data.id===n.data.rels.parents[0]),a=e.parents.find(e=>e.data.id===n.data.rels.parents[1]);return!r&&a?-1:r&&!a||!i&&s?1:i&&!s?-1:0});const o=e.x,d=(e.spouses||[]).map(e=>e.x),c=i.extent([o,...d]),u=a.findIndex(t=>t.data.id===e.data.id);for(let e=0;e{if(e.is_ancestry)return;if(0===e.depth)return;if(e.added)return;if(e.sibling)return;const t=e.parent,n=((null==t?void 0:t.spouses)||[]).find(t=>e.data.rels.parents.includes(t.data.id));if(t&&n){t.added||n.added||console.error("no added spouse",t,n);const r=t.added?t:n;i(e,r)}else if(t||n){const r=t||n;if(!r)throw new Error("no progeny parent");r.sx=r.x,r.sy=r.y,i(e,r)}function i(e,t){e.psx=c?t.y:t.sx,e.psy=c?t.sx:t.y}})}(S),function(e){e.forEach(e=>{if(e.y*=e.is_ancestry?-1:1,c){const t=e.x;e.x=e.y,e.y=t}})}(S),S.forEach(e=>e.all_rels_displayed=l(e,S)),w&&a({tree:S,data_stash:x,private_cards_config:w}),function(e){const t=[];e.forEach(n=>{if(t.includes(n.data.id)){const i=e.filter(e=>e.data.id===n.data.id);i.forEach((e,r)=>{e.tid=`${n.data.id}--x${r+1}`,e.duplicate=i.length,t.push(n.data.id)})}else n.tid=n.data.id,t.push(n.data.id)})}(S),b&&function(e){e.forEach(e=>{if(!e.spouse)return;const t=e.spouse;if(e.duplicate&&t.data._tgdp_sp){const n=t.data.main?"main":t.parent.data.id;t.data._tgdp_sp[n]?.hasOwnProperty(e.data.id)&&(e._toggle=t.data._tgdp_sp[n][e.data.id])}})}(S);const A=function(e,t,n){c&&([t,n]=[n,t]);const r=i.extent(e,e=>e.x),s=i.extent(e,e=>e.y);if(void 0===r[0]||void 0===r[1]||void 0===s[0]||void 0===s[1])throw new Error("No extent");return{width:r[1]-r[0]+t,height:s[1]-s[0]+n,x_off:-r[0]+t/2,y_off:-s[0]+n/2}}(S,n,s);return{data:S,data_stash:x,dim:A,main_id:$.id,is_horizontal:c};function R(e,t,a){const o="children"===t?function(e){const t=[...e.rels.children||[]].map(e=>x.find(t=>t.id===e)).filter(e=>void 0!==e);f&&t.sort(f);(function(e){e.sort((e,t)=>{const n=e._new_rel_data,i=t._new_rel_data;return n&&!i?1:!n&&i?-1:0})})(t),_&&_(e,x);return function(e,t,n){if(!t.rels.children)return;const i=t.rels.spouses||[];e.sort((e,s)=>{const a=r(e,t,n),o=r(s,t,n),d=a?i.indexOf(a.id):-1,l=o?i.indexOf(o.id):-1;return"M"===t.data.gender?d-l:l-d})}(t,e,x),t}:function(e){let t=[...e.rels.parents];const n=x.find(e=>e.id===t[0]);n&&"F"===n.data.gender&&t.reverse();return t.filter(e=>e).map(e=>x.find(t=>t.id===e)).filter(e=>void 0!==e)},d=i.tree().nodeSize([n,s]).separation(function(e,t){let n=1;a||(v(e,t)||(n+=.25),p||function(e,t){return w(e)||w(t)}(e,t)&&(n+=function(e,t){return.5*((e.data.rels.spouses||[]).length+(t.data.rels.spouses||[]).length)}(e,t)),v(e,t)&&!function(e,t){const n=[...e.data.rels.parents].sort(),i=[...t.data.rels.parents].sort();return n.length===i.length&&n.every((e,t)=>e===i[t])}(e,t)&&(n+=.125));return n}),l=i.hierarchy(e,o);!function(e,t){let n=t?g:m;p&&(n=1);return n||0===n?(i(e,0),e):e;function i(e,t){t===n?e.children&&delete e.children:e.children&&e.children.forEach(e=>{i(e,t+1)})}}(l,a),b&&function(e,t,n){n?h(e,C):u(e,t,C)}(l,x,a),y&&y(l,a),d(l);const c=l.descendants();return c.forEach(e=>{void 0===e.x&&(e.x=0),void 0===e.y&&(e.y=0)}),c;function v(e,t){return e.parent==t.parent}function w(e){return e.data.rels.spouses&&e.data.rels.spouses.length>0}}}function g(e){return m(e.data,e)}function m(e,t){return _(p(e),t)}function v(e){let t;const n=Object.assign({transition_time:1e3},e);n.main_id_history=[],n.data&&(s(n.data),p(n.data));return{state:n,updateTree:e=>{n.data&&0!==n.data.length&&(n.tree=function(){const e={main_id:n.main_id};void 0!==n.node_separation&&(e.node_separation=n.node_separation);void 0!==n.level_separation&&(e.level_separation=n.level_separation);void 0!==n.single_parent_empty_card&&(e.single_parent_empty_card=n.single_parent_empty_card);void 0!==n.is_horizontal&&(e.is_horizontal=n.is_horizontal);void 0!==n.one_level_rels&&(e.one_level_rels=n.one_level_rels);void 0!==n.modifyTreeHierarchy&&(e.modifyTreeHierarchy=n.modifyTreeHierarchy);void 0!==n.sortChildrenFunction&&(e.sortChildrenFunction=n.sortChildrenFunction);void 0!==n.sortSpousesFunction&&(e.sortSpousesFunction=n.sortSpousesFunction);void 0!==n.ancestry_depth&&(e.ancestry_depth=n.ancestry_depth);void 0!==n.progeny_depth&&(e.progeny_depth=n.progeny_depth);void 0!==n.show_siblings_of_main&&(e.show_siblings_of_main=n.show_siblings_of_main);void 0!==n.private_cards_config&&(e.private_cards_config=n.private_cards_config);void 0!==n.duplicate_branch_toggle&&(e.duplicate_branch_toggle=n.duplicate_branch_toggle);return _(n.data,e)}(),!n.main_id&&n.tree&&r(n.tree.main_id),t&&t(e))},updateData:e=>{s(e),p(e),n.data=e,function(){if(n.main_id){!n.data.find(e=>e.id===n.main_id)&&n.data.length>0&&r(n.data[0].id)}else n.data.length>0&&r(n.data[0].id)}()},updateMainId:r,getMainId:()=>n.main_id,getData:()=>n.data,getTree:()=>n.tree,setOnUpdate:e=>t=e,getMainDatum:function(){const e=n.data.find(e=>e.id===n.main_id);if(!e)throw new Error("Main datum not found");return e},getDatum:i,getTreeMainDatum:function(){if(!n.tree)throw new Error("No tree");const e=n.tree.data.find(e=>e.data.id===n.main_id);if(!e)throw new Error("No tree main datum");return e},getTreeDatum:function(e){if(!n.tree)throw new Error("No tree");const t=n.tree.data.find(t=>t.data.id===e);return t||void 0},getLastAvailableMainDatum:function(){let e=n.main_id_history.slice(0).reverse().find(e=>i(e));!e&&n.data.length>0&&(e=n.data[0].id);if(!e)throw new Error("No main id");e!==n.main_id&&r(e);const t=i(e);if(!t)throw new Error("Main datum not found");return t},methods:{}};function i(e){const t=n.data.find(t=>t.id===e);if(t)return t}function r(e){e!==n.main_id&&(n.main_id_history=n.main_id_history.filter(t=>t!==e).slice(-10),n.main_id_history.push(e),n.main_id=e)}function s(e){if(void 0===n.legacy_format){for(let t of e)if(t.rels.father||t.rels.mother)return void(n.legacy_format=!0);n.legacy_format=!1}}}function y({t:e,svg:t,transition_time:n=2e3}){const r=E(t),s=r.__zoomObj;i.select(r).transition().duration(n||0).delay(n?100:0).call(s.transform,i.zoomIdentity.scale(e.k).translate(e.x,e.y))}function w({svg:e,svg_dim:t,tree_dim:n,transition_time:i}){y({t:b(t,n),svg:e,transition_time:i})}function b(e,t){let n=Math.min(e.width/t.width,e.height/t.height);n>1&&(n=1);return{k:n,x:t.x_off+(e.width-t.width*n)/n/2,y:t.y_off+(e.height-t.height*n)/n/2}}function C({datum:e,svg:t,svg_dim:n,scale:i,transition_time:r}){const s=i||1;y({t:{k:s,x:(n.width/2-e.x*s)/s,y:(n.height/2-e.y*s)/s},svg:t,transition_time:r})}function x({amount:e,svg:t,transition_time:n=500}){const r=E(t),s=r.__zoomObj;if(!s)throw new Error("Zoom object not found");i.select(r).transition().duration(n||0).delay(n?100:0).call(s.scaleBy,e)}function $(e){const t=E(e);return i.zoomTransform(t)}function k(e,t){const n=E(e);x({amount:t/i.zoomTransform(n).k,svg:e})}function E(e){const t=e.__zoomObj?e:e.parentNode;if(!t.__zoomObj)throw new Error("Zoom object not found");return t}function S(e,t={}){if(e.__zoom)return void console.log("zoom already setup");const n=e.querySelector(".view"),r=i.zoom().on("zoom",t.onZoom||function(e){i.select(n).attr("transform",e.transform)});i.select(e).call(r),e.__zoomObj=r,t.zoom_polite&&r.filter(function(e){return!("wheel"===e.type&&!e.ctrlKey)&&!(e.touches&&e.touches.length<2)})}function M(e,t,n={}){const r=t.data.reduce((e,n)=>(function(e,t=!1){const n=[];return(e.spouses||e.coparent)&&function(e){function t(e,t){return{d:[[e.x,e.y],[t.x,t.y]],_d:()=>[e.is_ancestry?[r(e,"x")-1e-4,r(e,"y")]:[e.x,e.y],e.is_ancestry?[r(t,"x"),r(t,"y")]:[e.x-1e-4,e.y]],curve:!1,id:a(e,t),depth:e.depth,spouse:!0,is_ancestry:t.is_ancestry,source:e,target:t}}e.spouses?e.spouses.forEach(i=>n.push(t(e,i))):e.coparent&&n.push(t(e,e.coparent))}(e),function(e){if(!e.parents)return;const t=e.parents[0],r=e.parents[1]||t,o={x:i(t,r,"x"),y:i(t,r,"y")};n.push({d:s(e,o),_d:()=>s({x:e.x,y:e.y},{x:e.x,y:e.y}),curve:!0,id:a(e,t,r),depth:e.depth+1,is_ancestry:!0,source:e,target:[t,r]})}(e),function(e){e.children&&0!==e.children.length&&e.children.forEach((i,o)=>{const d=function(e,t){const n=(t.spouses||[]).find(t=>e.data.rels.parents.includes(t.data.id));return n}(i,e)||e,l=d.sx;if("number"!=typeof l)throw new Error("sx is not a number");const c=t?{x:e.x,y:l}:{x:l,y:e.y};n.push({d:s(i,c),_d:()=>s(c,{x:r(c,"x"),y:r(c,"y")}),curve:!0,id:a(i,e,d),depth:e.depth+1,is_ancestry:!1,source:[e,d],target:i})})}(e),n;function i(e,t,n,i=!1){return i?r(e,n)-(r(e,n)-r(t,n))/2:e[n]-(e[n]-t[n])/2}function r(e,t){const n=e.hasOwnProperty(`_${t}`)?e[`_${t}`]:e[t];if("number"!=typeof n)throw new Error(`${t} is not a number`);return n}function s(e,n){return t?function(e,t){const n=e.x+(t.x-e.x)/2;return[[e.x,e.y],[n,e.y],[n,e.y],[n,t.y],[n,t.y],[t.x,t.y]]}(e,n):function(e,t){const n=e.y+(t.y-e.y)/2;return[[e.x,e.y],[e.x,n],[e.x,n],[t.x,n],[t.x,n],[t.x,t.y]]}(e,n)}function a(...e){return e.map(e=>e.tid).sort().join(", ")}}(n,t.is_horizontal).forEach(t=>e[t.id]=t),e),{}),s=Object.values(r),a=i.select(e).select(".links_view").selectAll("path.link").data(s,e=>e.id);if(void 0===n.transition_time)throw new Error("transition_time is undefined");const o=a.exit(),d=a.enter().append("path").attr("class","link"),l=d.merge(a);o.each(function(e){const t=i.select(this);t.transition("op").duration(800).style("opacity",0),t.transition("path").duration(n.transition_time).attr("d",I(e,!0)).on("end",()=>t.remove())}),d.each(function(e){i.select(this).attr("fill","none").attr("stroke","#fff").attr("stroke-width",1).style("opacity",0).attr("d",I(e,!0))}),l.each(function(e){const r=i.select(this),s=n.initial?c(t,e,n.transition_time):0;r.transition("path").duration(n.transition_time).delay(s).attr("d",I(e)).style("opacity",1)})}function I(e,t=!1){const n=i.line().curve(i.curveMonotoneY),r=i.line().curve(i.curveBasis),s=t?e._d():e.d;return e.curve?r(s):n(s)}function A(e,t={}){const n=e.getBoundingClientRect(),r=`\n \n \n \n \n \n \n \n \n \n \n `,s=function(e){let t=e.querySelector("#f3Canvas");t||(t=i.create("div").attr("id","f3Canvas").attr("style","position: relative; overflow: hidden; width: 100%; height: 100%;").node());return t}(e),a=i.create("div").node();a.innerHTML=r;const o=a.querySelector("svg");return s.appendChild(o),e.appendChild(s),S(s,t),o}function R(e){return A(e,{onZoom:D(()=>e.querySelector("svg .view"),()=>e.querySelector("#htmlSvg .cards_view"))}),H(e),{svg:e.querySelector("svg.main_svg"),svgView:e.querySelector("svg .view"),htmlSvg:e.querySelector("#htmlSvg"),htmlView:e.querySelector("#htmlSvg .cards_view")}}function H(e){const t=i.select(e).select("#f3Canvas").append("div").attr("id","htmlSvg").attr("style","position: absolute; width: 100%; height: 100%; z-index: 2; top: 0; left: 0");return t.append("div").attr("class","cards_view").style("transform-origin","0 0"),t.node()}function D(e,t){return function(n){const r=n.transform;i.select(e()).style("transform",`translate(${r.x}px, ${r.y}px) scale(${r.k}) `),i.select(t()).style("transform",`translate(${r.x}px, ${r.y}px) scale(${r.k}) `)}}var F=Object.freeze({__proto__:null,createHtmlSvg:H,default:R,onZoomSetup:D});function O(e){let t=[];return function(n){const r=function(e,t){return t.length>0?t.filter(t=>!e.find(e=>e.data.id===t.data.id)):[]}(n,t);return t=[...n,...r],function(e,t){const n=i.select(e).selectAll("div.card_cont_2fake").data(t,e=>e.data.id),r=n.exit(),s=n.enter().append("div").attr("class","card_cont_2fake").style("display","none").attr("data-id",()=>Math.random()),a=s.merge(n);function o(e){e.unique_id=i.select(this).attr("data-id")}function d(e){e.unique_id=i.select(this).attr("data-id")}function l(e){e&&(e.unique_id=i.select(this).attr("data-id"),i.select(this).remove())}r.each(l),s.each(o),a.each(d)}(L(e),t),t}}function L(e){return i.select(e()).select("div.cards_view_fake").node()}const T=O;function P(e,t,n,r={}){r.initial=r.hasOwnProperty("initial")?r.initial:!i.select(t.parentNode).select(".card_cont").node(),r.transition_time=r.hasOwnProperty("transition_time")?r.transition_time:1e3,r.cardComponent?function(e,t,n,r={}){const a=r.cardHtmlDiv?r.cardHtmlDiv:e.closest("#f3Canvas").querySelector("#htmlSvg"),o=i.select(L(()=>a)).selectAll("div.card_cont_fake").data(t.data,e=>e.data.id),d=o.exit(),l=o.enter().append("div").attr("class","card_cont_fake").style("display","none"),u=l.merge(o);d.each(e=>s(e,!1,!0)),l.each(e=>s(e,!0,!1)),d.each(function(e){const t=e,s=t?[t._x,t._y]:[0,0],a=i.select(n(e)),o=i.select(this);a.transition().duration(r.transition_time).style("opacity",0).style("transform",`translate(${s[0]}px, ${s[1]}px)`).on("end",()=>o.remove())}),o.each(function(e){}),l.each(function(e){i.select(n(e)).style("position","absolute").style("top","0").style("left","0").style("opacity",0).style("transform",`translate(${e._x}px, ${e._y}px)`)}),u.each(function(e){const s=i.select(n(e)),a=r.initial?c(t,e,r.transition_time):0;s.transition().duration(r.transition_time).delay(a).style("transform",`translate(${e.x}px, ${e.y}px)`).style("opacity",1)})}(t,e,n,r):r.cardHtml?function(e,t,n,r={}){const a=function(e){if(r.cardHtmlDiv)return r.cardHtmlDiv;const t=e.closest("#f3Canvas");if(!t)throw new Error("canvas not found");const n=t.querySelector("#htmlSvg");if(!n)throw new Error("htmlSvg not found");return n}(e),o=i.select(a).select(".cards_view").selectAll("div.card_cont").data(t.data,e=>e.tid),d=o.exit(),l=o.enter().append("div").attr("class","card_cont").style("pointer-events","none"),u=l.merge(o);d.each(e=>s(e,!1,!0)),l.each(e=>s(e,!0,!1)),d.each(function(e){const t=e,n=t?[t._x,t._y]:[0,0],s=i.select(this);s.transition().duration(r.transition_time).style("opacity",0).style("transform",`translate(${n[0]}px, ${n[1]}px)`).on("end",()=>s.remove())}),o.each(function(e){}),l.each(function(e){i.select(this).style("position","absolute").style("top","0").style("left","0").style("transform",`translate(${e._x}px, ${e._y}px)`).style("opacity",0),n.call(this,e)}),u.each(function(e){n.call(this,e);const s=r.initial?c(t,e,r.transition_time):0;i.select(this).transition().duration(r.transition_time).delay(s).style("transform",`translate(${e.x}px, ${e.y}px)`).style("opacity",1)})}(t,e,n,r):function(e,t,n,r={}){const a=i.select(e).select(".cards_view").selectAll("g.card_cont").data(t.data,e=>e.data.id),o=a.exit(),d=a.enter().append("g").attr("class","card_cont"),l=d.merge(a);o.each(e=>s(e,!1,!0)),d.each(e=>s(e,!0,!1)),o.each(function(e){const t=e,n=t?[t._x,t._y]:[0,0],s=i.select(this);s.transition().duration(r.transition_time).style("opacity",0).attr("transform",`translate(${n[0]}, ${n[1]})`).on("end",()=>s.remove())}),a.each(function(e){}),d.each(function(e){i.select(this).attr("transform",`translate(${e._x}, ${e._y})`).style("opacity",0),n.call(this,e)}),l.each(function(e){n.call(this,e);const s=r.initial?c(t,e,r.transition_time):0;i.select(this).transition().duration(r.transition_time).delay(s).attr("transform",`translate(${e.x}, ${e.y})`).style("opacity",1)})}(t,e,n,r),M(t,e,r);const a=r.tree_position||"fit";return r.initial?w({svg:t,svg_dim:t.getBoundingClientRect(),tree_dim:e.dim,transition_time:0}):"fit"===a?w({svg:t,svg_dim:t.getBoundingClientRect(),tree_dim:e.dim,transition_time:r.transition_time}):"main_to_middle"===a&&C({datum:e.data[0],svg:t,svg_dim:t.getBoundingClientRect(),scale:r.scale,transition_time:r.transition_time}),!0}function q(e,{d:t}){return e.updateMainId(t.data.id),e.updateTree({}),!0}function j(e,t){const n=e.rels,i=[...n.parents,...n.spouses||[],...n.children||[]].filter(e=>!!e);for(const n of i){if(!V(t.find(e=>e.id===n),t,[e.id]))return!1}return!0}function V(e,t,n=[]){const i=t[0];if(e.id===i.id)return!0;const r=[...n];let s=!1;return function e(n){if(s)return;const a=n.rels;[...a.parents,...a.spouses||[],...a.children||[]].filter(e=>!!e).forEach(n=>{if(r.includes(n))return;r.push(n);const a=t.find(e=>e.id===n);a.id===i.id?s=!0:e(a)})}(e),s}function z(e,t,n){n.forEach((t,n)=>e.data[n]=t),N(e,t),e.to_add&&delete e.to_add,e.unknown&&delete e.unknown}function N(e,t){Object.keys(e.data).forEach(n=>{if(n.includes("__ref__")){const i=n.split("__ref__")[1],r=t.find(e=>e.id===i);if(!r)return;const s=n.split("__ref__")[0]+"__ref__"+e.id;r.data[s]=e.data[n]}})}function U(e,t){Object.keys(e.data).forEach(n=>{if(n.includes("__ref__")){const i=n.split("__ref__")[1],r=t.find(e=>e.id===i);if(!r)return;const s=n.split("__ref__")[0]+"__ref__"+e.id;delete r.data[s]}})}function B(e,t){return Z(e,t,!1),!1}function Z(e,t,n=!0){return j(e,t)?(t.forEach(t=>{for(let n in t.rels){if(!t.rels.hasOwnProperty(n))continue;const i=n;Array.isArray(t.rels[i])&&t.rels[i].includes(e.id)&&t.rels[i].splice(t.rels[i].findIndex(t=>t===e.id),1)}}),U(e,t),t.splice(t.findIndex(t=>t.id===e.id),1),0===t.length&&t.push(d({data:{gender:"M"}})),n&&W(t),{success:!0}):(U(e,t),e.data={gender:e.data.gender},e.unknown=!0,{success:!0})}function J(e){return W(e),e.forEach(e=>{delete e.main,delete e._tgdp,delete e._tgdp_sp,delete e.__tgdp_sp}),e.forEach(e=>{Object.keys(e).forEach(e=>{"_"===e[0]&&console.error("key starts with _",e)})}),e}function W(e){for(let t=e.length-1;t>=0;t--)e[t].to_add&&B(e[t],e)}function G(){return`\n \n ${Me()}\n \n \n `}function K(){return`\n \n ${Me()}\n \n \n `}function Y(){return`\n \n ${Me()}\n \n \n `}function Q(){return`\n \n ${Me()}\n \n \n \n `}function X(){return`\n \n ${Me()}\n \n \n `}function ee(){return`\n \n ${Me()}\n \n \n `}function te(){return`\n \n ${Me()}\n \n \n `}function ne(){return`\n \n ${Me()}\n \n \n `}function ie(){return`\n \n ${Me()}\n \n \n `}function re(){return`\n \n ${Me()}\n \n \n `}function se(){return'\n \n \n \n '}function ae(){return'\n \n \n \n \n \n \n \n \n \n \n '}function oe(){return`\n \n ${Me()}\n \n \n \n `}function de(){return`\n \n ${Me()}\n \n \n \n `}function le(){return`\n \n ${Me()}\n \n \n `}function ce(){return`\n \n ${Me()}\n \n \n `}function ue(){return`\n \n ${Me()}\n \n \n `}function he(){return`\n \n ${Me()}\n \n \n `}function pe(){return Se(Y())}function fe(){return Se(Q())}function _e(){return Se(X())}function ge(){return Se(ee())}function me(){return Se(te())}function ve(){return Se(ie())}function ye(){return Se(re())}function we(){return Se('\n \n \n \n ',"0 0 512 512")}function be(){return Se('\n \n \n \n \n \n \n \n \n \n \n ',"0 0 72 25")}function Ce(){return Se(oe())}function xe(){return Se(de())}function $e(){return Se(le())}function ke(){return Se(ue())}function Ee(){return Se(he())}function Se(e,t="0 0 24 24"){const n=e.match(/data-icon="([^"]+)"/);return`\n \n ${e}\n \n `}function Me(){return'\n \n '}var Ie=Object.freeze({__proto__:null,chevronDownIcon:le,chevronDownSvgIcon:$e,chevronUpIcon:ce,chevronUpSvgIcon:function(){return Se(ce())},historyBackIcon:ie,historyBackSvgIcon:ve,historyForwardIcon:re,historyForwardSvgIcon:ye,infoIcon:he,infoSvgIcon:Ee,linkOffIcon:ue,linkOffSvgIcon:ke,miniTreeIcon:ae,miniTreeSvgIcon:be,pencilIcon:ee,pencilOffIcon:te,pencilOffSvgIcon:me,pencilSvgIcon:ge,personIcon:se,personSvgIcon:we,plusIcon:X,plusSvgIcon:_e,toggleIconOff:de,toggleIconOn:oe,toggleSvgIconOff:xe,toggleSvgIconOn:Ce,trashIcon:ne,trashSvgIcon:function(){return Se(ne())},userEditIcon:K,userEditSvgIcon:function(){return Se(K())},userIcon:G,userPlusCloseIcon:Q,userPlusCloseSvgIcon:fe,userPlusIcon:Y,userPlusSvgIcon:pe,userSvgIcon:function(){return Se(G())}});function Ae(e){return` \n
\n ${Fe()}\n
\n ${e.no_edit?"":function(e){return`\n \n ${e.addRelativeActive?fe():pe()}\n \n `}(e)}\n ${e.no_edit?'
':function(e){return`\n \n ${e.editable?me():ge()}\n \n `}(e)}\n
\n\n ${Re(e)}\n\n ${He(e)}\n \n
\n \n \n
\n\n ${e.linkExistingRelative?De(e):""}\n\n
\n ${function(e){return`\n
\n \n
\n `}(e)}\n\n ${function(e){return`\n
\n \n
\n `}(e)}\n
\n `}function Re(e){return e.editable?`\n
\n ${e.gender_field.options.map(t=>`\n \n `).join("")}\n
\n `:""}function He(e){if(!e.editable)return function(){let t="";return e.fields.forEach(e=>{var n;if("rel_reference"===e.type){if(!e.initial_value)return;t+=`\n
\n ${e.label} - ${e.rel_label}\n ${e.initial_value||""}\n
`}else if("select"===e.type){const i=e;if(!e.initial_value)return;t+=`\n
\n ${i.label}\n ${(null===(n=i.options.find(e=>e.value===i.initial_value))||void 0===n?void 0:n.label)||""}\n
`}else t+=`\n
\n ${e.label}\n ${e.initial_value||""}\n
`}),t}();let t="";return e.fields.forEach(e=>{if("text"===e.type)t+=`\n
\n \n \n
`;else if("textarea"===e.type)t+=`\n
\n \n \n
`;else if("select"===e.type){const n=e;t+=`\n
\n \n \n
`}else"rel_reference"===e.type&&(t+=`\n
\n \n \n
`)}),t}function De(e){return`\n
\n
\n \n
\n `}function Fe(){return'\n \n ×\n \n '}function Oe(e,t){return Te(e,t)}function Le(e,t){return Te(e,t)}function Te(e,t){const n=function(e){return"new_rel"in e}(e),i=document.createElement("div");return function r(){const s=n?function(e){return` \n
\n \n \n ×\n \n \n

${e.title}

\n ${Re(e)}\n\n ${He(e)}\n \n
\n \n \n
\n\n ${e.linkExistingRelative?De(e):""}\n
\n `}(e):Ae(e);i.innerHTML=s,function(e,t,n,i){const r=e.querySelector("form");r.addEventListener("submit",t.onSubmit);r.querySelector(".f3-cancel-btn").addEventListener("click",s);function s(){t.editable=!1,t.onCancel&&t.onCancel(),i()}r.querySelector(".f3-close-btn").addEventListener("click",n)}(i,e,t,r),n?function(e,t){const n=e.querySelector("form"),i=n.querySelector(".f3-link-existing-relative select");i&&i.addEventListener("change",t.linkExistingRelative.onSelect)}(i,e):function(e,t,n){const i=e.querySelector("form"),r=i.querySelector(".f3-edit-btn");r&&r.addEventListener("click",l);const s=i.querySelector(".f3-delete-btn");s&&t.onDelete&&s.addEventListener("click",t.onDelete);const a=i.querySelector(".f3-add-relative-btn");a&&t.addRelative&&a.addEventListener("click",()=>{t.addRelativeActive?t.addRelativeCancel():t.addRelative(),t.addRelativeActive=!t.addRelativeActive,n()});const o=i.querySelector(".f3-remove-relative-btn");o&&t.removeRelative&&o.addEventListener("click",()=>{t.removeRelativeActive?t.removeRelativeCancel():t.removeRelative(),t.removeRelativeActive=!t.removeRelativeActive,n()});const d=i.querySelector(".f3-link-existing-relative select");d&&d.addEventListener("change",t.linkExistingRelative.onSelect);function l(){t.editable=!t.editable,n()}}(i,e,r);e.onFormCreation&&e.onFormCreation({cont:i,form_creator:e})}(),i}function Pe(e,t,n){let i=[],r=-1;return{changed:function(){r0}function o(t){const i=e.getMainId();(t=JSON.parse(JSON.stringify(t))).find(e=>e.id===i)||e.updateMainId(t.main_id),e.updateData(t),n()}}function qe(e,t){const n=i.select(e).append("div").attr("class","f3-history-controls");e.insertBefore(n.node(),e.firstChild);const r=n.append("button").attr("class","f3-back-button").on("click",()=>{t.back(),a()}),s=n.append("button").attr("class","f3-forward-button").on("click",()=>{t.forward(),a()});return r.html(ve()),s.html(ye()),{back_btn:r.node(),forward_btn:s.node(),updateButtons:a,destroy:function(){i.select(e).select(".f3-history-controls").remove()}};function a(){r.classed("disabled",!t.canBack()),s.classed("disabled",!t.canForward()),t.canBack()||t.canForward()?n.style("opacity",1).style("pointer-events","auto"):n.style("opacity",0).style("pointer-events","none")}}var je=Object.freeze({__proto__:null,addNewPerson:function({data_stash:e,datum:t}){e.push(t)},calculateDelay:c,calculateTreeFit:b,cardChangeMain:q,cardComponentSetup:function(e){const t=()=>e.querySelector("#htmlSvg");return A(e,{onZoom:D(()=>e.querySelector("svg .view"),()=>e.querySelector("#htmlSvg .cards_view"))}),i.select(t()).append("div").attr("class","cards_view_fake").style("display","none"),O(t)},cardToMiddle:C,checkIfConnectedToFirstPerson:V,checkIfRelativesConnectedWithoutPerson:j,cleanupDataJson:J,createFormEdit:Le,createFormNew:Oe,createHistory:Pe,createHistoryControls:qe,createNewPerson:d,createNewPersonWithGenderFromRel:function({data:e,rel_type:t,rel_datum:n}){const i=function(e,t){return["daughter","mother"].includes(t)||"spouse"===t&&"M"===e.data.gender?"F":"M"}(n,t);return d({data:e=Object.assign(e||{},{gender:i})})},deletePerson:Z,getCurrentZoom:$,htmlContSetup:R,isAllRelativeDisplayed:l,manualZoom:x,moveToAddToAdded:function(e,t){return delete e.to_add,e},onDeleteSyncRelReference:U,removeToAdd:B,removeToAddFromData:W,setupZoom:S,submitFormData:z,syncRelReference:N,treeFit:w,zoomTo:k});function Ve({d:e,card_dim:t,card_display:n}){return{template:`\n \n \n ${ze({d:e,card_dim:t,card_display:n}).template}\n \n `}}function ze({d:e,card_dim:t,card_display:n}){return{template:`\n \n \n \n \n ${Array.isArray(n)?n.map(t=>`${t(e.data)}`).join("\n"):n(e.data)}\n \n \n \n \n \n `}}function Ne({d:e,card_dim:t,is_new:n}){return{template:`\n \n `}}function Ue(e,t,n){const i=document.createElementNS("http://www.w3.org/2000/svg","g");i.innerHTML=e,n?t.insertBefore(i,t.firstChild):t.appendChild(i)}const Be={miniTree:function(e,t){if(e.data.to_add)return;const n=t.card_dim;if(e.all_rels_displayed)return;const r=i.create("svg:g").html(function({d:e,card_dim:t}){return{template:`\n \n \n \n \n \n \n \n \n \n \n `}}({d:e,card_dim:n}).template);return r.on("click",function(n){n.stopPropagation(),t.onMiniTreeClick?t.onMiniTreeClick.call(this,n,e):q(t.store,{d:e})}),r.node()},cardBody:function(e,t){const n=t.card_dim,r=i.create("svg:g").html(Ve({d:e,card_dim:n,card_display:t.card_display}).template);return r.on("click",function(n){n.stopPropagation(),t.onCardClick?t.onCardClick.call(this,n,e):q(t.store,{d:e})}),r.node()},cardImage:function(e,t){if(e.data.to_add)return;const n=t.card_dim;return i.create("svg:g").html(function({d:e,image:t,card_dim:n,maleIcon:i,femaleIcon:r}){return{template:`\n \n ${t?``:(e.data.data.gender,e.data.data.gender,`\n \n \n \n \n \n \n `)} \n \n `}}({d:e,image:e.data.data.avatar||null,card_dim:n,maleIcon:void 0,femaleIcon:void 0}).template).node()}};function Ze(e,t,n=!1){e&&(n?t.insertBefore(e,t.firstChild):t.appendChild(e))}function Je(e){const t="default"===e.style?s:"imageCircleRect"===e.style?function(t){return t.data.data[e.cardImageField]?a(t):o(t)}:"imageCircle"===e.style?a:"imageRect"===e.style?function(e){return n(e)}:"rect"===e.style?o:s;return function(n){if(this.innerHTML=`\n
\n ${e.mini_tree?function(t){return e.mini_tree?t.data.to_add||t.data._new_rel_data||t.all_rels_displayed?"":`
${be()}
`:""}(n):""}\n ${e.cardInnerHtmlCreator&&!n.data._new_rel_data?e.cardInnerHtmlCreator(n):t(n)}\n
\n `,this.querySelector(".card").addEventListener("click",t=>e.onCardClick(t,n)),e.onCardUpdate&&e.onCardUpdate.call(this,n),e.onCardMouseenter&&i.select(this).select(".card").on("mouseenter",t=>e.onCardMouseenter(t,n)),e.onCardMouseleave&&i.select(this).select(".card").on("mouseleave",t=>e.onCardMouseleave(t,n)),n.duplicate&&function(e,t){i.select(e).on("mouseenter",n=>{i.select(e.closest(".cards_view")).selectAll(".card_cont").select(".card").classed("f3-card-duplicate-hover",e=>e.data.id===t.data.id)}),i.select(e).on("mouseleave",t=>{i.select(e.closest(".cards_view")).selectAll(".card_cont").select(".card").classed("f3-card-duplicate-hover",!1)})}(this,n),e.duplicate_branch_toggle&&function(e,t,n,r){if(!t.hasOwnProperty("_toggle"))return;const s=e.querySelector(".card"),a=s.querySelector(".card-inner"),o=e.querySelector(".card").offsetWidth;let d,l;e.querySelector(".card").offsetHeight;const c={};if(t.spouse){const e=t.spouse,i=e.data.main?"main":e.parent.data.id;if(d=e.data._tgdp_sp[i][t.data.id]<0,c.top=60,c.left=t.sx-t.x-30+o/2,n&&(c.top=t.sy-t.x+4,c.left=o/2+4,Math.abs(t.sx-t.y)<10&&(c.left=o-4)),l=e._toggle_id_sp?e._toggle_id_sp[t.data.id]:-1,-1===l)return}else{const e=t.data.main?"main":t.parent.data.id;d=t.data._tgdp[e]<0,c.top=-65,c.left=o/2-30,n&&(c.top=5,c.left=-55),l=t._toggle_id}if(a.style.zIndex=1,i.select(s).append("div").attr("class","f3-toggle-div").attr("style","cursor: pointer; width: 60px; height: 60px;position: absolute; z-index: -1;").style("top",c.top+"px").style("left",c.left+"px").on("click",e=>{if(e.stopPropagation(),t.spouse){const e=t.spouse,n=e.data.main?"main":e.parent.data.id;e.data._tgdp_sp[n].hasOwnProperty(t.data.id)||console.error("no toggle",t,e);let i=e.data._tgdp_sp[n][t.data.id];i=i<0?(new Date).getTime():-(new Date).getTime(),e.data._tgdp_sp[n][t.data.id]=i}else{const e=t.data.main?"main":t.parent.data.id;let n=t.data._tgdp[e];n=n<0?(new Date).getTime():-(new Date).getTime(),t.data._tgdp[e]=n}r()}).append("div").html(d?xe():Ce()).select("svg").classed("f3-toggle-icon",!0).style("color",d?"#585656":"#61bf52").style("padding","0"),i.select(s).select(".f3-toggle-icon .f3-small-circle").style("fill","#fff"),i.select(s).select(".f3-toggle-icon").append("text").attr("transform",d?"translate(10.6, 14.5)":"translate(4.1, 14.5)").attr("fill","#fff").attr("font-size","7px").text("C"+l),d){let e;e=t.is_ancestry?n?"translate(5, -30)rotate(-90)":"translate(0, -10)":n?"translate(11, -22)rotate(90)":"translate(-7, -32)rotate(180)",i.select(s).select(".f3-toggle-div").insert("div").html(be()).select("svg").attr("style","position: absolute; z-index: -1;top: 0;left: 0;border-radius: 0;").style("width","66px").style("height","112px").attr("transform",e).attr("viewBox","0 0 72 125").select("line").attr("y1",t.is_ancestry?"62":"92")}}(this,n,e.store.state.is_horizontal,e.store.updateTree),location.origin.includes("localhost")&&(n.__node=this.querySelector(".card"),n.__label=n.data.data["first name"],n.data.to_add)){const e=n.spouse||n.coparent||null;e&&i.select(this).select(".card").attr("data-to-add",e.data.data["first name"])}};function n(t){return`\n
\n ${t.data.data[e.cardImageField]?``:c(t)}\n
${r(t)}
\n ${t.duplicate?u(t):""}\n
\n `}function r(t){return t.data._new_rel_data?function(e){const t=[];t.push(`data-rel-type="${e.data._new_rel_data.rel_type}"`),["son","daughter"].includes(e.data._new_rel_data.rel_type)&&t.push(`data-other-parent-id="${e.data._new_rel_data.other_parent_id}"`);return`
${e.data._new_rel_data.label}
`}(t):t.data.to_add?`
${e.empty_card_label||"ADD"}
`:t.data.unknown?`
${e.unknown_card_label||"UNKNOWN"}
`:`\n ${e.card_display.map(e=>`
${e(t.data)}
`).join("")}\n `}function s(e){return n(e)}function a(t){return function(t){return`\n
\n ${t.data.data[e.cardImageField]?``:c(t)}\n
${r(t)}
\n ${t.duplicate?u(t):""}\n
\n `}(t)}function o(e){return function(e){return`\n
\n ${r(e)}\n ${e.duplicate?u(e):""}\n
\n `}(e)}function d(){let t='style="';return e.card_dim.w||e.card_dim.h?(t+=`width: ${e.card_dim.w}px; min-height: ${e.card_dim.h}px;`,e.card_dim.height_auto?t+="height: auto;":t+=`height: ${e.card_dim.h}px;`,t+='"',t):""}function l(){let t='style="position: relative;';return e.card_dim.img_w||e.card_dim.img_h||e.card_dim.img_x||e.card_dim.img_y?(t+=`width: ${e.card_dim.img_w}px; height: ${e.card_dim.img_h}px;`,t+=`left: ${e.card_dim.img_x}px; top: ${e.card_dim.img_y}px;`,t+='"',t):""}function c(t){return t.data._new_rel_data?`
${_e()}
`:`
${e.defaultPersonIcon?e.defaultPersonIcon(t):we()}
`}function u(e){return`
x${e.duplicate}
`}}function We(e,t){function n(e,t,n){const{w:i,h:r}=e,s=t,a=n||[],o=e=>a.includes(e);return`${o("lx")?"M0,0":`M0,${s} Q 0,0 5,0`} ${o("rx")?`H${i}`:`H${i-s} Q ${i},0 ${i},5`} ${o("ry")?`V${r}`:`V${r-s} Q ${i},${r} ${i-s},${r}`} ${o("ly")?"H0":`H${s} Q 0,${r} 0,${r-s}`} z`}e.querySelector("defs#f3CardDef")||e.insertAdjacentHTML("afterbegin",`\n \n \n \n \n \n \n \n \n \n \n \n \n `)}function Ge(e){return We((e=function(e){const t={img:!0,mini_tree:!0,link_break:!1,card_dim:{w:220,h:70,text_x:75,text_y:15,img_w:60,img_h:60,img_x:5,img_y:5}};e||(e={});for(const n in t)void 0===e[n]&&(e[n]=t[n]);return e}(e)).svg,e.card_dim),function(t){const n="M"===t.data.data.gender?"card-male":"F"===t.data.data.gender?"card-female":"card-genderless",r=e.card_dim,s=i.create("svg:g").attr("class",`card ${n}`).attr("transform",`translate(${[-r.w/2,-r.h/2]})`);s.append("g").attr("class","card-inner").attr("clip-path","url(#card_clip)"),this.innerHTML="",this.appendChild(s.node()),s.on("click",function(n){n.stopPropagation(),e.onCardClick.call(this,n,t)}),t.data._new_rel_data?(Ue(Ne({d:t,card_dim:r,is_new:t.data.to_add}).template,s.node(),!0),Ue(function({d:e,card_dim:t,label:n}){return{template:`\n \n \n \n ${n}\n \n \n `}}({d:t,card_dim:r,label:t.data._new_rel_data.label}).template,this.querySelector(".card-inner"),!0),i.select(this.querySelector(".card-inner")).append("g").attr("class","card-edit-icon").attr("fill","currentColor").attr("transform",`translate(-1,2)scale(${r.img_h/22})`).html(X())):(Ue(Ne({d:t,card_dim:r,is_new:t.data.to_add}).template,s.node(),!0),Ue(Ve({d:t,card_dim:r,card_display:e.card_display}).template,this.querySelector(".card-inner"),!1),e.img&&Ze(Be.cardImage(t,e),this.querySelector(".card")),e.mini_tree&&Ze(Be.miniTree(t,e),this.querySelector(".card"),!0)),e.onCardUpdate&&e.onCardUpdate.call(this,t)}}function Ke(e){return void 0===e.onCardClick&&(e.onCardClick=(t,n)=>{e.store.updateMainId(n.data.id),e.store.updateTree({})}),Ge(e)}function Ye(e,t){return new Qe(e,t)}class Qe{constructor(e,t){this.cont=e,this.active=!1,this.onClose=t,this.popup_cont=i.select(this.cont).append("div").attr("class","f3-popup").node(),this.create()}create(){const e=i.select(this.popup_cont);e.html('\n
\n ×\n
\n
\n '),e.select(".f3-popup-close").on("click",()=>{this.close()}),e.on("click",t=>{t.target==e.node()&&this.close()})}activate(e){const t=i.select(this.popup_cont).select(".f3-popup-content-inner").node();e&&t.appendChild(e),this.open()}open(){this.active=!0}close(){this.popup_cont.remove(),this.active=!1,this.onClose&&this.onClose()}}function Xe(e,t,n){const i=t.find(t=>t.id===e),r={};return function e(n,i,s,a){if(!n)return;if(r[n])return;i&&(r[n]=i);const o=t.find(e=>e.id===n),d=o.rels;if("self"===i)d.parents.forEach(t=>e(t,"parent",s-1,n)),(d.spouses||[]).forEach(t=>e(t,"spouse",s)),(d.children||[]).forEach(t=>e(t,"child",s+1));else if("parent"===i)d.parents.forEach(t=>e(t,"grandparent",s-1,n)),(d.children||[]).forEach(t=>{a&&a===t||e(t,"sibling",s+1)});else if("spouse"===i);else if("child"===i)(d.children||[]).forEach(t=>e(t,"grandchild",s+1));else if("sibling"===i)(d.children||[]).forEach(t=>e(t,"nephew",s+1));else if("grandparent"===i)a||console.error(`${i} should have prev_rel_id`),d.parents.forEach(t=>e(t,"great-grandparent",s-1,n)),(d.children||[]).forEach(t=>{a&&a===t||e(t,"uncle",s+1)});else if(i.includes("grandchild"))(d.children||[]).forEach(t=>e(t,it(i,s+1),s+1));else if(i.includes("great-grandparent"))a||console.error(`${i} should have prev_rel_id`),d.parents.forEach(t=>e(t,it(i,s-1),s-1,n)),(d.children||[]).forEach(t=>{if(a&&a===t)return;const n=nt(s+1);0===n?e(t,"granduncle",s+1):n>0?e(t,it("granduncle",s+1),s+1):console.error(`${i} should have great_count > -1`)});else if("nephew"===i)(d.children||[]).forEach(t=>e(t,"grandnephew",s+1));else if(i.includes("grandnephew"))(d.children||[]).forEach(t=>e(t,it(i,s+1),s+1));else if("uncle"===i)(d.children||[]).forEach(t=>e(t,"1st Cousin",s+1));else if("granduncle"===i)(d.children||[]).forEach(t=>e(t,"1st Cousin 1x removed",s+1));else if(i.includes("great-granduncle")){const t=s+1,n=Math.abs(t);(d.children||[]).forEach(i=>e(i,`1st Cousin ${n}x removed`,t))}else i.slice(4).startsWith("Cousin")?(d.children||[]).forEach(t=>{const n=s+1,r=Math.abs(n),a=+i[0];0===n?e(t,`${tt(a+1)} Cousin`,n):n<0?e(t,`${tt(a+1)} Cousin ${r}x removed`,n):n>0&&e(t,`${tt(a)} Cousin ${r}x removed`,n)}):console.error(`${i} not found`)}(i.id,"self",0),function(e){const n=[];Object.keys(e).forEach(r=>{const s=e[r];if(s.includes("child"))return;if("spouse"===s)return;const a=et(i.id,r,t);if(!a)return console.error(`${t.find(e=>e.id===r).data} not found in main_ancestry`);a.is_half_kin&&n.push(r)}),n.forEach(t=>{e[t]=`Half ${e[t]}`})}(r),n.show_in_law&&function(e,t){Object.keys(e).forEach(n=>{const i=e[n],r=t.find(e=>e.id===n);if("spouse"===i){const t=[];r.rels.parents.forEach(e=>(s(e).rels.children||[]).forEach(e=>t.push(e))),t.forEach(t=>{e[t]||(e[t]="sibling-in-law")})}"sibling"===i&&(r.rels.spouses||[]).forEach(t=>{e[t]||(e[t]="sibling-in-law")}),"child"===i&&(r.rels.spouses||[]).forEach(t=>{e[t]||(e[t]="child-in-law")}),"uncle"===i&&(r.rels.spouses||[]).forEach(t=>{e[t]||(e[t]="uncle-in-law")}),i.includes("Cousin")&&(r.rels.spouses||[]).forEach(t=>{e[t]||(e[t]=`${i} in-law`)})})}(r,t),function(e){Object.keys(e).forEach(n=>{const i=e[n],r=t.find(e=>e.id===n).data.gender;if(i.includes("parent")){const t="M"===r?"father":"F"===r?"mother":"parent";e[n]=e[n].replace("parent",t)}else if(i.includes("sibling")){const t="M"===r?"brother":"F"===r?"sister":"sibling";e[n]=e[n].replace("sibling",t)}else if(i.includes("child")){const t="M"===r?"son":"F"===r?"daughter":"child";e[n]=e[n].replace("child",t)}else if(i.includes("uncle")){const t="M"===r?"uncle":"F"===r?"aunt":"aunt/uncle";e[n]=e[n].replace("uncle",t)}else if(i.includes("nephew")){const t="M"===r?"nephew":"F"===r?"niece":"neice/nephew";e[n]=e[n].replace("nephew",t)}})}(r),r;function s(e){return t.find(t=>t.id===e)}}function et(e,t,n){const i=function(e){const t=[];return function e(i){const r=n.find(e=>e.id===i),s=r.rels;t.push(o(s)),s.parents.forEach(t=>e(t))}(e),t}(e);let r,s,a;return function(e){const t=n.find(t=>t.id===e);i.find(e=>e[0]===t.id||e[1]===t.id)&&(s=!0,r=e,a=!1)}(t),function(t){(n.find(t=>t.id===e).rels.spouses||[]).includes(t)&&(r=[e,t])}(t),function t(d){if(r)return;if(d===e)return s=!0,r=d,void(a=!1);const l=n.find(e=>e.id===d),c=l.rels,u=o(c),h=i.find(e=>e[0]&&u[0]&&e[0]===u[0]||e[1]&&u[1]&&e[1]===u[1]);if(h)return r=u.filter((e,t)=>e===h[t]),f=h,void(a=(p=u).some((e,t)=>e!==f[t])||f.some((e,t)=>e!==p[t]));var p,f;c.parents.forEach(e=>t(e))}(t),r?{found:r,is_ancestor:s,is_half_kin:a}:null;function o(e){return e.parents}}function tt(e){const t=["st","nd","rd"];return t[e-1]?e+t[e-1]:e+"th"}function nt(e){return Math.abs(e)-2}function it(e,t){const n=nt(t);return e.includes("great-")&&(e=e.split("great-")[1]),1===n?`great-${e}`:n>1?`${n}x-great-${e}`:(console.error(`${e} should have great_count > 1`),e)}function rt(e,t,n,r){var s;let a;const o=r[t].toLowerCase();if(o.includes("in-law")){a=t;const i=n.find(e=>e.id===a);t=o.includes("sister")||o.includes("brother")?e:null===(s=i.rels.spouses)||void 0===s?void 0:s.find(e=>r[e]&&!r[e].includes("in-law"))}const d=et(e,t,n);if(!d)return console.error(`${t} not found in main_ancestry`);const l=d.is_ancestor?d.found:d.found[0],c=n.find(e=>e.id===l),u=i.hierarchy(c,function(e){return[...e.rels.children||[]].map(e=>n.find(t=>t.id===e)).filter(e=>e)}),h=u.descendants().map(e=>e.data.id),p=g(e,h),f=g(t,h);!function e(t){t.children=(t.children||[]).filter(e=>!!p.includes(e.data.id)||!!f.includes(e.data.id)),t.children.forEach(t=>e(t)),0===t.children.length&&delete t.children}(u);const _=u.descendants().map(e=>{const t={id:e.data.id,data:JSON.parse(JSON.stringify(e.data.data)),kinship:r[e.data.id],rels:{parents:[],spouses:[],children:[]}};return e.children&&e.children.length>0&&(t.rels.children=e.children.map(e=>e.data.id)),t});return _.length>0&&!d.is_ancestor&&!d.is_half_kin&&function(e){const i=e[0];if(!d)return console.error(`${t} not found in main_ancestry`);const s=l===d.found[0]?d.found[1]:d.found[0];i.rels.spouses=[s];const a=n.find(e=>e.id===s),o={id:a.id,data:JSON.parse(JSON.stringify(a.data)),kinship:r[a.id],rels:{spouses:[i.id],children:i.rels.children,parents:[]}};e.push(o),(i.rels.children||[]).forEach(t=>{const i=n.find(e=>e.id===t);e.find(e=>e.id===t).rels.parents=[...i.rels.parents]})}(_),a&&function(e){o.includes("sister")||o.includes("brother")?function(e){var n;const i=e.find(e=>e.id===t),s=m(a);e.push({id:a,data:JSON.parse(JSON.stringify(s.data)),kinship:r[a],rels:{spouses:[],children:[],parents:[]}});const o=[];s.rels.parents.forEach(e=>(m(e).rels.children||[]).forEach(e=>o.push(e)));const d=null===(n=m(t).rels.spouses)||void 0===n?void 0:n.find(e=>o.includes(e));i.rels.spouses=[d];const l=m(d),c={id:l.id,data:JSON.parse(JSON.stringify(l.data)),kinship:r[l.id],rels:{spouses:[i.id],children:[],parents:[]}};e.push(c),s.rels.parents.forEach(t=>{const n=m(t),i="M"===n.data.gender?"Father-in-law":"F"===n.data.gender?"Mother-in-law":"Parent-in-law",r={id:n.id,data:JSON.parse(JSON.stringify(n.data)),kinship:i,rels:{spouses:[],children:[d,a],parents:[]}},o=s.rels.parents.find(e=>e!=e);o&&r.rels.parents.push(o),e.unshift(r)})}(e):function(e){const i=e.find(e=>e.id===t),s=a;i.rels.spouses=[s];const o=n.find(e=>e.id===s),d={id:o.id,data:JSON.parse(JSON.stringify(o.data)),kinship:r[o.id],rels:{spouses:[i.id],children:[],parents:[]}};e.push(d)}(e)}(_),_;function g(e,t){const i=[e];return function e(r){const s=n.find(e=>e.id===r);s.rels.parents.forEach(n=>{t.includes(n)&&(i.push(n),e(n))})}(e),i}function m(e){return n.find(t=>t.id===e)}}function st(e,t,n){const i=e.id;n.forEach(e=>{e.rels.parents.includes(i)&&(e.rels.parents[e.rels.parents.indexOf(i)]=t),e.rels.spouses&&e.rels.spouses.includes(i)&&(e.rels.spouses=e.rels.spouses.filter(e=>e!==i),e.rels.spouses.includes(t)||e.rels.spouses.push(t)),e.rels.children&&e.rels.children.includes(i)&&(e.rels.children=e.rels.children.filter(e=>e!==i),e.rels.children.includes(t)||e.rels.children.push(t))});const r=n.find(e=>e.id===t),s=n.find(e=>e.id===i);if(!s)throw new Error("New rel not found");if(!r)throw new Error("Link rel not found");if((s.rels.children||[]).forEach(e=>{r.rels.children||(r.rels.children=[]),r.rels.children.includes(e)||r.rels.children.push(e)}),(s.rels.spouses||[]).forEach(e=>{r.rels.spouses||(r.rels.spouses=[]),r.rels.spouses.includes(e)||r.rels.spouses.push(e)}),0===r.rels.parents.length)r.rels.parents=[...s.rels.parents];else{const e=r.rels.parents.find(e=>{var t;return"M"===(null===(t=n.find(t=>t.id===e))||void 0===t?void 0:t.data.gender)}),t=r.rels.parents.find(e=>{var t;return"F"===(null===(t=n.find(t=>t.id===e))||void 0===t?void 0:t.data.gender)}),i=s.rels.parents.find(e=>{var t;return"M"===(null===(t=n.find(t=>t.id===e))||void 0===t?void 0:t.data.gender)}),a=s.rels.parents.find(e=>{var t;return"F"===(null===(t=n.find(t=>t.id===e))||void 0===t?void 0:t.data.gender)});i&&(e?(console.error("link rel already has father"),r.rels.parents[r.rels.parents.indexOf(e)]=i):r.rels.parents.push(i)),a&&(t?(console.error("link rel already has mother"),r.rels.parents[r.rels.parents.indexOf(t)]=a):r.rels.parents.push(a))}n.splice(n.findIndex(e=>e.id===i),1)}function at(e,t){const n=e._new_rel_data?t.find(t=>t.id===e._new_rel_data.rel_id):null,i=function(e,t){const n=[];return i(e),n;function i(e){e.rels.parents.forEach(e=>{if(e){n.push(e);const r=t.find(t=>t.id===e);if(!r)throw new Error("Parent not found");i(r)}})}}(e,t),r=s(e,t);if(e._new_rel_data&&["son","daughter"].includes(e._new_rel_data.rel_type)){if(!n)throw new Error("Rel datum not found");r.push(...s(n,t))}return t.filter(t=>t.id!==e.id&&t.id!==(null==n?void 0:n.id)&&!t._new_rel_data&&!t.to_add&&!t.unknown).filter(e=>!i.includes(e.id)).filter(e=>!r.includes(e.id)).filter(t=>!(t.rels.spouses||[]).includes(e.id));function s(e,t){const n=[];return function e(i){(i.rels.children?[...i.rels.children]:[]).forEach(i=>{n.push(i);const r=t.find(e=>e.id===i);if(!r)throw new Error("Child not found");e(r)})}(e),n}}function ot({datum:e,store:t,fields:n,postSubmitHandler:i,addRelative:r,removeRelative:s,deletePerson:a,onCancel:o,editFirst:d,link_existing_rel_config:l,onFormCreation:c,no_edit:u,onSubmit:h,onDelete:p,canEdit:f,canDelete:_}){let g=!_||_(e);let m;!f||f(e)||(u=!0,g=!1);const v={datum_id:e.id,fields:[],onSubmit:function(n){h?h(n,e,r,()=>i({})):(n.preventDefault(),r(),i({}));function r(){const i=new FormData(n.target);z(e,t.getData(),i)}},onCancel:o,onFormCreation:c,no_edit:u,gender_field:{id:"gender",type:"switch",label:"Gender",initial_value:e.data.gender,disabled:!1,options:[{value:"M",label:"Male"},{value:"F",label:"Female"}]}};if(e._new_rel_data)m=Object.assign(Object.assign({},v),{title:e._new_rel_data.label,new_rel:!0,editable:!0});else{if(!r)throw new Error("addRelative is required");if(!s)throw new Error("removeRelative is required");m=Object.assign(Object.assign({},v),{onDelete:function(){p?p(e,()=>a(),()=>i({delete:!0})):(a(),i({delete:!0}))},addRelative:()=>r.activate(e),addRelativeCancel:()=>r.onCancel(),addRelativeActive:r.is_active,removeRelative:()=>s.activate(e),removeRelativeCancel:()=>s.onCancel(),removeRelativeActive:s.is_active,editable:!1,can_delete:g})}return(e._new_rel_data||e.to_add||e.unknown)&&l&&(m.linkExistingRelative=function(e,t,n){if(!n)throw new Error("link_existing_rel_config is required");const i={title:n.title,select_placeholder:n.select_placeholder,options:at(e,t).map(e=>({value:e.id,label:n.linkRelLabel(e)})).sort((e,t)=>"string"==typeof e.label&&"string"==typeof t.label?e.label.localeCompare(t.label):e.label{"rel_reference"===n.type?function(n){n.getRelLabel||console.error("getRelLabel is not set");"spouse"===n.rel_type&&(e.rels.spouses||[]).forEach(i=>{const r=t.getDatum(i);if(!r)throw new Error("Spouse not found");const s=`${n.id}__ref__${i}`,a={id:s,type:"rel_reference",label:n.label,rel_id:i,rel_label:n.getRelLabel(r),initial_value:e.data[s],rel_type:n.rel_type};m.fields.push(a)})}(n):"select"===n.type?function(t){if(!t.options&&!t.optionCreator)return console.error("optionCreator or options is not set for field",t);const n={id:t.id,type:t.type,label:t.label,initial_value:e.data[t.id],placeholder:t.placeholder,options:t.options||t.optionCreator(e)};m.fields.push(n)}(n):m.fields.push({id:n.id,type:n.type,label:n.label,initial_value:e.data[n.id]})}),m;function y(e){const t=e.target.value;i({link_rel_id:t})}}class dt{constructor(e,t,n){return this.store=e,this.onActivate=t,this.cancelCallback=n,this.datum=null,this.onChange=null,this.onCancel=null,this.is_active=!1,this.addRelLabels=this.addRelLabelsDefault(),this}activate(e){this.is_active&&this.onCancel(),this.onActivate(),this.is_active=!0,this.store.state.one_level_rels=!0;const t=this.store;this.datum=e;let n=this.datum.data.gender;!function(e,t,n,i){let r={parent:!0,spouse:!0,child:!0};i&&(r=Object.assign(r,i(e))),e.rels.spouses||(e.rels.spouses=[]),e.rels.children||(e.rels.children=[]),r.parent&&function(){const i=e.rels.parents,r=i.find(e=>{var n;return"M"===(null===(n=t.find(t=>t.id===e))||void 0===n?void 0:n.data.gender)}),s=i.find(e=>{var n;return"F"===(null===(n=t.find(t=>t.id===e))||void 0===n?void 0:n.data.gender)});if(i.length<2&&!r){const i=d({data:{gender:"M"},rels:{children:[e.id]}});i._new_rel_data={rel_type:"father",label:n.father,rel_id:e.id},e.rels.parents.push(i.id),t.push(i)}if(i.length<2&&!s){const i=d({data:{gender:"F"},rels:{children:[e.id]}});i._new_rel_data={rel_type:"mother",label:n.mother,rel_id:e.id},e.rels.parents.push(i.id),t.push(i)}const a=t.find(t=>t.id===e.rels.parents[0]),o=t.find(t=>t.id===e.rels.parents[1]);a.rels.spouses||(a.rels.spouses=[]),o.rels.spouses||(o.rels.spouses=[]),a.rels.spouses.includes(o.id)||a.rels.spouses.push(o.id),o.rels.spouses.includes(a.id)||o.rels.spouses.push(a.id),a.rels.children||(a.rels.children=[]),o.rels.children||(o.rels.children=[]),a.rels.children.includes(e.id)||a.rels.children.push(e.id),o.rels.children.includes(e.id)||o.rels.children.push(e.id)}(),r.spouse&&(function(){if(e.rels.spouses||(e.rels.spouses=[]),e.rels.children){let i;e.rels.children.forEach(r=>{const s=t.find(e=>e.id===r);if(1===s.rels.parents.length){const r="M"===t.find(e=>e.id===s.rels.parents[0]).data.gender?"F":"M";i||(i=d({data:{gender:r},rels:{spouses:[e.id]}})),i._new_rel_data={rel_type:"spouse",label:n.spouse,rel_id:e.id},i.rels.children.push(s.id),e.rels.spouses.push(i.id),s.rels.parents.push(i.id),t.push(i)}})}}(),function(){e.rels.spouses||(e.rels.spouses=[]);const i=d({data:{gender:"M"===e.data.gender?"F":"M"},rels:{spouses:[e.id]}});i._new_rel_data={rel_type:"spouse",label:n.spouse,rel_id:e.id},e.rels.spouses.push(i.id),t.push(i)}()),r.child&&(e.rels.children||(e.rels.children=[]),e.rels.spouses||(e.rels.spouses=[]),e.rels.spouses.forEach(i=>{const r=t.find(e=>e.id===i);r.rels.children||(r.rels.children=[]);const s=d({data:{gender:"M"},rels:{parents:[e.id,r.id]}});s._new_rel_data={rel_type:"son",label:n.son,other_parent_id:r.id,rel_id:e.id},r.rels.children.push(s.id),e.rels.children.push(s.id),t.push(s);const a=d({data:{gender:"F"},rels:{parents:[e.id,r.id]}});a._new_rel_data={rel_type:"daughter",label:n.daughter,other_parent_id:r.id,rel_id:e.id},r.rels.children.push(a.id),e.rels.children.push(a.id),t.push(a)}))}(e,this.getStoreData(),this.addRelLabels,this.canAdd),t.updateTree({}),this.onChange=function(i,r){(null==i?void 0:i._new_rel_data)?(null==r?void 0:r.link_rel_id)?st(i,r.link_rel_id,t.getData()):delete i._new_rel_data:i.id===e.id?i.data.gender!==n&&(n=i.data.gender,t.getData().forEach(e=>{const t=e._new_rel_data;t&&"spouse"===t.rel_type&&(e.data.gender="M"===e.data.gender?"F":"M")})):console.error("Something went wrong")},this.onCancel=()=>function(e){if(!e.is_active)return;e.is_active=!1,e.store.state.one_level_rels=!1,e.cleanUp(),e.cancelCallback(e.datum),e.datum=null,e.onChange=null,e.onCancel=null}(this)}setAddRelLabels(e){if("object"==typeof e){for(const t in e){const n=t;this.addRelLabels[n]=e[n]}return this}console.error("add_rel_labels must be an object")}setCanAdd(e){return this.canAdd=e,this}addRelLabelsDefault(){return{father:"Add Father",mother:"Add Mother",spouse:"Add Spouse",son:"Add Son",daughter:"Add Daughter"}}getStoreData(){return this.store.getData()}cleanUp(e){return e||(e=this.store.getData()),function(e){for(let t=e.length-1;t>=0;t--){const n=e[t];n._new_rel_data&&(e.forEach(e=>{e.rels.parents.includes(n.id)&&e.rels.parents.splice(e.rels.parents.indexOf(n.id),1),e.rels.children&&e.rels.children.includes(n.id)&&e.rels.children.splice(e.rels.children.indexOf(n.id),1),e.rels.spouses&&e.rels.spouses.includes(n.id)&&e.rels.spouses.splice(e.rels.spouses.indexOf(n.id),1)}),e.splice(t,1))}}(e),e}}class lt{constructor(e,t,n,i){return this.store=e,this.onActivate=t,this.cancelCallback=n,this.modal=i,this.datum=null,this.onChange=null,this.onCancel=null,this.is_active=!1,this}activate(e){this.is_active&&this.onCancel(),this.onActivate(),this.is_active=!0,this.store.state.one_level_rels=!0;const t=this.store;t.updateTree({}),this.datum=e,this.onChange=function(n,r){const s=function(t){if(t.is_ancestry){if(e.rels.parents.includes(t.data.id))return"parent"}else if(t.spouse){if(!e.rels.spouses)throw new Error("Spouses not found");if(e.rels.spouses.includes(t.data.id))return"spouse"}else{if(!e.rels.children)throw new Error("Children not found");if(e.rels.children.includes(t.data.id))return"children"}return null}(n),a=e.rels;"parent"===s?function(){const i=n.data.id,s=t.getDatum(i);if(!s)throw new Error("Parent not found");if(!s.rels.children)throw new Error("Parent has no children");s.rels.children=s.rels.children.filter(t=>t!==e.id),a.parents=a.parents.filter(e=>e!==i),r()}.call(this):"spouse"===s?function(){const s=n.data;o()?d.call(this):l.call(this,!0);function o(){return(s.rels.children||[]).some(e=>{const n=t.getDatum(e);if(!n)throw new Error("Child not found");return!!n.rels.parents.includes(s.id)})}function d(){const t="M"===e.data.gender?"f3-male-bg":"F"===e.data.gender?"f3-female-bg":null,n="M"===s.data.gender?"f3-male-bg":"F"===s.data.gender?"f3-female-bg":null,r=i.create("div").html(`\n

You are removing a spouse relationship. Since there are shared children, please choose which parent should keep them in the family tree.

\n
\n \n \n
\n `);r.selectAll('[data-option="assign-to-current"]').on("click",()=>{l(!0),this.modal.close()}),r.selectAll('[data-option="assign-to-spouse"]').on("click",()=>{l(!1),this.modal.close()}),this.modal.activate(r.node())}function l(i){n.data.rels.spouses=n.data.rels.spouses.filter(t=>t!==e.id),a.spouses=a.spouses.filter(e=>e!==n.data.id);const s=i?e:n.data,o=i?n.data:e;(a.children||[]).forEach(e=>{const n=t.getDatum(e);if(!n)throw new Error("Child not found");n.rels.parents.includes(o.id)&&(n.rels.parents=n.rels.parents.filter(e=>e!==o.id))}),o.rels.children&&(o.rels.children=o.rels.children.filter(e=>!(s.rels.children||[]).includes(e))),r()}}.call(this):"children"===s&&function(){if(!a.children)throw new Error("Children not found");a.children=a.children.filter(e=>e!==n.data.id),n.data.rels.parents=n.data.rels.parents.filter(t=>t!==e.id),r()}.call(this)}.bind(this),this.onCancel=function(){if(!this.is_active)return;if(this.is_active=!1,this.store.state.one_level_rels=!1,!this.datum)throw new Error("Datum not found");this.cancelCallback(this.datum),this.datum=null,this.onChange=null,this.onCancel=null}.bind(this)}}class ct{constructor(e){this.cont=e,this.active=!1,this.onClose=null,this.modal_cont=i.select(this.cont).append("div").attr("class","f3-modal").node(),i.select(this.modal_cont).style("display","none"),this.create()}create(){const e=i.select(this.modal_cont);e.html('\n
\n ×\n
\n
\n
\n '),e.select(".f3-modal-close").on("click",()=>{this.close()}),e.on("click",t=>{t.target==e.node()&&this.close()})}activate(e,{boolean:t,onAccept:n,onCancel:r}={}){this.reset();const s=i.select(this.modal_cont).select(".f3-modal-content-inner").node();if("string"==typeof e?s.innerHTML=e:s.appendChild(e),t){if(!n)throw new Error("onAccept is required");if(!r)throw new Error("onCancel is required");i.select(this.modal_cont).select(".f3-modal-content-bottom").html('\n \n \n '),i.select(this.modal_cont).select(".f3-modal-accept").on("click",()=>{n(),this.reset(),this.close()}),i.select(this.modal_cont).select(".f3-modal-cancel").on("click",()=>{this.close()}),this.onClose=r}this.open()}reset(){this.onClose=null,i.select(this.modal_cont).select(".f3-modal-content-inner").html(""),i.select(this.modal_cont).select(".f3-modal-content-bottom").html("")}open(){this.modal_cont.style.display="block",this.active=!0}close(){this.modal_cont.style.display="none",this.active=!1,this.onClose&&this.onClose()}}class ut{constructor(e,t){return this.cont=e,this.store=t,this.fields=[{type:"text",label:"first name",id:"first name"},{type:"text",label:"last name",id:"last name"},{type:"text",label:"birthday",id:"birthday"},{type:"text",label:"avatar",id:"avatar"}],this.is_fixed=!0,this.no_edit=!1,this.onChange=null,this.editFirst=!1,this.postSubmit=null,this.onFormCreation=null,this.createFormEdit=null,this.createFormNew=null,this.formCont=this.getFormContDefault(),this.modal=this.setupModal(),this.addRelativeInstance=this.setupAddRelative(),this.removeRelativeInstance=this.setupRemoveRelative(),this.history=this.createHistory(),this}open(e){e.rels||(e=e.data),this.addRelativeInstance.is_active?function(t){e._new_rel_data?t.cardEditForm(e):(t.addRelativeInstance.onCancel(),t.cardEditForm(e),t.store.updateMainId(e.id),t.store.updateTree({}))}(this):this.removeRelativeInstance.is_active?function(t,n){if(!n)throw new Error("Tree datum not found");if(!t.removeRelativeInstance.datum)throw new Error("Remove relative datum not found");if(!t.removeRelativeInstance.onCancel)throw new Error("Remove relative onCancel not found");if(!t.removeRelativeInstance.onChange)throw new Error("Remove relative onChange not found");if(e.id===t.removeRelativeInstance.datum.id)t.removeRelativeInstance.onCancel(),t.cardEditForm(e);else{function i(){t.removeRelativeInstance.onCancel(),t.updateHistory(),t.store.updateTree({})}t.removeRelativeInstance.onChange(n,i.bind(t))}}(this,this.store.getTreeDatum(e.id)):this.cardEditForm(e)}setupAddRelative(){return((e,t,n)=>new dt(e,t,n))(this.store,()=>function(e){e.removeRelativeInstance.is_active&&e.removeRelativeInstance.onCancel()}(this),e=>function(e,t){e.store.updateMainId(t.id),e.store.updateTree({}),e.openFormWithId(t.id)}(this,e))}setupRemoveRelative(){return((e,t,n,i)=>new lt(e,t,n,i))(this.store,function(){this.addRelativeInstance.is_active&&this.addRelativeInstance.onCancel();e(this.cont,!0)}.bind(this),function(t){e(this.cont,!1),this.store.updateMainId(t.id),this.store.updateTree({}),this.openFormWithId(t.id)}.bind(this),this.modal);function e(e,t){i.select(e).select("#f3Canvas").classed("f3-remove-relative-active",t)}}createHistory(){const e=Pe(this.store,this._getStoreDataCopy.bind(this),function(){var e;console.log("historyUpdateTree"),this.addRelativeInstance.is_active&&this.addRelativeInstance.onCancel();this.removeRelativeInstance.is_active&&this.removeRelativeInstance.onCancel();this.store.updateTree({initial:!1}),this.history.controls.updateButtons(),this.openFormWithId(null===(e=this.store.getMainDatum())||void 0===e?void 0:e.id),this.onChange&&this.onChange()}.bind(this)),t=this.cont.querySelector(".f3-nav-cont");if(!t)throw new Error("Nav cont not found");const n=qe(t,e);return e.changed(),n.updateButtons(),Object.assign(Object.assign({},e),{controls:n})}openWithoutRelCancel(e){this.cardEditForm(e)}getFormContDefault(){let e=i.select(this.cont).select("div.f3-form-cont").node();return e||(e=i.select(this.cont).append("div").classed("f3-form-cont",!0).node()),{el:e,populate(t){e.innerHTML="",e.appendChild(t)},open(){i.select(e).classed("opened",!0)},close(){i.select(e).classed("opened",!1).html("")}}}setFormCont(e){return this.formCont=e,this}cardEditForm(e){const t={},n=null==e?void 0:e._new_rel_data;n?t.onCancel=()=>this.addRelativeInstance.onCancel():(t.addRelative=this.addRelativeInstance,t.removeRelative=this.removeRelativeInstance,t.deletePerson=()=>{Z(e,this.store.getData()),this.openFormWithId(this.store.getLastAvailableMainDatum().id),this.store.updateTree({})});const i=ot(Object.assign({store:this.store,datum:e,postSubmitHandler:t=>function(t,n){if(t.addRelativeInstance.is_active){t.addRelativeInstance.onChange(e,n),t.postSubmit&&t.postSubmit(e,t.store.getData());const i=t.addRelativeInstance.datum;if(!i)throw new Error("Active datum not found");t.store.updateMainId(i.id),t.openWithoutRelCancel(i)}else(e.to_add||e.unknown)&&(null==n?void 0:n.link_rel_id)?(st(e,n.link_rel_id,t.store.getData()),t.store.updateMainId(n.link_rel_id),t.openFormWithId(n.link_rel_id)):(null==n?void 0:n.delete)||(t.postSubmit&&t.postSubmit(e,t.store.getData()),t.openFormWithId(e.id));t.is_fixed||t.closeForm();t.store.updateTree({}),t.updateHistory()}(this,t),fields:this.fields,onCancel:()=>{},editFirst:this.editFirst,no_edit:this.no_edit,link_existing_rel_config:this.link_existing_rel_config,onFormCreation:this.onFormCreation,onSubmit:this.onSubmit,onDelete:this.onDelete,canEdit:this.canEdit,canDelete:this.canDelete},t)),r=n?(this.createFormNew||Oe)(i,this.closeForm.bind(this)):(this.createFormEdit||Le)(i,this.closeForm.bind(this));this.formCont.populate(r),this.openForm()}openForm(){this.formCont.open()}closeForm(){this.formCont.close(),this.store.updateTree({})}fixed(){return this.is_fixed=!0,this.formCont.el&&i.select(this.formCont.el).style("position","relative"),this}absolute(){return this.is_fixed=!1,this.formCont.el&&i.select(this.formCont.el).style("position","absolute"),this}setCardClickOpen(e){return e.setOnCardClick((t,n)=>{this.isAddingRelative()||this.isRemovingRelative()?this.open(n.data):(this.open(n.data),e.onCardClickDefault(t,n))}),this}openFormWithId(e){if(e){const t=this.store.getDatum(e);if(!t)throw new Error("Datum not found");this.openWithoutRelCancel(t)}else{const e=this.store.getMainDatum();if(!e)throw new Error("Main datum not found");this.openWithoutRelCancel(e)}}setNoEdit(){return this.no_edit=!0,this}setEdit(){return this.no_edit=!1,this}setFields(e){const t=[];if(!Array.isArray(e))return console.error("fields must be an array"),this;for(const n of e)"string"==typeof n?t.push({type:"text",label:n,id:n}):"object"==typeof n?n.id?t.push(n):console.error("fields must be an array of objects with id property"):console.error("fields must be an array of strings or objects");return this.fields=t,this}setOnChange(e){return this.onChange=e,this}setCanEdit(e){return this.canEdit=e,this}setCanDelete(e){return this.canDelete=e,this}setCanAdd(e){return this.addRelativeInstance.setCanAdd(e),this}addRelative(e){return e||(e=this.store.getMainDatum()),this.addRelativeInstance.activate(e),this}setupModal(){return e=this.cont,new ct(e);var e}setEditFirst(e){return this.editFirst=e,this}isAddingRelative(){return this.addRelativeInstance.is_active}isRemovingRelative(){return this.removeRelativeInstance.is_active}setAddRelLabels(e){return this.addRelativeInstance.setAddRelLabels(e),this}setLinkExistingRelConfig(e){return this.link_existing_rel_config=e,this}setOnFormCreation(e){return this.onFormCreation=e,this}setCreateFormEdit(e){return this.createFormEdit=e,this}setCreateFormNew(e){return this.createFormNew=e,this}_getStoreDataCopy(){let e=JSON.parse(JSON.stringify(this.store.getData()));return this.addRelativeInstance.is_active&&(e=this.addRelativeInstance.cleanUp(e)),e=J(e),e}getStoreDataCopy(){return this.exportData()}exportData(){let e=this._getStoreDataCopy();return e=f(e,this.store.state.legacy_format),e}getDataJson(){return JSON.stringify(this.exportData(),null,2)}updateHistory(){this.history&&(this.history.changed(),this.history.controls.updateButtons()),this.onChange&&this.onChange()}setPostSubmit(e){return this.postSubmit=e,this}setOnSubmit(e){return this.onSubmit=e,this}setOnDelete(e){return this.onDelete=e,this}destroy(){return this.history.controls.destroy(),this.history=null,this.formCont.el&&i.select(this.formCont.el).remove(),this.addRelativeInstance.onCancel&&this.addRelativeInstance.onCancel(),this.store.updateTree({}),this}}class ht{constructor(e,t,n={}){this.cont=e,this.options=[],this.onSelect=t,this.config=n,this.autocomplete_cont=i.select(this.cont).append("div").attr("class","f3-autocomplete-cont").node(),this.create()}create(){var e;const t=this;i.select(this.autocomplete_cont).html(`\n
\n
\n \n ${$e()}\n
\n
\n
\n `);const n=i.select(this.autocomplete_cont).select(".f3-autocomplete"),r=n.select("input"),s=n.select(".f3-autocomplete-items");function a(){n.classed("active",!0);const e=r.property("value"),i=t.options.filter(t=>t.label.toLowerCase().includes(e.toLowerCase()));i.forEach(function(t){const n=t.label.toLowerCase().indexOf(e.toLowerCase());t.label_html=-1!==n?t.label.substring(0,n)+""+t.label.substring(n,n+e.length)+""+t.label.substring(n+e.length):t.label}),i.sort(function(e,t){return e.labelt.label?1:0}),d(i)}function o(){n.classed("active",!1),d([])}function d(e){s.selectAll("div.f3-autocomplete-item").data(e,e=>null==e?void 0:e.value).join("div").attr("class","f3-autocomplete-item").on("click",(e,n)=>{t.onSelect(n.value)}).html(e=>e.optionHtml?e.optionHtml(e):function(e){return`
${e.label_html}
`}(e))}n.on("focusout",()=>{setTimeout(()=>{n.node().contains(document.activeElement)||o()},200)}),r.on("focus",()=>{t.options=t.getOptions(),a()}).on("input",a).on("keydown",function(e){const n=s.selectAll("div.f3-autocomplete-item").nodes(),r=n.findIndex(e=>i.select(e).classed("f3-selected"));if("ArrowDown"===e.key){e.preventDefault();a(n,r0?r-1:n.length-1)}else if("Enter"===e.key&&-1!==r){e.preventDefault();const s=i.select(n[r]).datum();s&&t.onSelect(s.value)}function a(e,t){e.forEach(e=>i.select(e).classed("f3-selected",!1)),e[t]&&(i.select(e[t]).classed("f3-selected",!0),e[t].scrollIntoView({block:"nearest"}))}}),s.on("wheel",e=>e.stopPropagation()),n.select(".f3-autocomplete-toggle").on("click",e=>{e.stopPropagation();const t=n.classed("active");if(n.classed("active",!t),t)o();else{r.node().focus(),a()}})}setOptionsGetter(e){return this.getOptions=e,this}setOptionsGetterPerson(e,t){return this.getOptions=()=>{const i=[];return e().forEach(e=>{e.to_add||e.unknown||e._new_rel_data||i.find(t=>t.value===e.id)||i.push({label:t(e),value:e.id,optionHtml:n(e)})}),i},this;function n(t){const n=!V(t,e());return e=>`\n
\n ${we()}\n ${e.label_html}\n ${n?`${ke()}`:""}\n
\n `}}destroy(){this.autocomplete_cont.remove()}}function pt(e){const t=[];return Array.isArray(e)?e.forEach(e=>{"function"==typeof e?t.push(e):"string"==typeof e?t.push(t=>t.data[e]):Array.isArray(e)&&t.push(t=>e.map(e=>t.data[e]).join(" "))}):"function"==typeof e?t.push(e):"string"==typeof e&&t.push(t=>t.data[e]),t}function ft(e,t){return new _t(e,t)}let _t=class{constructor(e,t){return this.cont=e,this.svg=this.cont.querySelector("svg.main_svg"),this.store=t,this.card_display=[e=>`${e.data["first name"]} ${e.data["last name"]}`],this.cardImageField="avatar",this.onCardClick=this.onCardClickDefault,this.style="default",this.mini_tree=!1,this.card_dim={},this}getCard(){return Je({store:this.store,card_display:this.card_display,cardImageField:this.cardImageField,defaultPersonIcon:this.defaultPersonIcon,onCardClick:this.onCardClick,style:this.style,mini_tree:this.mini_tree,onCardUpdate:this.onCardUpdate,card_dim:this.card_dim,empty_card_label:this.store.state.single_parent_empty_card_label||"",unknown_card_label:this.store.state.unknown_card_label||"",cardInnerHtmlCreator:this.cardInnerHtmlCreator,duplicate_branch_toggle:this.store.state.duplicate_branch_toggle,onCardMouseenter:this.onCardMouseenter?this.onCardMouseenter.bind(this):void 0,onCardMouseleave:this.onCardMouseleave?this.onCardMouseleave.bind(this):void 0})}setCardDisplay(e){return this.card_display=pt(e),this}setCardImageField(e){return this.cardImageField=e,this}setDefaultPersonIcon(e){return this.defaultPersonIcon=e,this}setOnCardClick(e){return this.onCardClick=e,this}onCardClickDefault(e,t){this.store.updateMainId(t.data.id),this.store.updateTree({})}setStyle(e){return this.style=e,this}setMiniTree(e){return this.mini_tree=e,this}setOnCardUpdate(e){return this.onCardUpdate=e,this}setCardDim(e){if("object"!=typeof e)return console.error("card_dim must be an object"),this;for(let t in e){const n=e[t];if("number"!=typeof n&&"boolean"!=typeof n)return console.error(`card_dim.${t} must be a number or boolean`),this;"width"===t&&(t="w"),"height"===t&&(t="h"),"img_width"===t&&(t="img_w"),"img_height"===t&&(t="img_h"),"img_x"===t&&(t="img_x"),"img_y"===t&&(t="img_y"),this.card_dim[t]=n}return this}resetCardDim(){return this.card_dim={},this}setCardInnerHtmlCreator(e){return this.cardInnerHtmlCreator=e,this}setOnHoverPathToMain(){return this.onCardMouseenter=this.onEnterPathToMain.bind(this),this.onCardMouseleave=this.onLeavePathToMain.bind(this),this}unsetOnHoverPathToMain(){return this.onCardMouseenter=void 0,this.onCardMouseleave=void 0,this}onEnterPathToMain(e,t){this.to_transition=t.data.id;const n=this.store.getTreeMainDatum(),r=i.select(this.cont).select("div.cards_view").selectAll(".card_cont"),s=i.select(this.cont).select("svg.main_svg .links_view").selectAll(".link"),{cards_node_to_main:a,links_node_to_main:o}=function(e,t,n,i){const r=n.is_ancestry,s=t.data();let a=[],o=[];if(r){const r=[];let c=n,u=0;for(;c!==i&&u<100;){u++;const e=s.find(e=>!0===e.spouse&&(e.source===c||e.target===c));if(e){const t=l(s.filter(t=>Array.isArray(t.target)&&t.target.includes(e.source)&&t.target.includes(e.target)),i);if(!t)break;r.push(e),r.push(t),c=t.source}else{const e=l(s.filter(e=>Array.isArray(e.target)&&e.target.includes(c)),i);if(!e)break;r.push(e),c=e.source}}t.each(function(e){r.includes(e)&&a.push({link:e,node:this})});const h=d(n,r);e.each(function(e){h.includes(e)&&o.push({card:e,node:this})})}else if(n.spouse&&n.spouse.data===i.data){t.each(function(e){e.target===n&&a.push({link:e,node:this})});const r=[i,n];e.each(function(e){r.includes(e)&&o.push({card:e,node:this})})}else if(n.sibling){t.each(function(e){if(!Array.isArray(n.parents))throw new Error("datum.parents is not an array");e.source===n&&a.push({link:e,node:this}),e.source===i&&Array.isArray(e.target)&&2===e.target.length&&a.push({link:e,node:this}),n.parents.includes(e.source)&&!Array.isArray(e.target)&&n.parents.includes(e.target)&&a.push({link:e,node:this})});const r=[i,n,...n.parents||[]];e.each(function(e){r.includes(e)&&o.push({card:e,node:this})})}else{let r=[],l=n,c=0;for(;l!==i&&c<100;){c++;const e=s.find(e=>e.target===l&&Array.isArray(e.source));if(e){const t=s.find(t=>{return!0===t.spouse&&(n=[t.source,t.target],i=e.source,n.every(e=>i.some(t=>e===t)));var n,i});r.push(e),r.push(t),l=t?t.source:e.source[0]}else{const e=s.find(e=>e.target===l&&!Array.isArray(e.source));if(!e)break;r.push(e),l=e.source}}t.each(function(e){r.includes(e)&&a.push({link:e,node:this})});const u=d(i,r);e.each(function(e){u.includes(e)&&o.push({card:e,node:this})})}return{cards_node_to_main:o,links_node_to_main:a};function d(e,t){const r=t.filter(e=>e).reduce((e,t)=>(Array.isArray(t.target)?e.push(...t.target):e.push(t.target),Array.isArray(t.source)?e.push(...t.source):e.push(t.source),e),[]),s=[i,n];return function e(t){t.data.rels.children&&t.data.rels.children.forEach(t=>{const n=r.find(e=>e.data.id===t);n&&(s.push(n),e(n))})}(e),s}function l(e,t){return 0===e.length?null:1===e.length?e[0]:e.find(e=>e.source===t)}}(r,s,t,n);return a.forEach(e=>{const n=200*Math.abs(t.depth-e.card.depth);i.select(e.node.querySelector("div.card-inner")).transition().duration(0).delay(n).on("end",()=>this.to_transition===t.data.id&&i.select(e.node.querySelector("div.card-inner")).classed("f3-path-to-main",!0))}),o.forEach(e=>{const n=200*Math.abs(t.depth-e.link.depth);i.select(e.node).transition().duration(0).delay(n).on("end",()=>this.to_transition===t.data.id&&i.select(e.node).classed("f3-path-to-main",!0))}),this}onLeavePathToMain(e,t){return this.to_transition=!1,i.select(this.cont).select("div.cards_view").selectAll("div.card-inner").classed("f3-path-to-main",!1),i.select(this.cont).select("svg.main_svg .links_view").selectAll(".link").classed("f3-path-to-main",!1),this}};function gt(e,t){return new mt(e,t)}let mt=class{constructor(e,t){return this.cont=e,this.store=t,this.svg=this.cont.querySelector("svg.main_svg"),this.card_dim={w:220,h:70,text_x:75,text_y:15,img_w:60,img_h:60,img_x:5,img_y:5},this.card_display=[],this.mini_tree=!0,this.link_break=!1,this.onCardClick=this.onCardClickDefault.bind(this),this}getCard(){return Ge({store:this.store,svg:this.svg,card_dim:this.card_dim,card_display:this.card_display,mini_tree:this.mini_tree,link_break:this.link_break,onCardClick:this.onCardClick,onCardUpdate:this.onCardUpdate})}setCardDisplay(e){return this.card_display=pt(e),this}setCardDim(e){if("object"!=typeof e)return console.error("card_dim must be an object"),this;for(let t in e){const n=e[t];if("number"!=typeof n&&"boolean"!=typeof n)return console.error(`card_dim.${t} must be a number or boolean`),this;"width"===t&&(t="w"),"height"===t&&(t="h"),"img_width"===t&&(t="img_w"),"img_height"===t&&(t="img_h"),"img_x"===t&&(t="img_x"),"img_y"===t&&(t="img_y"),this.card_dim[t]=n}return function(e,t){e.querySelector("defs#f3CardDef")&&e.querySelector("defs#f3CardDef").remove(),We(e,t)}(this.svg,this.card_dim),this}setOnCardUpdate(e){return this.onCardUpdate=e,this}setMiniTree(e){return this.mini_tree=e,this}setLinkBreak(e){return this.link_break=e,this}onCardClickDefault(e,t){this.store.updateMainId(t.data.id),this.store.updateTree({})}setOnCardClick(e){return this.onCardClick=e,this}};function vt(e,t){return new yt(e,t)}class yt{constructor(e,t){this.getCard=null,this.transition_time=2e3,this.linkSpouseText=null,this.personSearch=null,this.is_card_html=!1,this.beforeUpdate=null,this.afterUpdate=null,this.cont=function(e){"string"==typeof e&&(e=document.querySelector(e));if(!e)throw new Error("cont not found");return e}(e);const{svg:n}=R(this.cont);this.svg=n,function(e){i.select(e).append("div").attr("class","f3-nav-cont")}(this.cont);const r=t&&t.length>0?t[0].id:"";return this.store=this.createStore(t,r),this.setOnUpdate(),this.editTreeInstance=null,this}createStore(e,t){return v({data:e,main_id:t,node_separation:250,level_separation:150,single_parent_empty_card:!0,is_horizontal:!1})}setOnUpdate(){this.store.setOnUpdate(e=>{this.beforeUpdate&&this.beforeUpdate(e),e=Object.assign({transition_time:this.store.state.transition_time},e||{}),this.is_card_html&&(e=Object.assign({},e||{},{cardHtml:!0})),P(this.store.getTree(),this.svg,this.getCard(),e||{}),this.linkSpouseText&&function(e,t,n){const r=[];t.data.forEach(e=>{e.coparent&&"F"===e.data.data.gender&&r.push({nodes:[e,e.coparent],id:`${e.data.id}--${e.coparent.data.id}`}),e.spouses&&e.spouses.forEach(t=>r.push({nodes:[t,e],id:`${t.data.id}--${e.data.id}`}))});const s=i.select(e).select(".links_view").selectAll("g.link-text").data(r,e=>e.id),a=s.exit(),o=s.enter().append("g").attr("class","link-text"),d=o.merge(s),l=(e,t)=>e.spouse&&"F"===e.data.data.gender?e.x-n.node_separation/2:t.spouse&&"M"===t.data.data.gender?t.x+n.node_separation/2:Math.min(e.x,t.x)+n.node_separation/2;a.each(function(e){const t=i.select(this);t.transition("text").duration(100).style("opacity",0).on("end",()=>t.remove())}),o.each(function(e){const[t,n]=e.nodes,r=i.select(this);r.attr("transform",`translate(${l(t,n)}, ${t.y-3})`).style("opacity",0),r.append("text").style("font-size","12px").style("fill","#fff").style("text-anchor","middle")}),d.each(function(e){const[r,s]=e.nodes,a=i.select(this),o=n.initial?c(t,r,n.transition_time):0;a.select("text").text(n.linkSpouseText(r,s)),a.transition("text").duration(n.transition_time).delay(o).attr("transform",`translate(${l(r,s)}, ${r.y-3})`),a.transition("text-op").duration(100).delay(o+n.transition_time).style("opacity",1)})}(this.svg,this.store.getTree(),Object.assign({},e||{},{linkSpouseText:this.linkSpouseText,node_separation:this.store.state.node_separation})),this.afterUpdate&&this.afterUpdate(e)})}updateTree(e={initial:!1}){return this.store.updateTree(e),this}updateData(e){return this.store.updateData(e),this}setCardYSpacing(e){return"number"!=typeof e?(console.error("card_y_spacing must be a number"),this):(this.store.state.level_separation=e,this)}setCardXSpacing(e){return"number"!=typeof e?(console.error("card_x_spacing must be a number"),this):(this.store.state.node_separation=e,this)}setOrientationVertical(){return this.store.state.is_horizontal=!1,this}setOrientationHorizontal(){return this.store.state.is_horizontal=!0,this}setShowSiblingsOfMain(e){return this.store.state.show_siblings_of_main=e,this}setModifyTreeHierarchy(e){return this.store.state.modifyTreeHierarchy=e,this}setPrivateCardsConfig(e){return this.store.state.private_cards_config=e,this}setLinkSpouseText(e){return this.linkSpouseText=e,this}setSingleParentEmptyCard(e,{label:t="Unknown"}={}){return this.store.state.single_parent_empty_card=e,this.store.state.single_parent_empty_card_label=t,this.editTreeInstance&&this.editTreeInstance.addRelativeInstance.is_active&&this.editTreeInstance.addRelativeInstance.onCancel(),W(this.store.getData()||[]),this}setCard(e){if(e===ft)return this.setCardHtml();if(e===gt)return this.setCardSvg();throw new Error("Card must be an instance of cardHtml or cardSvg")}setCardHtml(){const e=this.cont.querySelector("#htmlSvg");if(!e)throw new Error("htmlSvg not found");this.is_card_html=!0,this.svg.querySelector(".cards_view").innerHTML="",e.style.display="block";const t=ft(this.cont,this.store);return this.getCard=()=>t.getCard(),t}setCardSvg(){const e=this.cont.querySelector("#htmlSvg");if(!e)throw new Error("htmlSvg not found");this.is_card_html=!1,this.svg.querySelector(".cards_view").innerHTML="",e.style.display="none";const t=gt(this.cont,this.store);return this.getCard=()=>t.getCard(),t}setTransitionTime(e){return this.store.state.transition_time=e,this}setSortChildrenFunction(e){return this.store.state.sortChildrenFunction=e,this}setSortSpousesFunction(e){return this.store.state.sortSpousesFunction=e,this}setAncestryDepth(e){return this.store.state.ancestry_depth=e,this}setProgenyDepth(e){return this.store.state.progeny_depth=e,this}getMaxDepth(e){return o(e,this.store.getData())}calculateKinships(e,t={}){return Xe(e,this.store.getData(),t)}getKinshipsDataStash(e,t){return rt(e,t,this.store.getData(),this.calculateKinships(e))}setDuplicateBranchToggle(e){return this.store.state.duplicate_branch_toggle=e,this}editTree(){return this.editTreeInstance=(e=this.cont,t=this.store,new ut(e,t));var e,t}updateMain(e){let t;return t=e.id?e.id:e.data.id,this.store.updateMainId(t),this.store.updateTree({}),this}updateMainId(e){return this.store.updateMainId(e),this}getMainDatum(){return this.store.getMainDatum()}setBeforeUpdate(e){return this.beforeUpdate=e,this}setAfterUpdate(e){return this.afterUpdate=e,this}setPersonDropdown(e,{cont:t=this.cont.querySelector(".f3-nav-cont"),onSelect:n,placeholder:i="Search"}={}){return n||(n=function(e){const t=this.store.getDatum(e);if(!t)throw new Error("Datum not found");this.editTreeInstance&&this.editTreeInstance.open(t);this.updateMainId(e),this.updateTree({initial:!1})}.bind(this)),this.personSearch=function(e,t,n={}){return new ht(e,t,n)}(t,n,{placeholder:i}),this.personSearch.setOptionsGetterPerson(this.store.getData,e),this}unSetPersonSearch(){return this.personSearch.destroy(),this.personSearch=null,this}}function wt(e){return(e=e[0].toUpperCase()+e.slice(1)).includes("great-")&&(e=e.replace("great-","Great-")),e}var bt=Object.freeze({__proto__:null,Card:Ke,CardHtml:Je,CardSvg:Ge,appendElement:Ze,infoPopup:Ye,kinshipInfo:function(e,t,n){const{self_id:r,getLabel:s,title:a}=e,o=Xe(r,n,e),d=o[t];if(!d)return;let l=d;l="self"===d?"You":wt(l);const c=`\n
\n
\n ${a}\n \n ${l}\n ${Ee()}\n \n
\n
\n `,u=i.create("div").html(c).select("div").node();let h=null;return i.select(u).select(".f3-kinship-info-icon").on("click",e=>function(e,a){const d=250,l=400;let c=e.clientX-d-10,u=e.clientY-l-10;c+d>window.innerWidth&&(c=window.innerWidth-d-10);u<0&&(u=10);if(h&&h.active)return h.close(),void(h=null);h=Ye(a),i.select(h.popup_cont).style("width",`${d}px`).style("height",`${l}px`).style("left",`${c}px`).style("top",`${u}px`);const p=h.popup_cont.querySelector(".f3-popup-content-inner");h.activate(),function(e,t,n,r,s,a){i.select(s).select("#SmallChart").node()||i.select(s).append("div").attr("id","SmallChart").attr("class","f3");const o=i.select("#SmallChart");o.selectAll("*").remove();const d=rt(e,t,n,r);let l=!0;const c=o.append("div");function u(e){const n=vt("#SmallChart",e).setTransitionTime(500).setCardXSpacing(170).setCardYSpacing(70).setSingleParentEmptyCard(!1);function r(e){let n="self"===e.data.kinship?"You":e.data.kinship;return n=wt(n),l||(n=a(e.data)),`\n
\n
${n}
\n
\n `;function i(){return"self"===e.data.kinship?"card-kinship-self"+(l?"":" f3-real-label"):e.data.id===t?"card-kinship-rel":"card-kinship-default"}}function s(){c.classed("f3-kinship-labels-toggle",!0),c.append("label").text("Kinship labels").append("input").attr("type","checkbox").attr("checked",!0).on("change",e=>{l=!l,n.updateTree({initial:!1,tree_position:"inherit"})})}function o(e){const t=n.cont.querySelector("svg.main_svg");$(t).k>e&&k(t,e)}n.setCardHtml().setStyle("rect").setCardInnerHtmlCreator(e=>r(e)).setOnCardUpdate(function(e){i.select(this).select(".card").classed("card-main",!1)}).onCardClick=(e,t)=>{},n.updateTree({initial:!0}),setTimeout(()=>o(.65),100),s()}u(d)}(r,t,n,o,p,s)}(e,u)),u}});const Ct=gt,xt=ft,$t=Object.assign({},F,{setupHtmlSvg:function(e){i.select(e()).append("div").attr("class","cards_view_fake").style("display","none")},setupReactiveTreeData:T,getUniqueId:function(e){return e.unique_id}});var kt=Object.freeze({__proto__:null,CalculateTree:g,Card:Ke,CardHtml:xt,CardHtmlClass:_t,CardSvg:Ct,CardSvgClass:mt,calculateTree:m,cardHtml:ft,cardSvg:gt,createChart:vt,createStore:v,createSvg:A,elements:bt,formatData:p,formatDataForExport:f,handlers:je,htmlHandlers:$t,icons:Ie,view:P});e.CalculateTree=g,e.Card=Ke,e.CardHtml=xt,e.CardHtmlClass=_t,e.CardSvg=Ct,e.CardSvgClass=mt,e.calculateTree=m,e.cardHtml=ft,e.cardSvg=gt,e.createChart=vt,e.createStore=v,e.createSvg=A,e.default=kt,e.elements=bt,e.formatData=p,e.formatDataForExport=f,e.handlers=je,e.htmlHandlers=$t,e.icons=Ie,e.view=P,Object.defineProperty(e,"__esModule",{value:!0})}); diff --git a/dist/styles/family-chart.css b/dist/styles/family-chart.css new file mode 100644 index 00000000..64e9c330 --- /dev/null +++ b/dist/styles/family-chart.css @@ -0,0 +1,880 @@ +.f3 { + --female-color: rgb(196, 138, 146); + --male-color: rgb(120, 159, 172); + --genderless-color: lightgray; + --background-color: rgb(33, 33, 33); + --text-color: #fff; + + font-family: 'Roboto', sans-serif; +} + +.f3 * { + box-sizing: border-box; +} + +.f3 .cursor-pointer { + cursor: pointer; +} +.f3 svg.main_svg { + width: 100%; + height: 100%; +} +.f3 svg.main_svg text { + fill: currentColor; +} +.f3 rect.card-female, .f3 .card-female .card-body-rect, .f3 .card-female .text-overflow-mask { + fill: var(--female-color); +} +.f3 rect.card-male, .f3 .card-male .card-body-rect, .f3 .card-male .text-overflow-mask { + fill: var(--male-color); +} +.f3 .card-genderless .card-body-rect, .f3 .card-genderless .text-overflow-mask { + fill: var(--genderless-color); +} +.f3 .card_add .card-body-rect { + fill: #3b5560; + stroke-width: 4px; + stroke: #fff; + cursor: pointer; +} +.f3 g.card_add text { + fill: #fff; +} +.f3 .card-main-outline { + stroke: currentColor; + stroke-width: 3px; +} +.f3 .card_family_tree rect { + transition: 0.3s; +} +.f3 .card_family_tree:hover rect { + transform: scale(1.1); +} +.f3 .card_add_relative { + cursor: pointer; + color: #fff; + transition: 0.3s; +} +.f3 .card_add_relative circle { + fill: rgba(0, 0, 0, 0); +} +.f3 .card_add_relative:hover { + color: black; +} +.f3 .card_edit.pencil_icon { + color: #fff; + transition: 0.3s; +} +.f3 .card_edit.pencil_icon:hover { + color: black; +} +.f3 .card_break_link, .f3 .link_upper, .f3 .link_lower, .f3 .link_particles { + transform-origin: 50% 50%; + transition: 1s; +} +.f3 .card_break_link { + color: #fff; +} +.f3 .card_break_link.closed .link_upper { + transform: translate(-140.5px, 655.6px); +} +.f3 .card_break_link.closed .link_upper g { + transform: rotate(-58deg); +} +.f3 .card_break_link.closed .link_particles { + transform: scale(0); +} +.f3 .input-field input { + height: 2.5rem !important; +} +.f3 .input-field > label:not(.label-icon).active { + -webkit-transform: translateY(-8px) scale(0.8); + transform: translateY(-8px) scale(0.8); +} +.f3.f3-cont { + width:100%; + height:900px; + max-height:70vh; + background-color: var(--background-color); + color: var(--text-color); +} +.f3 { + position: relative; + display: flex; +} + + + + +/* form-info */ +.f3-form input[type="text"], +.f3-form textarea, +.f3-form select { + width: 100%; + padding: 8px 12px; + margin: 8px 0; + border: 1px solid #ddd; + border-radius: 4px; + box-sizing: border-box; + font-size: 14px; + background: var(--background-color); + color: currentColor; +} + +.f3-form input[type="text"]:focus, +.f3-form textarea:focus, +.f3-form select:focus { + box-shadow: 0 0 5px rgba(76, 175, 80, 0.2); +} + +.f3-form button { + padding: 10px 20px; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; + margin: 10px 0; + transition: background-color 0.3s ease-in-out, border-color 0.3s ease-in-out, color 0.3s ease-in-out; +} + +.f3-form button[type="submit"] { + background-color: #4CAF50; + color: white; +} + +.f3-cancel-btn { + background-color: #ccc; +} + +.f3-form .f3-delete-btn { + background-color: transparent; + border: 1px solid #f44336; + color: #f44336; + width: 100%; + padding: 5px 10px; +} + +.f3-delete-btn:hover { + background-color: #da190b; + border-color: #da190b; + color: #fff; +} + +.f3-delete-btn:disabled { + opacity: 0.5; + background-color: transparent; + color: #f44336; + cursor: not-allowed; +} + +.f3-form .f3-remove-relative-btn { + background-color: transparent; + border: 1px solid currentColor; + color: currentColor; + width: 100%; + padding: 5px 10px; +} + +.f3-remove-relative-btn:hover, .f3-remove-relative-btn.active { + background-color: var(--text-color); + border-color: var(--text-color); + color: var(--background-color); +} + +.f3-radio-group { + margin: 15px 0; +} + +.f3-radio-group label { + margin-right: 15px; + cursor: pointer; +} + +.f3-radio-group input[type="radio"] { + margin-right: 5px; +} + +.f3-info-field-label, .f3-form-field label { + font-weight: bold; + font-size: 12px; + display: block; + opacity: 0.8; +} + +.f3-info-field-value { + font-weight: normal; + display: block; + border: none; + outline: none; + border-bottom: 1px solid rgba(255,255,255,0.2); + padding-bottom: 1px; + margin-bottom: 10px; + min-height: 18px; +} + +.f3-form-buttons { + text-align: right; +} + +.f3-form-title { + text-align: center; +} + +.f3-form.non-editable .f3-form-buttons, +.f3-form.non-editable .f3-delete-btn, +.f3-form.non-editable .f3-remove-relative-btn, +.f3-form.non-editable .f3-link-existing-relative { + display: none; +} + +.f3-close-btn { + cursor: pointer; + position: absolute; + left: 10px; + top: 8px; + font-size: 30px; + color: var(--text-color); +} + +.f3-edit-btn { + position: relative; + top: -1px; + width: 24px; + height: 24px; + cursor: pointer; + display: inline-block; +} + +.f3-add-relative-btn { + cursor: pointer; + width: 27px; + height: 27px; + margin-right: 5px; + display: inline-block; +} + +/* card-html */ + +.f3 div.card { + cursor: pointer; + color: var(--text-color); + position: relative; + line-height: 1.2; +} + +.f3 div.card-image-circle { + border-radius: 50%; + padding: 5px; + width: 90px; + height: 90px; +} + +.f3 div.card-image-circle div.card-label { + position: absolute; + bottom: -10px; + left: 50%; + transform: translate(-50%, 50%); + max-width: 150%; + min-height: 22px; + text-align: center; + background-color: rgba(0, 0, 0, 0.5); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + border-radius: 3px; + padding: 0 5px; +} + +.f3 div.card-image-circle img { + width: 100%; + height: 100%; + border-radius: 50%; + object-fit: cover; +} + +.f3 div.card-image-circle svg { + width: 100%; + height: 100%; + padding: 5px; + border-radius: 50%; + object-fit: cover; +} + +.f3 div.card-image-circle img { + width: 100%; + height: 100%; + border-radius: 50%; + object-fit: cover; +} + +.f3 div.card-rect { + padding: 5px; + border-radius: 3px; + width: 120px; + min-height: 70px; + overflow: hidden; + text-align: center; + display: flex; + flex-direction: column; + justify-content: center; +} + + +.f3 div.card-image-rect { + width: 200px; + min-height: 70px; + display: flex; + align-items: center; + border-radius: 5px; +} + +.f3 div.card-image-rect .person-icon { + height: 70px; + width: 70px; + object-fit: cover; + flex: 0 0 auto; + padding: 5px; + margin-right: 10px; +} + +.f3 div.card-image-rect img { + height: 70px; + width: 70px; + object-fit: cover; + flex: 0 0 auto; + padding: 5px; + margin-right: 10px; + border-radius: 8px; +} + +.f3 div.card-image-rect svg { + object-fit: cover; + width: 100%; + height: 100%; + padding: 5px; + border-radius: 7px; +} + +.f3 div.card-image-rect div.card-label { + height: 100%; + overflow: hidden; + display: flex; + flex-direction: column; + justify-content: center; +} + +.f3 div.mini-tree { + text-align: right; + position: absolute; + top: -15px; + right: -2px; + z-index: -1; +} +.f3 div.mini-tree svg { + width: 55px; +} + +.f3 .f3-card-duplicate-tag { + position: absolute; + top: 2px; + right: 2px; + color: rgb(255, 251, 220); + background-color: rgba(255, 251, 220, 0); + border-radius: 50%; + padding: 2px; + transition: color 0.2s ease-in-out, background-color 0.2s ease-in-out; +} + +.f3 .f3-card-duplicate-hover div.card-inner { + transform: translate(0, -2px); + outline: 4px solid rgb(255, 251, 220); +} + +.f3 .f3-card-duplicate-hover .f3-card-duplicate-tag { + background-color: rgba(255, 251, 220, .8); + color: #000; +} + +.f3 .f3-remove-relative-active .card { + background-color: var(--background-color); +} + +.f3 .f3-remove-relative-active .card-inner { + transition: border 0.2s ease-in-out, opacity 0.2s ease-in-out, transform 0.2s ease-in-out; + opacity: .75; +} + +.f3 .f3-remove-relative-active .card:hover .card-inner { + opacity: .25; +} + +.f3 .f3-remove-relative-active .card-male.card-depth--1:hover .card-inner { + transform: translate(-8px, -8px); +} + +.f3 .f3-remove-relative-active .card.card-female.card-depth--1:hover .card-inner { + transform: translate(8px, -8px); +} + +.f3 .f3-remove-relative-active .card.card-female.card-depth-0:hover .card-inner { + transform: translate(8px, 0); +} + +.f3 .f3-remove-relative-active .card.card-male.card-depth-0:hover .card-inner { + transform: translate(-8px, 0); +} + +.f3 .f3-remove-relative-active .card.card-depth-1:hover .card-inner { + transform: translate(0, 8px); +} + +.f3 .f3-remove-relative-active .card.card-main .card-inner { + transform: translate(0, 0)!important; + opacity: 1!important; +} + + + +.f3 div.card > div { + transition: transform 0.2s ease-in-out; + box-shadow: 0 0 0 0 rgba(0, 0, 0, 0.8); +} + +.f3 .card-inner { + outline: 0px solid rgba(255, 255, 255, 1); + transition: outline 0.5s ease-in-out; +} + +.f3 div.card-female .card-inner, .f3 div.card-female .person-icon svg { + background-color: var(--female-color); +} +.f3 div.card-male .card-inner, .f3 div.card-male .person-icon svg { + background-color: var(--male-color); +} +.f3 div.card-genderless .card-inner, .f3 div.card-genderless .person-icon svg { + background-color: var(--genderless-color); +} + +.f3 div.card-new-rel .card-inner, .f3 div.card-new-rel .person-icon svg { + background-color: var(--background-color); +} + +.f3 div.card-to-add .card-inner { + background-color: var(--background-color); + border: 1px solid; +} + +.f3 div.card-to-add .card-inner .card-label { + margin: 0 auto; +} + +.f3 div.card-to-add .person-icon { + display: none; +} + +.f3 div.card-new-rel .card-inner { + border-width: 1px; + border-style: dashed; + outline: 0px !important; +} +.f3 div.card-new-rel.card-female .card-inner, .f3 div.card-to-add.card-female .card-inner { + border-color: var(--female-color); + color: var(--female-color); +} +.f3 div.card-new-rel.card-male .card-inner, .f3 div.card-to-add.card-male .card-inner { + color: var(--male-color); + border-color: var(--male-color); +} + +.f3 div.card-unknown .card-inner { + background-color: var(--background-color); + border: 1px solid; +} + +.f3 div.card-unknown .card-inner .card-label { + margin: 0 auto; +} + +.f3 div.card-unknown .person-icon { + display: none; +} + +.f3 div.card-new-rel .card-inner { + border-width: 1px; + border-style: dashed; + outline: 0px !important; +} +.f3 div.card-new-rel.card-female .card-inner, .f3 div.card-unknown.card-female .card-inner { + border-color: var(--female-color); + color: var(--female-color); +} +.f3 div.card-new-rel.card-male .card-inner, .f3 div.card-unknown.card-male .card-inner { + color: var(--male-color); + border-color: var(--male-color); +} + +.f3 div.card:hover > div { + transform: translate(0, -2px); +} +.f3 div.card-main .card-inner, .f3 div.card:hover .card-inner { + box-shadow: 0 0 20px 0 rgba(0, 0, 0, 0.8); +} + +.f3 div.card-main .card-inner { + outline: 4px solid rgba(220, 220, 220, 1); +} + +.f3 div.card-inner.f3-path-to-main { + outline: 4px solid rgba(255, 255, 255, 1); +} + +.f3 .link { + transition: stroke-width 0.2s ease-in-out; +} + +.f3 .link.f3-path-to-main { + stroke-width: 4px; +} + + + + + +.f3-form-cont { + position: relative; + z-index: 6; + right: 0; + top: 0; + width: 0; + height: 100%; + background-color: var(--background-color); + overflow: auto; + flex: 0 0 auto; + box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.5); +} + +.f3-form-cont.opened { + width: 350px; +} + +.f3-form { + padding: 20px; +} + +.f3-form hr { + border-style: solid; + border-width: thin 0 0 0; + opacity: 0.15; +} + +.f3-nav-cont { + position: absolute; + top: 0; + left: 0; + width: 100%; + display: flex; +} + +.f3-history-controls { + padding: 8px 5px 7px 9px; + display: inline-block; + position: relative; + z-index: 2; +} + +.f3-back-button, .f3-forward-button { + width: 30px; + height: 30px; + transition: opacity 0.3s ease; + cursor: pointer; + display: inline-block; + background-color: transparent; + border: none; + margin-right: 10px; + color: currentColor; +} + +.f3-history-controls svg { + height: 100%; +} + +.f3-back-button.disabled, .f3-forward-button.disabled { + opacity: 0.5; +} + +.f3-modal { + display: none; + position: absolute; + z-index: 10; + left: 0; + top: 0; + width: 100%; + height: 100%; + overflow: auto; + background-color: rgb(0,0,0); + background-color: rgba(0,0,0,0.4); +} + +.f3-modal-content { + position: relative; + background-color: var(--background-color); + margin: 15% auto; + padding: 20px; + border: 1px solid #888; + border-radius: 5px; + width: 500px; + max-width: 90%; +} + +.f3-modal-close { + color: #aaa; + position: absolute; + right: 10px; + top: 7px; + font-size: 28px; + font-weight: bold; +} + +.f3-modal-close:hover, +.f3-modal-close:focus { + color: black; + text-decoration: none; + cursor: pointer; +} + +.f3-popup { + position: fixed; + box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.8); +} + +.f3-popup-content { + position: relative; + background-color: var(--background-color); + border: 1px solid #888; + border-radius: 5px; + overflow: hidden; + width: 100%; + height: 100%; +} + +.f3-popup-nav { + height: 20px; +} + +.f3-popup-content-inner { + width: 100%; + height: 100%; +} + +.f3-popup-close { + color: #aaa; + position: absolute; + z-index: 4; + right: 6px; + top: 1px; + font-size: 28px; + font-weight: bold; + line-height: 1; +} + +.f3-popup-close:hover, +.f3-popup-close:focus { + color: black; + text-decoration: none; + cursor: pointer; +} + + + +.f3-btn { + position: relative; + cursor: pointer; + padding: 5px 10px; + + overflow: hidden; + + border-width: 0; + outline: none; + border-radius: 3px; + box-shadow: 0 1px 4px rgba(0, 0, 0, .6); + + background-color: var(--text-color); + color: var(--background-color); + + transition: background-color .3s; + font-size: 14px; +} + +.f3-btn:hover, .f3-btn:focus { + background-color: var(--background-color); + color: var(--text-color); +} + +.f3-female-bg { + background-color: var(--female-color); +} + +.f3-male-bg { + background-color: var(--male-color); +} + +.f3-genderless-bg { + background-color: var(--genderless-color); +} + +.f3-female-color { + color: var(--female-color); +} + +.f3-male-color { + color: var(--male-color); +} + +.f3-genderless-color { + color: var(--genderless-color); +} + +.f3-autocomplete-cont { + position: relative; + display: inline-block; + z-index: 2; + font-size: 14px; + width: 200px; +} + +.f3-autocomplete input { + border: 1px solid rgba(255, 255, 255, 0.2); + background-color: var(--background-color); + color: var(--text-color); + padding: 10px; + width: 100%; +} +.f3-autocomplete input:focus { + outline: none; +} + +.f3-autocomplete-toggle { + position: absolute; + right: 10px; + top: 10px; + cursor: pointer; + color: var(--text-color); + transition: color 0.3s ease-in-out; + width: 20px; +} + +.f3-autocomplete-items { + border: 1px solid rgba(255, 255, 255, 0.2); + border-top: none; + overflow-y: auto; + max-height: 0; + background-color: var(--background-color); + transition: max-height 0.3s ease-in-out; +} + +.f3-autocomplete.active .f3-autocomplete-items { + max-height: 300px; +} + +.f3-autocomplete-item > div { + padding: 10px; + cursor: pointer; + background-color: var(--background-color); + border-bottom: 1px solid rgba(255, 255, 255, 0.2); + transition: background-color 0.3s ease-in-out, color 0.3s ease-in-out; +} +.f3-autocomplete-item > div:hover, .f3-autocomplete-item.f3-selected > div { + background-color: var(--text-color); + color: var(--background-color); +} + +.f3-autocomplete-active { + background-color: DodgerBlue !important; + color: #ffffff; +} + +.f3-kinship-info { + padding: 10px 20px; +} + +.f3-kinship-info .f3-info-field { + color:#b3b01e +} + +.f3-kinship-info-icon { + cursor:pointer; + display:inline-block; + width:18px; + height:18px; + color:#04a4f4; + position:relative; + top:4px; + left:2px; +} + +.f3-kinship-info .f3 { + width:100%; + height: 100%; + position:relative; + background-color:rgb(33,33,33); + color:#fff; +} + +.f3 .f3-kinship-info .card-kinship-self { + min-height: 0px; + width: 60px; + height: 60px; + border-radius: 50%; + background-color: var(--background-color) !important; + border: solid 3px; + color: #437fae; + font-weight: bold; +} + +.f3 .f3-kinship-info .card-kinship-self.f3-real-label { + width: 150px; + height: 50px; + border-radius: 50px; +} + +.f3 .f3-kinship-info .card-kinship-rel { + min-height: 0px; + width: 150px; + height: 50px; + border-radius: 50px; + background-color: #1d3456 !important; + font-weight: bold; +} + +.f3 .f3-kinship-info .card-kinship-default { + min-height: 0px; + width: 150px; + height: 50px; + border-radius: 50px; + background-color: var(--background-color) !important; + border: solid 1px; +} + +.f3-kinship-labels-toggle { + position: absolute; + top: 0; + left: 0; + z-index: 10; + font-size: 12px; +} + +.f3-kinship-labels-toggle label { + cursor: pointer; + color: #fff; + font-weight: bold; + text-align: center; + padding: 2px 5px; +} + +.f3-kinship-labels-toggle input[type="checkbox"] { + cursor: pointer; + margin-left: 5px; + margin-right: 5px; + margin-top: 5px; + margin-bottom: 5px; +} \ No newline at end of file diff --git a/dist/types/core/add-relative.d.ts b/dist/types/core/add-relative.d.ts new file mode 100644 index 00000000..5a5c00ac --- /dev/null +++ b/dist/types/core/add-relative.d.ts @@ -0,0 +1,38 @@ +import { Data, Datum } from "../types/data"; +import { Store } from "../types/store"; +declare const _default: (store: Store, onActivate: () => void, cancelCallback: (datum: Datum) => void) => AddRelative; +export default _default; +export declare class AddRelative { + store: Store; + onActivate: () => void; + cancelCallback: (datum: Datum) => void; + datum: Datum | null; + onChange: ((updated_datum: Datum, props: any) => void) | null; + onCancel: (() => void) | null; + is_active: boolean; + addRelLabels: { + father: string; + mother: string; + spouse: string; + son: string; + daughter: string; + }; + canAdd?: (datum: Datum) => { + parent?: boolean; + spouse?: boolean; + child?: boolean; + }; + constructor(store: Store, onActivate: () => void, cancelCallback: (datum: Datum) => void); + activate(datum: Datum): void; + setAddRelLabels(add_rel_labels: AddRelative['addRelLabels']): this | undefined; + setCanAdd(canAdd: AddRelative['canAdd']): this; + addRelLabelsDefault(): { + father: string; + mother: string; + spouse: string; + son: string; + daughter: string; + }; + getStoreData(): Data; + cleanUp(data?: Data | undefined): Data; +} diff --git a/dist/types/core/cards/card-html.d.ts b/dist/types/core/cards/card-html.d.ts new file mode 100644 index 00000000..dea34f7d --- /dev/null +++ b/dist/types/core/cards/card-html.d.ts @@ -0,0 +1,51 @@ +import { Store } from "../../types/store"; +import { Datum } from "../../types/data"; +import { TreeDatum } from "../../types/treeData"; +export default function CardHtmlWrapper(cont: HTMLElement, store: Store): CardHtml; +/** + * CardHtml class - Handles HTML-based card rendering and customization for family tree nodes. + * + * @example + * ```typescript + * import * as f3 from 'family-chart' + * const f3Chart = f3.createChart('#FamilyChart', data) + * const f3Card = f3Chart.setCardHtml() // returns a CardHtml instance + * .setCardDisplay([["first name","last name"],["birthday"]]); + * ``` + */ +export declare class CardHtml { + cont: HTMLElement; + svg: SVGElement; + store: Store; + card_display: any; + cardImageField: string; + onCardClick: any; + style: 'default' | 'imageCircleRect' | 'imageCircle' | 'imageRect' | 'rect'; + mini_tree: boolean; + onCardUpdate: any; + card_dim: { + [key: string]: number | boolean; + }; + cardInnerHtmlCreator: undefined | ((d: TreeDatum) => string); + defaultPersonIcon: undefined | ((d: TreeDatum) => string); + onCardMouseenter: undefined | ((e: Event, d: TreeDatum) => void); + onCardMouseleave: undefined | ((e: Event, d: TreeDatum) => void); + to_transition: Datum['id'] | undefined | false; + constructor(cont: HTMLElement, store: Store); + getCard(): (d: TreeDatum) => void; + setCardDisplay(card_display: CardHtml['card_display']): this; + setCardImageField(cardImageField: CardHtml['cardImageField']): this; + setDefaultPersonIcon(defaultPersonIcon: CardHtml['defaultPersonIcon']): this; + setOnCardClick(onCardClick: CardHtml['onCardClick']): this; + onCardClickDefault(e: MouseEvent, d: TreeDatum): void; + setStyle(style: CardHtml['style']): this; + setMiniTree(mini_tree: CardHtml['mini_tree']): this; + setOnCardUpdate(onCardUpdate: CardHtml['onCardUpdate']): this; + setCardDim(card_dim: CardHtml['card_dim']): this; + resetCardDim(): this; + setCardInnerHtmlCreator(cardInnerHtmlCreator: CardHtml['cardInnerHtmlCreator']): this; + setOnHoverPathToMain(): this; + unsetOnHoverPathToMain(): this; + onEnterPathToMain(e: Event, datum: TreeDatum): this; + onLeavePathToMain(e: Event, d: TreeDatum): this; +} diff --git a/dist/types/core/cards/card-svg.d.ts b/dist/types/core/cards/card-svg.d.ts new file mode 100644 index 00000000..84a8e31a --- /dev/null +++ b/dist/types/core/cards/card-svg.d.ts @@ -0,0 +1,24 @@ +import { Store } from "../../types/store"; +import { TreeDatum } from "../../types/treeData"; +import { CardDim } from "../../renderers/card-svg/templates"; +export default function CardSvgWrapper(cont: HTMLElement, store: Store): CardSvg; +export declare class CardSvg { + cont: HTMLElement; + store: Store; + svg: SVGElement; + card_dim: CardDim; + card_display: any; + mini_tree: boolean; + link_break: boolean; + onCardClick: (e: MouseEvent, d: TreeDatum) => void; + onCardUpdate: ((d: TreeDatum) => void) | undefined; + constructor(cont: HTMLElement, store: Store); + getCard(): (d: TreeDatum) => void; + setCardDisplay(card_display: CardSvg['card_display']): this; + setCardDim(card_dim: CardSvg['card_dim']): this; + setOnCardUpdate(onCardUpdate: CardSvg['onCardUpdate']): this; + setMiniTree(mini_tree: CardSvg['mini_tree']): this; + setLinkBreak(link_break: CardSvg['link_break']): this; + onCardClickDefault(e: MouseEvent, d: TreeDatum): void; + setOnCardClick(onCardClick: CardSvg['onCardClick']): this; +} diff --git a/dist/types/core/cards/utils.d.ts b/dist/types/core/cards/utils.d.ts new file mode 100644 index 00000000..be3caf89 --- /dev/null +++ b/dist/types/core/cards/utils.d.ts @@ -0,0 +1 @@ +export declare function processCardDisplay(card_display: any): any[]; diff --git a/dist/types/core/chart.d.ts b/dist/types/core/chart.d.ts new file mode 100644 index 00000000..f39557b7 --- /dev/null +++ b/dist/types/core/chart.d.ts @@ -0,0 +1,259 @@ +import { EditTree } from "./edit"; +import { Data, Datum } from "../types/data"; +import { Store } from "../types/store"; +import * as ST from "../types/store"; +import { CardHtml } from "../core/cards/card-html"; +import { CardSvg } from "../core/cards/card-svg"; +import { TreeDatum } from "../types/treeData"; +import { ViewProps } from "../renderers/view"; +import { KinshipInfoConfig } from "../features/kinships/calculate-kinships"; +type LinkSpouseText = ((sp1: TreeDatum, sp2: TreeDatum) => string) | null; +export default function createChart(cont: HTMLElement | string, data: Data): Chart; +/** + * Main Chart class - The primary class for creating and managing family tree visualizations. + * + * This is the main entry point for the Family Chart library. Use this class to: + * - Create and configure family tree visualizations + * - Set up data, styling, and interaction options + * - Control tree layout, orientation, and display settings + * - Manage user interactions and updates + * + * @example + * ```typescript + * const f3Chart = createChart('#FamilyChart', data) // returns a Chart instance; + * ``` + */ +export declare class Chart { + cont: HTMLElement; + store: Store; + svg: SVGElement; + getCard: null | (() => (d: TreeDatum) => void); + is_card_html: boolean; + transition_time: number; + linkSpouseText: LinkSpouseText | null; + personSearch: any; + beforeUpdate: Function | null; + afterUpdate: Function | null; + editTreeInstance: EditTree | null; + constructor(cont: HTMLElement | string, data: Data); + private createStore; + private setOnUpdate; + /** + * Update the tree + * @param props - The properties to update the tree with. + * @param props.initial - Whether to update the tree initially. + * @param props.tree_position - The position of the tree. + * - 'fit' to fit the tree to the container, + * - 'main_to_middle' to center the tree on the main person, + * - 'inherit' to inherit the position from the previous update. + * @param props.transition_time - The transition time. + * @returns The CreateChart instance + */ + updateTree(props?: ViewProps): this; + /** + * Update the data + * @param data - The data to update the tree with. + * @returns The CreateChart instance + */ + updateData(data: Data): this; + /** + * Set the card y spacing + * @param card_y_spacing - The card y spacing between the cards. Level separation. + * @returns The CreateChart instance + */ + setCardYSpacing(card_y_spacing: ST.LevelSeparation): this; + /** + * Set the card x spacing + * @param card_x_spacing - The card x spacing between the cards. Node separation. + * @returns The CreateChart instance + */ + setCardXSpacing(card_x_spacing: ST.NodeSeparation): this; + /** + * Set the orientation to vertical + * @returns The CreateChart instance + */ + setOrientationVertical(): this; + /** + * Set the orientation to horizontal + * @returns The CreateChart instance + */ + setOrientationHorizontal(): this; + /** + * Set whether to show the siblings of the main person + * @param show_siblings_of_main - Whether to show the siblings of the main person. + * @returns The CreateChart instance + */ + setShowSiblingsOfMain(show_siblings_of_main: ST.ShowSiblingsOfMain): this; + /** + * set function that will modify the tree hierarchy. it can be used to delete or add cards in the tree. + * @param modifyTreeHierarchy - function that will modify the tree hierarchy. + * @returns The CreateChart instance + */ + setModifyTreeHierarchy(modifyTreeHierarchy: ST.ModifyTreeHierarchy): this; + /** + * Set the private cards config + * @param private_cards_config - The private cards config. + * @param private_cards_config.condition - The condition to check if the card is private. + * - Example: (d: Datum) => d.data.living === true + * @returns The CreateChart instance + */ + setPrivateCardsConfig(private_cards_config: ST.PrivateCardsConfig): this; + /** + * Option to set text on spouse links + * @param linkSpouseText - The function to set the text on the spouse links. + * - Example: (sp1, sp2) => getMarriageDate(sp1, sp2) + * @returns The CreateChart instance + */ + setLinkSpouseText(linkSpouseText: LinkSpouseText): this; + /** + * Set whether to show the single parent empty card + * @param single_parent_empty_card - Whether to show the single parent empty card. + * @param label - The label to display for the single parent empty card. + * @returns The CreateChart instance + */ + setSingleParentEmptyCard(single_parent_empty_card: boolean, { label }?: { + label?: string | undefined; + }): this; + /** + * Set the Card creation function + * @param Card - The card function. + * @returns The CreateChart instance + */ + setCard(card: (cont: HTMLElement, store: Store) => CardHtml | CardSvg): CardHtml | CardSvg; + /** + * Set the Card HTML function + * @returns The CardHtml instance + */ + setCardHtml(): CardHtml; + /** + * Set the Card SVG function + * @returns The CardSvg instance + */ + setCardSvg(): CardSvg; + /** + * Set the transition time + * @param transition_time - The transition time in milliseconds + * @returns The CreateChart instance + */ + setTransitionTime(transition_time: ST.TransitionTime): this; + /** + * Set the sort children function + * @param sortChildrenFunction - The sort children function. + * - Example: (a, b) => a.data.birth_date - b.data.birth_date + * @returns The CreateChart instance + */ + setSortChildrenFunction(sortChildrenFunction: ST.SortChildrenFunction): this; + /** + * Set the sort spouses function + * @param sortSpousesFunction - The sort spouses function. + * - Example: + * (d, data) => { + * const spouses = d.data.rels.spouses || [] + * return spouses.sort((a, b) => { + * const sp1 = data.find(d0 => d0.id === a) + * const sp2 = data.find(d0 => d0.id === b) + * if (!sp1 || !sp2) return 0 + * return getMarriageDate(d, sp1) - getMarriageDate(d, sp2) + * }) + * }) + * } + * @returns The CreateChart instance + */ + setSortSpousesFunction(sortSpousesFunction: ST.SortSpousesFunction): this; + /** + * Set how many generations to show in the ancestry + * @param ancestry_depth - The number of generations to show in the ancestry. + * @returns The CreateChart instance + */ + setAncestryDepth(ancestry_depth: ST.AncestryDepth): this; + /** + * Set how many generations to show in the progeny + * @param progeny_depth - The number of generations to show in the progeny. + * @returns The CreateChart instance + */ + setProgenyDepth(progeny_depth: ST.ProgenyDepth): this; + /** + * Get the max depth of a person in the ancestry and progeny + * @param d_id - The id of the person to get the max depth of. + * @returns The max depth of the person in the ancestry and progeny. {ancestry: number, progeny: number} + */ + getMaxDepth(d_id: Datum['id']): { + ancestry: number; + progeny: number; + }; + /** + * Calculate the kinships of a person + * @param d_id - The id of the person to calculate the kinships of. + * @param config - The config for the kinships. + * @param config.show_in_law - Whether to show in law relations. + * @returns The kinships of the person. + */ + calculateKinships(d_id: Datum['id'], config?: KinshipInfoConfig): import("../features/kinships/calculate-kinships").Kinships; + /** + * Get the kinships data stash with which we can create small family tree with relatives that connects 2 people + * @param main_id - The id of the main person. + * @param rel_id - The id of the person to get the kinships of. + * @returns The kinships data stash. + */ + getKinshipsDataStash(main_id: Datum['id'], rel_id: Datum['id']): void | import("../features/kinships/kinships-data").DatumKinship[]; + /** + * Set whether to show toggable tree branches are duplicated + * @param duplicate_branch_toggle - Whether to show toggable tree branches are duplicated. + * @returns The CreateChart instance + */ + setDuplicateBranchToggle(duplicate_branch_toggle: ST.DuplicateBranchToggle): this; + /** + * Initialize the edit tree + * @returns The edit tree instance. + */ + editTree(): EditTree; + /** + * Update the main person + * @param d - New main person. + * @returns The CreateChart instance + */ + updateMain(d: Datum): this; + /** + * Update the main person + * @param id - New main person id. + * @returns The CreateChart instance + */ + updateMainId(id: Datum['id']): this; + /** + * Get the main person + * @returns The main person. + */ + getMainDatum(): Datum; + /** + * Set the before update of the tree. + * @param fn - The function to call before the update. + * @returns The CreateChart instance + */ + setBeforeUpdate(fn: Function): this; + /** + * Set the after update of the tree. + * @param fn - The function to call after the update. + * @returns The CreateChart instance + */ + setAfterUpdate(fn: Function): this; + /** + * Set the person dropdown + * @param getLabel - The function to get the label of the person to show in the dropdown. + * @param config - The config for the person dropdown. + * @param config.cont - The container to put the dropdown in. Default is the .f3-nav-cont element. + * @param config.onSelect - The function to call when a person is selected. Default is setting clicked person as main person and updating the tree. + * @param config.placeholder - The placeholder for the search input. Default is 'Search'. + * @returns The CreateChart instance + */ + setPersonDropdown(getLabel: Function, { cont, onSelect, placeholder }?: { + cont?: HTMLElement; + onSelect?: (d_id: Datum['id']) => void; + placeholder?: string; + }): this; + /** + * Unset the person dropdown + * @returns The CreateChart instance + */ + unSetPersonSearch(): this; +} +export {}; diff --git a/dist/types/core/edit.d.ts b/dist/types/core/edit.d.ts new file mode 100644 index 00000000..9a5ba965 --- /dev/null +++ b/dist/types/core/edit.d.ts @@ -0,0 +1,132 @@ +import { HistoryWithControls } from "../features/history"; +import { RemoveRelative } from "./remove-relative"; +import { Modal } from "../features/modal"; +import { Store } from "../types/store"; +import { Data, Datum } from "../types/data"; +import { AddRelative } from "./add-relative"; +import { FormCreator, FormCreatorSetupProps } from "../types/form"; +import { CardHtml } from "./cards/card-html"; +import { CardSvg } from "./cards/card-svg"; +import { LegacyDatum } from "../store/format-data"; +type Card = CardHtml | CardSvg; +declare const _default: (cont: HTMLElement, store: Store) => EditTree; +export default _default; +/** + * EditTree class - Provides comprehensive editing capabilities for family tree data. + * + * This class handles all editing operations for family tree data, including: + * - Adding new family members and relationships + * - Editing existing person information + * - Removing family members and relationships + * - Form management and validation + * - History tracking and undo/redo functionality + * - Modal dialogs and user interactions + * + * @example + * ```typescript + * import * as f3 from 'family-chart' + * const f3Chart = f3.createChart('#FamilyChart', data) + * const f3EditTree = f3Chart.editTree() // returns an EditTree instance + * .setFields(["first name","last name","birthday"]) + * .setOnChange(() => { + * const updated_data = f3EditTree.exportData() + * // do something with the updated data + * }) + * ``` + */ +export declare class EditTree { + cont: HTMLElement; + store: Store; + fields: { + type: string; + label: string; + id: string; + }[]; + formCont: { + el?: HTMLElement; + populate: (form_element: HTMLElement) => void; + open: () => void; + close: () => void; + }; + is_fixed: boolean; + no_edit: boolean; + onChange: (() => void) | null; + editFirst: boolean; + postSubmit: ((datum: Datum, data: Data) => void) | null; + link_existing_rel_config?: FormCreatorSetupProps['link_existing_rel_config']; + onFormCreation: null | ((props: { + cont: HTMLElement; + form_creator: FormCreator; + }) => void); + addRelativeInstance: AddRelative; + removeRelativeInstance: RemoveRelative; + history: HistoryWithControls; + modal: Modal; + createFormEdit: ((form_creator: FormCreator, closeCallback: () => void) => HTMLElement) | null; + createFormNew: ((form_creator: FormCreator, closeCallback: () => void) => HTMLElement) | null; + onSubmit: FormCreatorSetupProps['onSubmit']; + onDelete: FormCreatorSetupProps['onDelete']; + canEdit: FormCreatorSetupProps['canEdit']; + canDelete: FormCreatorSetupProps['canDelete']; + constructor(cont: HTMLElement, store: Store); + /** + * Open the edit form + * @param datum - The datum to edit + */ + open(datum: Datum): void; + private setupAddRelative; + private setupRemoveRelative; + private createHistory; + /** + * Open the edit form without canceling the add relative or remove relative view + * @param datum - The datum to edit + */ + openWithoutRelCancel(datum: Datum): void; + private getFormContDefault; + setFormCont(formCont: EditTree['formCont']): this; + cardEditForm(datum: Datum): void; + openForm(): void; + closeForm(): void; + fixed(): this; + absolute(): this; + setCardClickOpen(card: Card): this; + openFormWithId(d_id: Datum['id']): void; + setNoEdit(): this; + setEdit(): this; + setFields(fields: any[]): this; + /** + * Set the onChange function to be called when the data changes via editing, adding, or removing a relative + * @param fn - The onChange function + */ + setOnChange(fn: EditTree['onChange']): this; + setCanEdit(canEdit: EditTree['canEdit']): this; + setCanDelete(canDelete: EditTree['canDelete']): this; + setCanAdd(canAdd: AddRelative['canAdd']): this; + addRelative(datum: Datum | undefined): this; + setupModal(): Modal; + setEditFirst(editFirst: EditTree['editFirst']): this; + isAddingRelative(): boolean; + isRemovingRelative(): boolean; + setAddRelLabels(add_rel_labels: AddRelative['addRelLabels']): this; + setLinkExistingRelConfig(link_existing_rel_config: EditTree['link_existing_rel_config']): this; + setOnFormCreation(onFormCreation: EditTree['onFormCreation']): this; + setCreateFormEdit(createFormEdit: EditTree['createFormEdit']): this; + setCreateFormNew(createFormNew: EditTree['createFormNew']): this; + private _getStoreDataCopy; + /** + * deprecated: use exportData instead. This function will be removed in a future version. + * Export the data + * @returns family chart data + */ + getStoreDataCopy(): Data | LegacyDatum[]; + /** + * @returns family chart data + */ + exportData(): Data | LegacyDatum[]; + getDataJson(): string; + updateHistory(): void; + setPostSubmit(postSubmit: EditTree['postSubmit']): this; + setOnSubmit(onSubmit: EditTree['onSubmit']): this; + setOnDelete(onDelete: EditTree['onDelete']): this; + destroy(): this; +} diff --git a/dist/types/core/form-creator.d.ts b/dist/types/core/form-creator.d.ts new file mode 100644 index 00000000..547da026 --- /dev/null +++ b/dist/types/core/form-creator.d.ts @@ -0,0 +1,2 @@ +import { FormCreatorSetupProps, FormCreator } from "../types/form"; +export declare function formCreatorSetup({ datum, store, fields, postSubmitHandler, addRelative, removeRelative, deletePerson, onCancel, editFirst, link_existing_rel_config, onFormCreation, no_edit, onSubmit, onDelete, canEdit, canDelete, }: FormCreatorSetupProps): FormCreator; diff --git a/dist/types/core/remove-relative.d.ts b/dist/types/core/remove-relative.d.ts new file mode 100644 index 00000000..6fa07fa5 --- /dev/null +++ b/dist/types/core/remove-relative.d.ts @@ -0,0 +1,18 @@ +import { TreeDatum } from "../types/treeData"; +import { Store } from "../types/store"; +import { Datum } from "../types/data"; +import { Modal } from "../features/modal"; +declare const _default: (store: RemoveRelative["store"], onActivate: RemoveRelative["onActivate"], cancelCallback: RemoveRelative["cancelCallback"], modal: RemoveRelative["modal"]) => RemoveRelative; +export default _default; +export declare class RemoveRelative { + store: Store; + onActivate: () => void; + cancelCallback: (datum: Datum) => void; + modal: Modal; + datum: Datum | null; + onChange: ((rel_tree_datum: TreeDatum, onAccept: () => void) => void) | null; + onCancel: (() => void) | null; + is_active: boolean; + constructor(store: RemoveRelative['store'], onActivate: RemoveRelative['onActivate'], cancelCallback: RemoveRelative['cancelCallback'], modal: RemoveRelative['modal']); + activate(datum: Datum): void; +} diff --git a/dist/types/elements.d.ts b/dist/types/elements.d.ts new file mode 100644 index 00000000..e39a416e --- /dev/null +++ b/dist/types/elements.d.ts @@ -0,0 +1,6 @@ +export * from './renderers/card-svg/elements'; +export { default as CardHtml } from './renderers/card-html'; +export { default as CardSvg } from './renderers/card-svg/card-svg'; +export { Card } from './renderers/card-svg/card-svg'; +export { default as infoPopup } from './features/info-popup'; +export { kinshipInfo } from './features/kinships/kinship-info'; diff --git a/dist/types/exports.d.ts b/dist/types/exports.d.ts new file mode 100644 index 00000000..eecb0757 --- /dev/null +++ b/dist/types/exports.d.ts @@ -0,0 +1,30 @@ +export type * from './types/index'; +export { default as createStore } from "./store/store"; +export { default as view } from "./renderers/view"; +export { default as createSvg } from "./renderers/svg"; +export * as handlers from './handlers'; +export * as elements from './elements'; +export * as icons from './renderers/icons'; +export { default as createChart } from './core/chart'; +export { default as cardSvg } from './core/cards/card-svg'; +export { default as cardHtml } from './core/cards/card-html'; +export { formatData, formatDataForExport } from "./store/format-data"; +export { CalculateTree } from "./layout/calculate-tree"; +export { calculateTreeWithV1Data as calculateTree } from "./layout/calculate-tree"; +export { Card } from './renderers/card-svg/card-svg'; +import cardSvg from './core/cards/card-svg'; +import cardHtml from './core/cards/card-html'; +/** @deprecated Use cardSvg instead. This export will be removed in a future version. */ +export declare const CardSvg: typeof cardSvg; +/** @deprecated Use cardHtml instead. This export will be removed in a future version. */ +export declare const CardHtml: typeof cardHtml; +export { CardHtml as CardHtmlClass } from './core/cards/card-html'; +export { CardSvg as CardSvgClass } from './core/cards/card-svg'; +import * as htmlHandlers from './renderers/html'; +import { setupHtmlSvg, getUniqueId } from './features/card-component/handlers'; +declare const htmlHandlersWithDeprecated: typeof htmlHandlers & { + setupHtmlSvg: typeof setupHtmlSvg; + setupReactiveTreeData: (getHtmlSvg: () => HTMLElement) => (new_tree_data: import("./types/treeData").TreeDatum[]) => import("./types/treeData").TreeDatum[]; + getUniqueId: typeof getUniqueId; +}; +export { htmlHandlersWithDeprecated as htmlHandlers }; diff --git a/dist/types/features/autocomplete.d.ts b/dist/types/features/autocomplete.d.ts new file mode 100644 index 00000000..f783d8b1 --- /dev/null +++ b/dist/types/features/autocomplete.d.ts @@ -0,0 +1,27 @@ +import { Datum } from "../types/data"; +export default function (cont: Autocomplete['cont'], onSelect: Autocomplete['onSelect'], config?: Autocomplete['config']): Autocomplete; +interface AutocompleteOption { + label: string; + value: string; + optionHtml: (d: AutocompleteOption) => string; + label_html?: string; + class?: string; +} +declare class Autocomplete { + cont: HTMLElement; + autocomplete_cont: HTMLElement; + options: AutocompleteOption[]; + onSelect: (value: string) => void; + config?: { + placeholder?: string; + }; + getOptions?: () => Autocomplete['options']; + constructor(cont: HTMLElement, onSelect: (value: string) => void, config?: { + placeholder?: string; + }); + create(): void; + setOptionsGetter(getOptions: () => Autocomplete['options']): this; + setOptionsGetterPerson(getData: () => Datum[], getLabel: (d: Datum) => string): this; + destroy(): void; +} +export {}; diff --git a/dist/types/features/card-component/card-component.d.ts b/dist/types/features/card-component/card-component.d.ts new file mode 100644 index 00000000..ab181222 --- /dev/null +++ b/dist/types/features/card-component/card-component.d.ts @@ -0,0 +1,3 @@ +import { Tree } from "../../layout/calculate-tree"; +import { ViewProps } from "../../renderers/view"; +export default function updateCardsComponent(svg: SVGElement, tree: Tree, Card: any, props?: ViewProps): void; diff --git a/dist/types/features/card-component/handlers.d.ts b/dist/types/features/card-component/handlers.d.ts new file mode 100644 index 00000000..07ae4cb9 --- /dev/null +++ b/dist/types/features/card-component/handlers.d.ts @@ -0,0 +1,11 @@ +import { TreeDatum } from "../../types/treeData"; +export default function cardComponentSetup(cont: HTMLElement): (new_tree_data: TreeDatum[]) => TreeDatum[]; +declare function setupReactiveTreeData(getHtmlSvg: () => HTMLElement): (new_tree_data: TreeDatum[]) => TreeDatum[]; +export declare function getCardsViewFake(getHtmlSvg: () => HTMLElement): HTMLElement; +/** @deprecated This export will be removed in a future version. Use setupReactiveTreeData instead. */ +export declare function setupHtmlSvg(getHtmlSvg: () => HTMLElement): void; +/** @deprecated This export will be removed in a future version. Use setupReactiveTreeData instead. */ +declare const _setupReactiveTreeData: typeof setupReactiveTreeData; +export { _setupReactiveTreeData as setupReactiveTreeData }; +/** @deprecated This export will be removed in a future version. Use setupReactiveTreeData instead. */ +export declare function getUniqueId(d: any): any; diff --git a/dist/types/features/duplicates-toggle/duplicates-ancestry.d.ts b/dist/types/features/duplicates-toggle/duplicates-ancestry.d.ts new file mode 100644 index 00000000..e5dfe0d1 --- /dev/null +++ b/dist/types/features/duplicates-toggle/duplicates-ancestry.d.ts @@ -0,0 +1 @@ +export function handleDuplicateHierarchyAncestry(root: any, on_toggle_one_close_others?: boolean): void; diff --git a/dist/types/features/duplicates-toggle/duplicates-progeny.d.ts b/dist/types/features/duplicates-toggle/duplicates-progeny.d.ts new file mode 100644 index 00000000..bc5f3a4e --- /dev/null +++ b/dist/types/features/duplicates-toggle/duplicates-progeny.d.ts @@ -0,0 +1,2 @@ +export function handleDuplicateSpouseToggle(tree: any): void; +export function handleDuplicateHierarchyProgeny(root: any, data_stash: any, on_toggle_one_close_others?: boolean): void; diff --git a/dist/types/features/duplicates-toggle/duplicates-toggle-renderer.d.ts b/dist/types/features/duplicates-toggle/duplicates-toggle-renderer.d.ts new file mode 100644 index 00000000..3ac55c97 --- /dev/null +++ b/dist/types/features/duplicates-toggle/duplicates-toggle-renderer.d.ts @@ -0,0 +1 @@ +export function handleCardDuplicateToggle(node: any, d: any, is_horizontal: any, updateTree: any): void; diff --git a/dist/types/features/history.d.ts b/dist/types/features/history.d.ts new file mode 100644 index 00000000..e6882bf5 --- /dev/null +++ b/dist/types/features/history.d.ts @@ -0,0 +1,20 @@ +import { Store } from "../types/store"; +import { Data } from "../types/data"; +export interface History { + changed: () => void; + back: () => void; + forward: () => void; + canForward: () => boolean; + canBack: () => boolean; +} +export interface HistoryControls { + back_btn: HTMLElement; + forward_btn: HTMLElement; + updateButtons: () => void; + destroy: () => void; +} +export interface HistoryWithControls extends History { + controls: HistoryControls; +} +export declare function createHistory(store: Store, getStoreDataCopy: () => Data, onUpdate: () => void): History; +export declare function createHistoryControls(cont: HTMLElement, history: History): HistoryControls; diff --git a/dist/types/features/info-popup.d.ts b/dist/types/features/info-popup.d.ts new file mode 100644 index 00000000..5b4d3240 --- /dev/null +++ b/dist/types/features/info-popup.d.ts @@ -0,0 +1,12 @@ +export default function (cont: HTMLElement, onClose?: () => void): InfoPopup; +export declare class InfoPopup { + cont: HTMLElement; + popup_cont: HTMLElement; + active: boolean; + onClose?: () => void; + constructor(cont: HTMLElement, onClose?: () => void); + create(): void; + activate(content?: HTMLElement): void; + open(): void; + close(): void; +} diff --git a/dist/types/features/kinships/calculate-kinships.d.ts b/dist/types/features/kinships/calculate-kinships.d.ts new file mode 100644 index 00000000..9d68b540 --- /dev/null +++ b/dist/types/features/kinships/calculate-kinships.d.ts @@ -0,0 +1,17 @@ +import { Datum, Data } from "../../types/data"; +import { DatumKinship } from "./kinships-data"; +export interface KinshipInfoConfig { + self_id?: Datum['id']; + getLabel?: (d: DatumKinship) => string; + title?: string; + show_in_law?: boolean; +} +export interface Kinships { + [key: Datum['id']]: string; +} +export declare function calculateKinships(d_id: Datum['id'], data_stash: Data, kinship_info_config: KinshipInfoConfig): Kinships; +export declare function findSameAncestor(main_id: Datum['id'], rel_id: Datum['id'], data_stash: Data): { + found: string | string[]; + is_ancestor: undefined; + is_half_kin: undefined; +} | null; diff --git a/dist/types/features/kinships/kinship-info.d.ts b/dist/types/features/kinships/kinship-info.d.ts new file mode 100644 index 00000000..1bec3508 --- /dev/null +++ b/dist/types/features/kinships/kinship-info.d.ts @@ -0,0 +1,3 @@ +import { KinshipInfoConfig } from './calculate-kinships'; +import { Datum, Data } from '../../types/data'; +export declare function kinshipInfo(kinship_info_config: KinshipInfoConfig, rel_id: Datum['id'], data_stash: Data): HTMLElement | undefined; diff --git a/dist/types/features/kinships/kinships-data.d.ts b/dist/types/features/kinships/kinships-data.d.ts new file mode 100644 index 00000000..87f2c026 --- /dev/null +++ b/dist/types/features/kinships/kinships-data.d.ts @@ -0,0 +1,6 @@ +import { Datum, Data } from "../../types/data"; +import { Kinships } from "./calculate-kinships"; +export interface DatumKinship extends Datum { + kinship?: string; +} +export declare function getKinshipsDataStash(main_id: Datum['id'], rel_id: Datum['id'], data_stash: Data, kinships: Kinships): void | DatumKinship[]; diff --git a/dist/types/features/link-spouse-text.d.ts b/dist/types/features/link-spouse-text.d.ts new file mode 100644 index 00000000..b0b3a5e9 --- /dev/null +++ b/dist/types/features/link-spouse-text.d.ts @@ -0,0 +1,10 @@ +import { Tree } from "../layout/calculate-tree"; +import { TreeDatum } from "../types/treeData"; +interface LinkSpouseTextProps { + node_separation: number; + initial?: boolean; + transition_time?: number; + linkSpouseText: (sp1: TreeDatum, sp2: TreeDatum) => string; +} +export default function linkSpouseText(svg: SVGElement, tree: Tree, props: LinkSpouseTextProps): void; +export {}; diff --git a/dist/types/features/modal.d.ts b/dist/types/features/modal.d.ts new file mode 100644 index 00000000..dbba6639 --- /dev/null +++ b/dist/types/features/modal.d.ts @@ -0,0 +1,17 @@ +export default function (cont: HTMLElement): Modal; +export declare class Modal { + cont: HTMLElement; + modal_cont: HTMLElement; + active: boolean; + onClose: (() => void) | null; + constructor(cont: HTMLElement); + create(): void; + activate(content: string | HTMLElement, { boolean, onAccept, onCancel }?: { + boolean?: boolean; + onAccept?: () => void; + onCancel?: () => void; + }): void; + reset(): void; + open(): void; + close(): void; +} diff --git a/dist/types/handlers.d.ts b/dist/types/handlers.d.ts new file mode 100644 index 00000000..a8eadfa5 --- /dev/null +++ b/dist/types/handlers.d.ts @@ -0,0 +1,10 @@ +export * from './handlers/general'; +export * from './renderers/card-svg/methods'; +export * from './store/new-person'; +export * from './handlers/check-person-connection'; +export * from './store/edit'; +export * from './renderers/create-form'; +export * from './features/history'; +export * from './handlers/view-handlers'; +export { default as htmlContSetup } from './renderers/html'; +export { default as cardComponentSetup } from './features/card-component/handlers'; diff --git a/dist/types/handlers/check-person-connection.d.ts b/dist/types/handlers/check-person-connection.d.ts new file mode 100644 index 00000000..8c99c8ed --- /dev/null +++ b/dist/types/handlers/check-person-connection.d.ts @@ -0,0 +1,3 @@ +import { Datum } from "../types/data"; +export declare function checkIfRelativesConnectedWithoutPerson(datum: Datum, data_stash: Datum[]): boolean; +export declare function checkIfConnectedToFirstPerson(datum: Datum, data_stash: Datum[], exclude_ids?: Datum['id'][]): boolean; diff --git a/dist/types/handlers/general.d.ts b/dist/types/handlers/general.d.ts new file mode 100644 index 00000000..56c016a1 --- /dev/null +++ b/dist/types/handlers/general.d.ts @@ -0,0 +1,8 @@ +import { TreeDatum } from "../types/treeData"; +import { Tree } from "../layout/calculate-tree"; +export declare function isAllRelativeDisplayed(d: TreeDatum, data: TreeDatum[]): boolean; +export declare function calculateDelay(tree: Tree, d: { + depth: number; + is_ancestry?: boolean; + spouse?: boolean; +} | TreeDatum, transition_time: number): number; diff --git a/dist/types/handlers/view-handlers.d.ts b/dist/types/handlers/view-handlers.d.ts new file mode 100644 index 00000000..c5333167 --- /dev/null +++ b/dist/types/handlers/view-handlers.d.ts @@ -0,0 +1,46 @@ +import * as d3 from "d3"; +import { TreeDatum } from "../types/treeData"; +type SvgDim = { + width: number; + height: number; +}; +type TreeDim = { + width: number; + height: number; + x_off: number; + y_off: number; +}; +interface TreeFitProps { + svg: SVGElement; + svg_dim: SvgDim; + tree_dim: TreeDim; + transition_time?: number; +} +export declare function treeFit({ svg, svg_dim, tree_dim, transition_time }: TreeFitProps): void; +export declare function calculateTreeFit(svg_dim: SvgDim, tree_dim: TreeDim): { + k: number; + x: number; + y: number; +}; +type CardToMiddleProps = { + datum: TreeDatum; + svg: SVGElement; + svg_dim: SvgDim; + scale?: number; + transition_time?: number; +}; +export declare function cardToMiddle({ datum, svg, svg_dim, scale, transition_time }: CardToMiddleProps): void; +type ManualZoomProps = { + amount: number; + svg: SVGElement; + transition_time?: number; +}; +export declare function manualZoom({ amount, svg, transition_time }: ManualZoomProps): void; +export declare function getCurrentZoom(svg: SVGElement): d3.ZoomTransform; +export declare function zoomTo(svg: SVGElement, zoom_level: number): void; +export interface ZoomProps { + onZoom?: (e: any) => void; + zoom_polite?: boolean; +} +export declare function setupZoom(el: any, props?: ZoomProps): void; +export {}; diff --git a/dist/types/index.d.ts b/dist/types/index.d.ts new file mode 100644 index 00000000..d4b56fd1 --- /dev/null +++ b/dist/types/index.d.ts @@ -0,0 +1,3 @@ +export * from './exports'; +import * as exports from './exports'; +export default exports; diff --git a/dist/types/layout/calculate-tree.d.ts b/dist/types/layout/calculate-tree.d.ts new file mode 100644 index 00000000..e7115a8f --- /dev/null +++ b/dist/types/layout/calculate-tree.d.ts @@ -0,0 +1,54 @@ +import * as d3 from "d3"; +import type { Datum, Data } from "../types/data"; +import type { TreeData } from "../types/treeData"; +interface HN extends d3.HierarchyNode { +} +export interface CalculateTreeOptions { + main_id?: string | null; + node_separation?: number; + level_separation?: number; + single_parent_empty_card?: boolean; + is_horizontal?: boolean; + one_level_rels?: boolean; + sortChildrenFunction?: ((a: Datum, b: Datum) => number) | undefined; + sortSpousesFunction?: ((d: Datum, data: Data) => void) | undefined; + ancestry_depth?: number | undefined; + progeny_depth?: number | undefined; + show_siblings_of_main?: boolean; + modifyTreeHierarchy?: (tree: HN, is_ancestry: boolean) => void; + private_cards_config?: any; + duplicate_branch_toggle?: boolean; + on_toggle_one_close_others?: boolean; +} +export interface Tree { + data: TreeData; + data_stash: Data; + dim: { + width: number; + height: number; + x_off: number; + y_off: number; + }; + main_id: string; + is_horizontal: boolean; +} +export default function calculateTree(data: Data, { main_id, node_separation, level_separation, single_parent_empty_card, is_horizontal, one_level_rels, sortChildrenFunction, sortSpousesFunction, ancestry_depth, progeny_depth, show_siblings_of_main, modifyTreeHierarchy, private_cards_config, duplicate_branch_toggle, on_toggle_one_close_others, }: CalculateTreeOptions): Tree; +/** + * Calculate the tree + * @param options - The options for the tree + * @param options.data - The data for the tree + * @returns The tree + * @deprecated Use f3.calculateTree instead + */ +export declare function CalculateTree(options: CalculateTreeOptions & { + data: Data; +}): Tree; +import { LegacyDatum } from "../store/format-data"; +/** + * Calculate the tree with v1 data + * @param data - The data for the tree + * @param options - The options for the tree + * @returns The tree + */ +export declare function calculateTreeWithV1Data(data: LegacyDatum[], options: CalculateTreeOptions): Tree; +export {}; diff --git a/dist/types/layout/create-links.d.ts b/dist/types/layout/create-links.d.ts new file mode 100644 index 00000000..02ab1d40 --- /dev/null +++ b/dist/types/layout/create-links.d.ts @@ -0,0 +1,13 @@ +import { TreeDatum } from "../types/treeData"; +export interface Link { + d: [number, number][]; + _d: () => [number, number][]; + curve: boolean; + id: string; + depth: number; + is_ancestry: boolean | undefined; + source: TreeDatum | TreeDatum[]; + target: TreeDatum | TreeDatum[]; + spouse?: boolean; +} +export declare function createLinks(d: TreeDatum, is_horizontal?: boolean): Link[]; diff --git a/dist/types/layout/handlers.d.ts b/dist/types/layout/handlers.d.ts new file mode 100644 index 00000000..78136a32 --- /dev/null +++ b/dist/types/layout/handlers.d.ts @@ -0,0 +1,23 @@ +import { TreeDatum } from "../types/treeData"; +import { Data, Datum } from "../types/data"; +import { CalculateTreeOptions } from "./calculate-tree"; +export declare function sortChildrenWithSpouses(children: Datum[], datum: Datum, data: Data): Datum[] | undefined; +export declare function sortAddNewChildren(children: Datum[]): Datum[]; +export declare function calculateEnterAndExitPositions(d: TreeDatum, entering: boolean, exiting: boolean): void; +export declare function setupSiblings({ tree, data_stash, node_separation, sortChildrenFunction }: { + tree: TreeDatum[]; + data_stash: Data; + node_separation: number; + sortChildrenFunction: CalculateTreeOptions['sortChildrenFunction']; +}): void; +export declare function handlePrivateCards({ tree, data_stash, private_cards_config }: { + tree: TreeDatum[]; + data_stash: Data; + private_cards_config: { + condition: (d: Datum) => boolean; + }; +}): void; +export declare function getMaxDepth(d_id: Datum['id'], data_stash: Data): { + ancestry: number; + progeny: number; +}; diff --git a/dist/types/layout/path-to-main.d.ts b/dist/types/layout/path-to-main.d.ts new file mode 100644 index 00000000..0c330174 --- /dev/null +++ b/dist/types/layout/path-to-main.d.ts @@ -0,0 +1,13 @@ +import { TreeDatum } from "../types/treeData"; +import { Link } from "./create-links"; +import { CardHtmlSelection, LinkSelection } from "../types/view"; +export default function pathToMain(cards: CardHtmlSelection, links: LinkSelection, datum: TreeDatum, main_datum: TreeDatum): { + cards_node_to_main: { + card: TreeDatum; + node: HTMLDivElement; + }[]; + links_node_to_main: { + link: Link; + node: SVGPathElement; + }[]; +}; diff --git a/dist/types/renderers/card-html.d.ts b/dist/types/renderers/card-html.d.ts new file mode 100644 index 00000000..e7977870 --- /dev/null +++ b/dist/types/renderers/card-html.d.ts @@ -0,0 +1,20 @@ +import { Store } from "../types/store"; +import { TreeDatum } from "../types/treeData"; +import { CardDim } from "../types/card"; +export default function CardHtml(props: { + style: 'default' | 'imageCircleRect' | 'imageCircle' | 'imageRect' | 'rect'; + cardInnerHtmlCreator?: (d: TreeDatum) => string; + onCardClick: (e: Event, d: TreeDatum) => void; + onCardUpdate: (d: TreeDatum) => void; + onCardMouseenter?: (e: Event, d: TreeDatum) => void; + onCardMouseleave?: (e: Event, d: TreeDatum) => void; + mini_tree: boolean; + card_dim: CardDim; + defaultPersonIcon?: (d: TreeDatum) => string; + empty_card_label: string; + unknown_card_label: string; + cardImageField: string; + card_display: ((d: TreeDatum['data']) => string)[]; + duplicate_branch_toggle?: boolean; + store: Store; +}): (this: HTMLElement, d: TreeDatum) => void; diff --git a/dist/types/renderers/card-svg/card-svg.d.ts b/dist/types/renderers/card-svg/card-svg.d.ts new file mode 100644 index 00000000..1ebcf633 --- /dev/null +++ b/dist/types/renderers/card-svg/card-svg.d.ts @@ -0,0 +1,24 @@ +import { TreeDatum } from "../../types/treeData"; +import { CardDim } from "./templates"; +import { Store } from "../../types/store"; +interface CardSvgProps { + store: Store; + svg: SVGElement; + card_dim: CardDim; + card_display: (data: TreeDatum['data']) => string; + onCardClick: (e: MouseEvent, d: TreeDatum) => void; + img?: boolean; + mini_tree?: boolean; + link_break?: boolean; + onMiniTreeClick?: (e: MouseEvent, d: TreeDatum) => void; + onLineBreakClick?: (e: MouseEvent, d: TreeDatum) => void; + onCardUpdate?: (d: TreeDatum) => void; +} +export default function CardSvg(props: CardSvgProps): (this: HTMLElement, d: TreeDatum) => void; +/** + * @deprecated Use cardSvg instead. This export will be removed in a future version. + */ +export declare function Card(props: CardSvgProps & { + store: Store; +}): (this: HTMLElement, d: TreeDatum) => void; +export {}; diff --git a/dist/types/renderers/card-svg/defs.d.ts b/dist/types/renderers/card-svg/defs.d.ts new file mode 100644 index 00000000..ecf14bbf --- /dev/null +++ b/dist/types/renderers/card-svg/defs.d.ts @@ -0,0 +1,3 @@ +import { CardDim } from "./templates"; +export default function setupCardSvgDefs(svg: SVGElement, card_dim: CardDim): void; +export declare function updateCardSvgDefs(svg: SVGElement, card_dim: CardDim): void; diff --git a/dist/types/renderers/card-svg/elements.d.ts b/dist/types/renderers/card-svg/elements.d.ts new file mode 100644 index 00000000..cfffaaaa --- /dev/null +++ b/dist/types/renderers/card-svg/elements.d.ts @@ -0,0 +1,25 @@ +import { Store } from "../../types/store"; +import { TreeDatum } from "../../types/treeData"; +import { CardDim } from "./templates"; +declare const CardElements: { + miniTree: typeof miniTree; + cardBody: typeof cardBody; + cardImage: typeof cardImage; +}; +export default CardElements; +declare function miniTree(d: TreeDatum, props: { + card_dim: CardDim; + onMiniTreeClick?: (e: MouseEvent, d: TreeDatum) => void; + store: Store; +}): Element | null | undefined; +declare function cardBody(d: TreeDatum, props: { + card_dim: CardDim; + onCardClick: (e: MouseEvent, d: TreeDatum) => void; + store: Store; + card_display: (data: TreeDatum['data']) => string; +}): Element | null; +declare function cardImage(d: TreeDatum, props: { + card_dim: CardDim; + store: Store; +}): Element | null | undefined; +export declare function appendElement(el_maybe: Element, parent: Element, is_first?: boolean): void; diff --git a/dist/types/renderers/card-svg/methods.d.ts b/dist/types/renderers/card-svg/methods.d.ts new file mode 100644 index 00000000..e872848a --- /dev/null +++ b/dist/types/renderers/card-svg/methods.d.ts @@ -0,0 +1,5 @@ +import { Store } from "../../types/store"; +import { TreeDatum } from "../../types/treeData"; +export declare function cardChangeMain(store: Store, { d }: { + d: TreeDatum; +}): boolean; diff --git a/dist/types/renderers/card-svg/templates.d.ts b/dist/types/renderers/card-svg/templates.d.ts new file mode 100644 index 00000000..093af17e --- /dev/null +++ b/dist/types/renderers/card-svg/templates.d.ts @@ -0,0 +1,73 @@ +import { TreeDatum } from "../../types/treeData"; +export interface CardDim { + w: number; + h: number; + text_x: number; + text_y: number; + img_w: number; + img_h: number; + img_x: number; + img_y: number; +} +export declare function CardBody({ d, card_dim, card_display }: { + d: TreeDatum; + card_dim: CardDim; + card_display: (data: TreeDatum['data']) => string; +}): { + template: string; +}; +export declare function CardBodyAddNewRel({ d, card_dim, label }: { + d: TreeDatum; + card_dim: CardDim; + label: string; +}): { + template: string; +}; +export declare function CardText({ d, card_dim, card_display }: { + d: TreeDatum; + card_dim: CardDim; + card_display: (data: TreeDatum['data']) => string; +}): { + template: string; +}; +export declare function CardBodyOutline({ d, card_dim, is_new }: { + d: TreeDatum; + card_dim: CardDim; + is_new: boolean; +}): { + template: string; +}; +export declare function MiniTree({ d, card_dim }: { + d: TreeDatum; + card_dim: CardDim; +}): { + template: string; +}; +export declare function LinkBreakIcon({ x, y, rt, closed }: { + x: number; + y: number; + rt: number; + closed: boolean; +}): { + template: string; +}; +export declare function LinkBreakIconWrapper({ d, card_dim }: { + d: TreeDatum; + card_dim: CardDim; +}): { + template: string; +}; +export declare function CardImage({ d, image, card_dim, maleIcon, femaleIcon }: { + d: TreeDatum; + image: string; + card_dim: CardDim; + maleIcon?: ({ card_dim }: { + card_dim: CardDim; + }) => string; + femaleIcon?: ({ card_dim }: { + card_dim: CardDim; + }) => string; +}): { + template: string; +}; +export declare function appendTemplate(template: string, parent: Element, is_first: boolean): void; diff --git a/dist/types/renderers/create-form-html.d.ts b/dist/types/renderers/create-form-html.d.ts new file mode 100644 index 00000000..3f1cdba0 --- /dev/null +++ b/dist/types/renderers/create-form-html.d.ts @@ -0,0 +1,3 @@ +import { EditDatumFormCreator, NewRelFormCreator } from '../types/form'; +export declare function getHtmlNew(form_creator: NewRelFormCreator): string; +export declare function getHtmlEdit(form_creator: EditDatumFormCreator): string; diff --git a/dist/types/renderers/create-form.d.ts b/dist/types/renderers/create-form.d.ts new file mode 100644 index 00000000..d2d622ce --- /dev/null +++ b/dist/types/renderers/create-form.d.ts @@ -0,0 +1,3 @@ +import { EditDatumFormCreator, NewRelFormCreator } from '../types/form'; +export declare function createFormNew(form_creator: NewRelFormCreator, closeCallback: () => void): HTMLDivElement; +export declare function createFormEdit(form_creator: EditDatumFormCreator, closeCallback: () => void): HTMLDivElement; diff --git a/dist/types/renderers/html.d.ts b/dist/types/renderers/html.d.ts new file mode 100644 index 00000000..194b41eb --- /dev/null +++ b/dist/types/renderers/html.d.ts @@ -0,0 +1,10 @@ +export default function htmlContSetup(cont: HTMLElement): { + svg: SVGElement; + svgView: Element | null; + htmlSvg: Element | null; + htmlView: Element | null; +}; +declare function createHtmlSvg(cont: HTMLElement): HTMLDivElement | null; +export declare function onZoomSetup(getSvgView: () => HTMLElement, getHtmlView: () => HTMLElement): (e: any) => void; +/** @deprecated This export will be removed in a future version. Use htmlContSetup instead. */ +export { createHtmlSvg }; diff --git a/dist/types/renderers/icons.d.ts b/dist/types/renderers/icons.d.ts new file mode 100644 index 00000000..299e4d72 --- /dev/null +++ b/dist/types/renderers/icons.d.ts @@ -0,0 +1,36 @@ +export declare function userIcon(): string; +export declare function userEditIcon(): string; +export declare function userPlusIcon(): string; +export declare function userPlusCloseIcon(): string; +export declare function plusIcon(): string; +export declare function pencilIcon(): string; +export declare function pencilOffIcon(): string; +export declare function trashIcon(): string; +export declare function historyBackIcon(): string; +export declare function historyForwardIcon(): string; +export declare function personIcon(): string; +export declare function miniTreeIcon(): string; +export declare function toggleIconOn(): string; +export declare function toggleIconOff(): string; +export declare function chevronDownIcon(): string; +export declare function chevronUpIcon(): string; +export declare function linkOffIcon(): string; +export declare function infoIcon(): string; +export declare function userSvgIcon(): string; +export declare function userEditSvgIcon(): string; +export declare function userPlusSvgIcon(): string; +export declare function userPlusCloseSvgIcon(): string; +export declare function plusSvgIcon(): string; +export declare function pencilSvgIcon(): string; +export declare function pencilOffSvgIcon(): string; +export declare function trashSvgIcon(): string; +export declare function historyBackSvgIcon(): string; +export declare function historyForwardSvgIcon(): string; +export declare function personSvgIcon(): string; +export declare function miniTreeSvgIcon(): string; +export declare function toggleSvgIconOn(): string; +export declare function toggleSvgIconOff(): string; +export declare function chevronDownSvgIcon(): string; +export declare function chevronUpSvgIcon(): string; +export declare function linkOffSvgIcon(): string; +export declare function infoSvgIcon(): string; diff --git a/dist/types/renderers/svg.d.ts b/dist/types/renderers/svg.d.ts new file mode 100644 index 00000000..f73e39a1 --- /dev/null +++ b/dist/types/renderers/svg.d.ts @@ -0,0 +1,2 @@ +import { ZoomProps } from "../handlers/view-handlers"; +export default function createSvg(cont: HTMLElement, props?: ZoomProps): SVGSVGElement; diff --git a/dist/types/renderers/view-cards-html.d.ts b/dist/types/renderers/view-cards-html.d.ts new file mode 100644 index 00000000..bb21b867 --- /dev/null +++ b/dist/types/renderers/view-cards-html.d.ts @@ -0,0 +1,3 @@ +import { Tree } from "../layout/calculate-tree"; +import { ViewProps } from "./view"; +export default function updateCardsHtml(svg: SVGElement, tree: Tree, Card: any, props?: ViewProps): void; diff --git a/dist/types/renderers/view-cards-svg.d.ts b/dist/types/renderers/view-cards-svg.d.ts new file mode 100644 index 00000000..6a21d7e3 --- /dev/null +++ b/dist/types/renderers/view-cards-svg.d.ts @@ -0,0 +1,3 @@ +import { Tree } from "../layout/calculate-tree"; +import { ViewProps } from "./view"; +export default function updateCardsSvg(svg: SVGElement, tree: Tree, Card: any, props?: ViewProps): void; diff --git a/dist/types/renderers/view-links.d.ts b/dist/types/renderers/view-links.d.ts new file mode 100644 index 00000000..ab54b53a --- /dev/null +++ b/dist/types/renderers/view-links.d.ts @@ -0,0 +1,3 @@ +import { ViewProps } from "./view"; +import { Tree } from "../layout/calculate-tree"; +export default function updateLinks(svg: SVGElement, tree: Tree, props?: ViewProps): void; diff --git a/dist/types/renderers/view.d.ts b/dist/types/renderers/view.d.ts new file mode 100644 index 00000000..7be6b1df --- /dev/null +++ b/dist/types/renderers/view.d.ts @@ -0,0 +1,11 @@ +import { Tree } from "../layout/calculate-tree"; +export interface ViewProps { + initial?: boolean; + transition_time?: number; + cardComponent?: boolean; + cardHtml?: boolean; + cardHtmlDiv?: HTMLElement; + tree_position?: 'fit' | 'main_to_middle' | 'inherit'; + scale?: number; +} +export default function (tree: Tree, svg: SVGElement, Card: any, props?: ViewProps): boolean; diff --git a/dist/types/store/add-existing-rel.d.ts b/dist/types/store/add-existing-rel.d.ts new file mode 100644 index 00000000..f533f306 --- /dev/null +++ b/dist/types/store/add-existing-rel.d.ts @@ -0,0 +1,3 @@ +import { Data, Datum } from "../types/data"; +export declare function handleLinkRel(updated_datum: Datum, link_rel_id: Datum['id'], store_data: Data): void; +export declare function getLinkRelOptions(datum: Datum, data: Data): Datum[]; diff --git a/dist/types/store/add-relative.d.ts b/dist/types/store/add-relative.d.ts new file mode 100644 index 00000000..a1b2883b --- /dev/null +++ b/dist/types/store/add-relative.d.ts @@ -0,0 +1,5 @@ +import { Data, Datum } from "../types/data"; +import { AddRelative } from "../core/add-relative"; +export declare function updateGendersForNewRelatives(updated_datum: Datum, data: Data): void; +export declare function cleanUp(data: Data): void; +export declare function addDatumRelsPlaceholders(datum: Datum, store_data: Data, addRelLabels: AddRelative['addRelLabels'], canAdd?: AddRelative['canAdd']): Data; diff --git a/dist/types/store/edit.d.ts b/dist/types/store/edit.d.ts new file mode 100644 index 00000000..930d8411 --- /dev/null +++ b/dist/types/store/edit.d.ts @@ -0,0 +1,11 @@ +import { Data, Datum } from "../types/data"; +export declare function submitFormData(datum: Datum, data_stash: Data, form_data: FormData): void; +export declare function syncRelReference(datum: Datum, data_stash: Data): void; +export declare function onDeleteSyncRelReference(datum: Datum, data_stash: Data): void; +export declare function moveToAddToAdded(datum: Datum, data_stash: Data): Datum; +export declare function removeToAdd(datum: Datum, data_stash: Data): boolean; +export declare function deletePerson(datum: Datum, data_stash: Data, clean_to_add?: boolean): { + success: boolean; +}; +export declare function cleanupDataJson(data: Data): Data; +export declare function removeToAddFromData(data: Data): void; diff --git a/dist/types/store/format-data.d.ts b/dist/types/store/format-data.d.ts new file mode 100644 index 00000000..61a5d96c --- /dev/null +++ b/dist/types/store/format-data.d.ts @@ -0,0 +1,12 @@ +import { Data, Datum } from "../types/data"; +export interface LegacyDatum extends Omit { + rels: { + father?: string; + mother?: string; + spouses?: string[]; + children?: string[]; + parents?: string[]; + }; +} +export declare function formatData(data: any): Data; +export declare function formatDataForExport(data: LegacyDatum[], legacy_format?: boolean): LegacyDatum[]; diff --git a/dist/types/store/new-person.d.ts b/dist/types/store/new-person.d.ts new file mode 100644 index 00000000..a626d032 --- /dev/null +++ b/dist/types/store/new-person.d.ts @@ -0,0 +1,42 @@ +import { Data, Datum } from "../types/data"; +type RelType = 'daughter' | 'son' | 'mother' | 'father' | 'spouse'; +export declare function createNewPerson({ data, rels }: { + data: Datum['data']; + rels?: { + parents?: string[]; + spouses?: string[]; + children?: string[]; + }; +}): { + id: string; + data: { + [key: string]: any; + gender: "M" | "F"; + }; + rels: { + parents: string[]; + spouses: string[]; + children: string[]; + }; +}; +export declare function createNewPersonWithGenderFromRel({ data, rel_type, rel_datum }: { + data: Datum['data']; + rel_type: RelType; + rel_datum: Datum; +}): { + id: string; + data: { + [key: string]: any; + gender: "M" | "F"; + }; + rels: { + parents: string[]; + spouses: string[]; + children: string[]; + }; +}; +export declare function addNewPerson({ data_stash, datum }: { + data_stash: Data; + datum: Datum; +}): void; +export {}; diff --git a/dist/types/store/store.d.ts b/dist/types/store/store.d.ts new file mode 100644 index 00000000..c5f6a3ae --- /dev/null +++ b/dist/types/store/store.d.ts @@ -0,0 +1,2 @@ +import { Store, StoreState } from "../types/store"; +export default function createStore(initial_state: StoreState): Store; diff --git a/dist/types/types/card.d.ts b/dist/types/types/card.d.ts new file mode 100644 index 00000000..d535480d --- /dev/null +++ b/dist/types/types/card.d.ts @@ -0,0 +1,13 @@ +export interface CardDim { + w?: number; + h?: number; + text_x?: number; + text_y?: number; + img_w?: number; + img_h?: number; + img_x?: number; + img_y?: number; + height_auto?: boolean; +} +export { CardHtml } from '../core/cards/card-html'; +export { CardSvg } from '../core/cards/card-svg'; diff --git a/dist/types/types/data.d.ts b/dist/types/types/data.d.ts new file mode 100644 index 00000000..bc9c7e2e --- /dev/null +++ b/dist/types/types/data.d.ts @@ -0,0 +1,14 @@ +export interface Datum { + id: string; + data: { + gender: 'M' | 'F'; + [key: string]: any; + }; + rels: { + parents: string[]; + spouses: string[]; + children: string[]; + }; + [key: string]: any; +} +export type Data = Datum[]; diff --git a/dist/types/types/form.d.ts b/dist/types/types/form.d.ts new file mode 100644 index 00000000..40cc02fa --- /dev/null +++ b/dist/types/types/form.d.ts @@ -0,0 +1,104 @@ +import { Datum } from "./data"; +import { Store } from "./store"; +import { AddRelative } from "../core/add-relative"; +import { RemoveRelative } from "../core/remove-relative"; +import { EditTree } from "../core/edit"; +export interface FormCreatorSetupProps { + datum: Datum; + store: Store; + fields: any[]; + postSubmitHandler: (props: any) => void; + onCancel: () => void; + editFirst: boolean; + no_edit: boolean; + link_existing_rel_config?: { + linkRelLabel: (d: Datum) => string; + title?: string; + select_placeholder?: string; + }; + onFormCreation: EditTree['onFormCreation']; + addRelative?: AddRelative; + removeRelative?: RemoveRelative; + deletePerson?: () => void; + onSubmit?: (e: Event, datum: Datum, applyChanges: () => void, postSubmit: () => void) => void; + onDelete?: (datum: Datum, deletePerson: () => void, postSubmit: (props: any) => void) => void; + canEdit?: (datum: Datum) => boolean; + canDelete?: (datum: Datum) => boolean; +} +export interface BaseFormCreator { + datum_id: string; + fields: any[]; + onSubmit: (e: any) => void; + onCancel: () => void; + onFormCreation: FormCreatorSetupProps['onFormCreation']; + no_edit: boolean; + gender_field: { + id: 'gender'; + type: 'switch'; + label: 'Gender'; + initial_value: 'M' | 'F'; + disabled: boolean; + options: { + value: 'M' | 'F'; + label: string; + }[]; + }; + linkExistingRelative?: any; +} +export interface EditDatumFormCreator extends BaseFormCreator { + onDelete: () => void; + addRelative: () => void; + addRelativeCancel: () => void; + addRelativeActive: boolean; + removeRelative: () => void; + removeRelativeCancel: () => void; + removeRelativeActive: boolean; + editable: boolean; + can_delete: boolean; +} +export interface NewRelFormCreator extends BaseFormCreator { + title: string; + new_rel: boolean; + editable: boolean; +} +export type FormCreator = EditDatumFormCreator | NewRelFormCreator; +export interface Field { + id: string; + type: string; + label: string; + initial_value: string; + placeholder?: string; +} +export interface RelReferenceField extends Field { + type: 'rel_reference'; + rel_id: string; + rel_label: string; + rel_type: 'spouse'; +} +export interface RelReferenceFieldCreator { + rel_type: 'spouse'; + id: string; + label: string; + getRelLabel: (datum: Datum) => string; +} +export interface SelectField extends Field { + type: 'select'; + options: { + value: string; + label: string; + }[]; +} +export interface SelectFieldCreator { + id: string; + type: 'select'; + label: string; + placeholder?: string; + options?: { + value: string; + label: string; + }[]; + optionCreator?: (datum: Datum) => { + value: string; + label: string; + }[]; +} diff --git a/dist/types/types/index.d.ts b/dist/types/types/index.d.ts new file mode 100644 index 00000000..2efb412d --- /dev/null +++ b/dist/types/types/index.d.ts @@ -0,0 +1,10 @@ +export * from './data'; +export * from './card'; +export * from './store'; +export * from './treeData'; +export * from './view'; +export { Chart } from '../core/chart'; +export { EditTree } from '../core/edit'; +export { AddRelative } from '../core/add-relative'; +export { CardSvg } from '../core/cards/card-svg'; +export { CardHtml } from '../core/cards/card-html'; diff --git a/dist/types/types/store.d.ts b/dist/types/types/store.d.ts new file mode 100644 index 00000000..b6b22482 --- /dev/null +++ b/dist/types/types/store.d.ts @@ -0,0 +1,61 @@ +import { Datum, Data } from './data'; +import { TreeDatum } from './treeData'; +import { CalculateTreeOptions, Tree } from '../layout/calculate-tree'; +import { ViewProps } from '../renderers/view'; +export type TransitionTime = number; +export type SingleParentEmptyCardLabel = string; +export type UnknownCardLabel = string; +export type DuplicateBranchToggle = boolean; +export type LevelSeparation = number; +export type NodeSeparation = number; +export type PrivateCardsConfig = { + condition: (d: Datum) => boolean; +}; +export type ShowSiblingsOfMain = boolean; +export type ModifyTreeHierarchy = CalculateTreeOptions['modifyTreeHierarchy']; +export type SortChildrenFunction = ((a: Datum, b: Datum) => number); +export type SortSpousesFunction = ((d: Datum, data: Data) => void); +export type AncestryDepth = number; +export type ProgenyDepth = number; +export interface StoreState extends CalculateTreeOptions { + data: Data; + main_id: Datum['id']; + main_id_history?: Datum['id'][]; + tree?: Tree; + legacy_format?: boolean; + transition_time?: TransitionTime; + single_parent_empty_card_label?: SingleParentEmptyCardLabel; + unknown_card_label?: UnknownCardLabel; + duplicate_branch_toggle?: DuplicateBranchToggle; + level_separation?: LevelSeparation; + node_separation?: NodeSeparation; + private_cards_config?: PrivateCardsConfig; + show_siblings_of_main?: ShowSiblingsOfMain; + sortChildrenFunction?: SortChildrenFunction; + sortSpousesFunction?: SortSpousesFunction; + ancestry_depth?: AncestryDepth; + progeny_depth?: ProgenyDepth; +} +export interface Store { + state: StoreState; + updateTree: (props?: ViewProps) => void; + updateData: (data: Data) => void; + updateMainId: (id: Datum['id']) => void; + getMainId: () => Datum['id']; + getData: () => Data; + getTree: () => StoreState['tree']; + setOnUpdate: (f: (props?: ViewProps) => void) => void; + getMainDatum: () => Datum; + getDatum: (id: Datum['id']) => Datum | undefined; + getTreeMainDatum: () => TreeDatum; + getTreeDatum: (id: Datum['id']) => TreeDatum | undefined; + getLastAvailableMainDatum: () => Datum; + methods: { + [key: string]: (...args: any[]) => any; + }; +} +export interface UpdateTreeProps { + initial?: boolean; + tree_position?: 'fit' | 'main_to_middle' | 'inherit'; + transition_time?: number; +} diff --git a/dist/types/types/treeData.d.ts b/dist/types/types/treeData.d.ts new file mode 100644 index 00000000..9c6dacf7 --- /dev/null +++ b/dist/types/types/treeData.d.ts @@ -0,0 +1,62 @@ +import { Datum } from './data'; +/** + * Represents a node in the family tree. + */ +export interface TreeDatum { + /** The underlying data for this node. */ + data: Datum; + /** X position in the tree layout. */ + x: number; + /** Y position in the tree layout. */ + y: number; + /** Depth of the node in the tree. */ + depth: number; + /** Reference to the parent node (d3 hierarchy parent). */ + parent?: TreeDatum; + /** Unique tree node ID. */ + tid?: string; + /** Previous X position (for transitions/animations). */ + _x?: number; + /** Previous Y position (for transitions/animations). */ + _y?: number; + /** Spouse X position. */ + sx?: number; + /** Spouse Y position. */ + sy?: number; + /** Parent spouse X position. */ + psx?: number; + /** Parent spouse Y position. */ + psy?: number; + /** True if the card is transitioning out of the tree (e.g., during view change). */ + exiting?: boolean; + /** True if the node was just added. */ + added?: boolean; + /** True if not all relatives of this person are displayed in the current tree view. */ + all_rels_displayed?: boolean; + /** Children of this node (main person and progeny). */ + children?: TreeDatum[]; + /** Parents of this node (main person and ancestry). */ + parents?: TreeDatum[]; + /** Spouses of this person (progeny and main person only). */ + spouses?: TreeDatum[]; + /** If this person is added as a spouse of progeny or main person, this property is set. */ + spouse?: TreeDatum; + /** For ancestry nodes, connects to spouse. */ + coparent?: TreeDatum; + /** If this person is a duplicate, this is the number of duplicates. */ + duplicate?: number; + /** True if this person is an ancestor. */ + is_ancestry?: boolean; + /** True if this node is a sibling (setShowSiblingsOfMain is true). */ + sibling?: boolean; + /** True if this card is private and should be treated differently. */ + is_private?: boolean; + /** if we want to modify hierarchy of the tree, we can omit displaying some spouses */ + _ignore_spouses?: Datum['id'][]; + /** Reference to the DOM node for this tree datum. (for debugging) */ + __node?: HTMLElement; + /** Reference to the label for this tree datum. (for debugging) */ + __label?: string; +} +/** An array of tree nodes. */ +export type TreeData = TreeDatum[]; diff --git a/dist/types/types/view.d.ts b/dist/types/types/view.d.ts new file mode 100644 index 00000000..907d8590 --- /dev/null +++ b/dist/types/types/view.d.ts @@ -0,0 +1,7 @@ +import { TreeDatum } from "./treeData"; +import { Link } from "../layout/create-links"; +import { Selection, BaseType } from "d3"; +export interface CardHtmlSelection extends Selection { +} +export interface LinkSelection extends Selection { +}