Add homepage block, reactive controls, i18n, and full-width layout
- Implement ModuleBlockInterface for tree/user homepage embedding with configurable individual and generation settings - Make generation sliders reactive (auto-navigate on change) - Persist generation counts when clicking between persons - Expand chart to full viewport width and remaining height - Add German and Dutch translations for all custom strings - Pass translated labels to JS bio card tooltip via window.fullDiagramI18n - Use webtrees core translation keys where available - Increase default generations to 4, remove show siblings checkbox - Expose Chart class globally for block instantiation
This commit is contained in:
@@ -7,6 +7,20 @@
|
|||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
background: #f8f9fa;
|
background: #f8f9fa;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
|
/* Break out of webtrees content column to use full viewport width */
|
||||||
|
width: 100vw;
|
||||||
|
margin-left: calc(-50vw + 50%);
|
||||||
|
border-radius: 0;
|
||||||
|
border-left: none;
|
||||||
|
border-right: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Block variant: stay within card boundaries */
|
||||||
|
.full-diagram-block {
|
||||||
|
width: 100%;
|
||||||
|
margin-left: 0;
|
||||||
|
border-radius: 0.5rem;
|
||||||
|
border: 1px solid #dee2e6;
|
||||||
}
|
}
|
||||||
|
|
||||||
.full-diagram-chart {
|
.full-diagram-chart {
|
||||||
|
|||||||
2
resources/js/full-diagram.min.js
vendored
2
resources/js/full-diagram.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -5,6 +5,9 @@
|
|||||||
*/
|
*/
|
||||||
import Chart from "./lib/chart.js";
|
import Chart from "./lib/chart.js";
|
||||||
|
|
||||||
|
// Expose Chart class for block usage
|
||||||
|
window.FullDiagramChart = Chart;
|
||||||
|
|
||||||
async function init() {
|
async function init() {
|
||||||
const data = window.fullDiagramData;
|
const data = window.fullDiagramData;
|
||||||
const baseUrl = window.fullDiagramBaseUrl;
|
const baseUrl = window.fullDiagramBaseUrl;
|
||||||
|
|||||||
@@ -3,12 +3,24 @@
|
|||||||
*
|
*
|
||||||
* Shows: full name, profile photo, birth, baptism, marriage, death,
|
* Shows: full name, profile photo, birth, baptism, marriage, death,
|
||||||
* occupation, residence, current age (if alive) or age at death.
|
* occupation, residence, current age (if alive) or age at death.
|
||||||
|
*
|
||||||
|
* Uses window.fullDiagramI18n for translated labels.
|
||||||
*/
|
*/
|
||||||
import { select } from "../d3.js";
|
import { select } from "../d3.js";
|
||||||
|
|
||||||
let activeTooltip = null;
|
let activeTooltip = null;
|
||||||
let hideTimer = null;
|
let hideTimer = null;
|
||||||
|
|
||||||
|
/** Get a translated string, with optional substitution. */
|
||||||
|
function t(key, ...args) {
|
||||||
|
const i18n = window.fullDiagramI18n || {};
|
||||||
|
let str = i18n[key] || key;
|
||||||
|
for (const arg of args) {
|
||||||
|
str = str.replace("__AGE__", arg);
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Show a bio card tooltip for a person.
|
* Show a bio card tooltip for a person.
|
||||||
*
|
*
|
||||||
@@ -59,19 +71,19 @@ export function showBioCard(data, cardElement, containerSelector) {
|
|||||||
// Facts list
|
// Facts list
|
||||||
const facts = tooltip.append("div").attr("class", "bio-facts");
|
const facts = tooltip.append("div").attr("class", "bio-facts");
|
||||||
|
|
||||||
addFact(facts, "Born", data.birthDate, data.birthPlace);
|
addFact(facts, t("Born"), data.birthDate, data.birthPlace);
|
||||||
addFact(facts, "Baptism", data.baptismDate);
|
addFact(facts, t("Baptism"), data.baptismDate);
|
||||||
addFact(facts, "Marriage", data.marriageDate);
|
addFact(facts, t("Marriage"), data.marriageDate);
|
||||||
addFact(facts, "Died", data.deathDate, data.deathPlace);
|
addFact(facts, t("Died"), data.deathDate, data.deathPlace);
|
||||||
addFact(facts, "Occupation", data.occupation);
|
addFact(facts, t("Occupation"), data.occupation);
|
||||||
addFact(facts, "Residence", data.residence);
|
addFact(facts, t("Residence"), data.residence);
|
||||||
|
|
||||||
// Link to profile
|
// Link to profile
|
||||||
tooltip
|
tooltip
|
||||||
.append("a")
|
.append("a")
|
||||||
.attr("href", data.url)
|
.attr("href", data.url)
|
||||||
.attr("class", "bio-link")
|
.attr("class", "bio-link")
|
||||||
.text("View profile \u2192");
|
.text(t("View profile") + " \u2192");
|
||||||
|
|
||||||
activeTooltip = tooltip;
|
activeTooltip = tooltip;
|
||||||
}
|
}
|
||||||
@@ -100,15 +112,15 @@ function computeAge(data) {
|
|||||||
const deathYear = parseInt(data.deathYear, 10);
|
const deathYear = parseInt(data.deathYear, 10);
|
||||||
if (!isNaN(deathYear)) {
|
if (!isNaN(deathYear)) {
|
||||||
const age = deathYear - birthYear;
|
const age = deathYear - birthYear;
|
||||||
return `Died at age ${age}`;
|
return t("Died at age %s", age);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "Deceased";
|
return t("Deceased");
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentYear = new Date().getFullYear();
|
const currentYear = new Date().getFullYear();
|
||||||
const age = currentYear - birthYear;
|
const age = currentYear - birthYear;
|
||||||
return `Age ~${age}`;
|
return t("Age ~%s", age);
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleHide() {
|
function scheduleHide() {
|
||||||
|
|||||||
81
resources/views/modules/full-diagram/block-config.phtml
Normal file
81
resources/views/modules/full-diagram/block-config.phtml
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Fisharebest\Webtrees\I18N;
|
||||||
|
use Fisharebest\Webtrees\Individual;
|
||||||
|
use Fisharebest\Webtrees\Tree;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Individual|null $individual
|
||||||
|
* @var Tree $tree
|
||||||
|
* @var int $ancestor_generations
|
||||||
|
* @var int $descendant_generations
|
||||||
|
* @var bool $show_siblings
|
||||||
|
*/
|
||||||
|
?>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<label class="col-sm-3 col-form-label" for="xref">
|
||||||
|
<?= I18N::translate('Individual') ?>
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<?= view('components/select-individual', [
|
||||||
|
'name' => 'xref',
|
||||||
|
'individual' => $individual,
|
||||||
|
'tree' => $tree,
|
||||||
|
]) ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<label class="col-sm-3 col-form-label" for="ancestor_generations">
|
||||||
|
<?= I18N::translate('Generations of ancestors') ?>
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="form-control"
|
||||||
|
id="ancestor_generations"
|
||||||
|
name="ancestor_generations"
|
||||||
|
min="1"
|
||||||
|
max="10"
|
||||||
|
value="<?= $ancestor_generations ?>"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<label class="col-sm-3 col-form-label" for="descendant_generations">
|
||||||
|
<?= I18N::translate('Generations of descendants') ?>
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-9">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
class="form-control"
|
||||||
|
id="descendant_generations"
|
||||||
|
name="descendant_generations"
|
||||||
|
min="1"
|
||||||
|
max="10"
|
||||||
|
value="<?= $descendant_generations ?>"
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-sm-9 offset-sm-3">
|
||||||
|
<div class="form-check">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
class="form-check-input"
|
||||||
|
id="show_siblings"
|
||||||
|
name="show_siblings"
|
||||||
|
value="1"
|
||||||
|
<?= $show_siblings ? 'checked' : '' ?>
|
||||||
|
>
|
||||||
|
<label class="form-check-label" for="show_siblings">
|
||||||
|
<?= I18N::translate('Show siblings') ?>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
74
resources/views/modules/full-diagram/block.phtml
Normal file
74
resources/views/modules/full-diagram/block.phtml
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Fisharebest\Webtrees\I18N;
|
||||||
|
use Fisharebest\Webtrees\Individual;
|
||||||
|
use Fisharebest\Webtrees\Module\ModuleChartInterface;
|
||||||
|
use Fisharebest\Webtrees\Tree;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var ModuleChartInterface $module
|
||||||
|
* @var Individual $individual
|
||||||
|
* @var Tree $tree
|
||||||
|
* @var string $tree_data
|
||||||
|
* @var string $javascript_url
|
||||||
|
* @var string $stylesheet_url
|
||||||
|
* @var int $block_id
|
||||||
|
* @var int $ancestor_generations
|
||||||
|
* @var int $descendant_generations
|
||||||
|
* @var bool $show_siblings
|
||||||
|
*/
|
||||||
|
|
||||||
|
$containerId = 'full-diagram-block-' . $block_id;
|
||||||
|
?>
|
||||||
|
|
||||||
|
<link rel="stylesheet" href="<?= e($stylesheet_url) ?>">
|
||||||
|
|
||||||
|
<div id="<?= $containerId ?>" class="full-diagram-container full-diagram-block wt-chart" data-tree-name="<?= e($tree->name()) ?>" style="height:450px;">
|
||||||
|
<div class="full-diagram-chart"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
if (!window.fullDiagramI18n) {
|
||||||
|
window.fullDiagramI18n = <?= json_encode([
|
||||||
|
'Born' => I18N::translate('Born'),
|
||||||
|
'Baptism' => I18N::translate('Baptism'),
|
||||||
|
'Marriage' => I18N::translate('Marriage'),
|
||||||
|
'Died' => I18N::translate('Died'),
|
||||||
|
'Occupation' => I18N::translate('Occupation'),
|
||||||
|
'Residence' => I18N::translate('Residence'),
|
||||||
|
'View profile' => I18N::translate('View profile'),
|
||||||
|
'Died at age %s' => I18N::translate('Died at age %s', '__AGE__'),
|
||||||
|
'Deceased' => I18N::translate('Deceased'),
|
||||||
|
'Age ~%s' => I18N::translate('Age ~%s', '__AGE__'),
|
||||||
|
], JSON_THROW_ON_ERROR) ?>;
|
||||||
|
}
|
||||||
|
var data = <?= $tree_data ?>;
|
||||||
|
var baseUrl = <?= json_encode(route($module::ROUTE_NAME, [
|
||||||
|
'tree' => $tree->name(),
|
||||||
|
'xref' => '__XREF__',
|
||||||
|
'ancestor_generations' => $ancestor_generations,
|
||||||
|
'descendant_generations' => $descendant_generations,
|
||||||
|
'show_siblings' => $show_siblings ? '1' : '0',
|
||||||
|
]), JSON_THROW_ON_ERROR) ?>;
|
||||||
|
|
||||||
|
function initBlock() {
|
||||||
|
if (typeof window.FullDiagramChart === 'undefined') {
|
||||||
|
// JS not loaded yet, retry
|
||||||
|
setTimeout(initBlock, 100);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var chart = new window.FullDiagramChart('#<?= $containerId ?>', data, baseUrl);
|
||||||
|
chart.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (document.readyState === 'loading') {
|
||||||
|
document.addEventListener('DOMContentLoaded', initBlock);
|
||||||
|
} else {
|
||||||
|
initBlock();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<script src="<?= e($javascript_url) ?>"></script>
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Fisharebest\Webtrees\I18N;
|
||||||
use Fisharebest\Webtrees\Individual;
|
use Fisharebest\Webtrees\Individual;
|
||||||
use Fisharebest\Webtrees\Module\ModuleChartInterface;
|
use Fisharebest\Webtrees\Module\ModuleChartInterface;
|
||||||
use Fisharebest\Webtrees\Tree;
|
use Fisharebest\Webtrees\Tree;
|
||||||
@@ -13,17 +14,50 @@ use Fisharebest\Webtrees\Tree;
|
|||||||
* @var string $tree_data
|
* @var string $tree_data
|
||||||
* @var string $javascript_url
|
* @var string $javascript_url
|
||||||
* @var string $stylesheet_url
|
* @var string $stylesheet_url
|
||||||
|
* @var int $ancestor_generations
|
||||||
|
* @var int $descendant_generations
|
||||||
|
* @var bool $show_siblings
|
||||||
*/
|
*/
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<link rel="stylesheet" href="<?= e($stylesheet_url) ?>">
|
<link rel="stylesheet" href="<?= e($stylesheet_url) ?>">
|
||||||
|
|
||||||
<div id="full-diagram-container" class="full-diagram-container wt-chart" data-tree-name="<?= e($tree->name()) ?>" style="width:100%;height:700px;">
|
<div id="full-diagram-container" class="full-diagram-container wt-chart" data-tree-name="<?= e($tree->name()) ?>">
|
||||||
<div class="full-diagram-chart"></div>
|
<div class="full-diagram-chart"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
(function() {
|
||||||
|
var el = document.getElementById('full-diagram-container');
|
||||||
|
function resize() {
|
||||||
|
var rect = el.getBoundingClientRect();
|
||||||
|
el.style.height = Math.max(400, window.innerHeight - rect.top - 16) + 'px';
|
||||||
|
}
|
||||||
|
resize();
|
||||||
|
window.addEventListener('resize', resize);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
window.fullDiagramI18n = <?= json_encode([
|
||||||
|
'Born' => I18N::translate('Born'),
|
||||||
|
'Baptism' => I18N::translate('Baptism'),
|
||||||
|
'Marriage' => I18N::translate('Marriage'),
|
||||||
|
'Died' => I18N::translate('Died'),
|
||||||
|
'Occupation' => I18N::translate('Occupation'),
|
||||||
|
'Residence' => I18N::translate('Residence'),
|
||||||
|
'View profile' => I18N::translate('View profile'),
|
||||||
|
'Died at age %s' => I18N::translate('Died at age %s', '__AGE__'),
|
||||||
|
'Deceased' => I18N::translate('Deceased'),
|
||||||
|
'Age ~%s' => I18N::translate('Age ~%s', '__AGE__'),
|
||||||
|
], JSON_THROW_ON_ERROR) ?>;
|
||||||
window.fullDiagramData = <?= $tree_data ?>;
|
window.fullDiagramData = <?= $tree_data ?>;
|
||||||
window.fullDiagramBaseUrl = <?= json_encode(route($module::ROUTE_NAME, ['tree' => $tree->name(), 'xref' => '__XREF__']), JSON_THROW_ON_ERROR) ?>;
|
window.fullDiagramBaseUrl = <?= json_encode(route($module::ROUTE_NAME, [
|
||||||
|
'tree' => $tree->name(),
|
||||||
|
'xref' => '__XREF__',
|
||||||
|
'ancestor_generations' => $ancestor_generations,
|
||||||
|
'descendant_generations' => $descendant_generations,
|
||||||
|
'show_siblings' => $show_siblings ? '1' : '0',
|
||||||
|
]), JSON_THROW_ON_ERROR) ?>;
|
||||||
</script>
|
</script>
|
||||||
<script src="<?= e($javascript_url) ?>"></script>
|
<script src="<?= e($javascript_url) ?>"></script>
|
||||||
|
|||||||
@@ -35,73 +35,94 @@ use FullDiagram\Configuration;
|
|||||||
|
|
||||||
<h2 class="wt-page-title"><?= $title ?></h2>
|
<h2 class="wt-page-title"><?= $title ?></h2>
|
||||||
|
|
||||||
<form method="post" class="wt-page-options wt-page-options-chart d-print-none mb-3">
|
<div class="wt-page-options wt-page-options-chart d-print-none mb-3">
|
||||||
<?= csrf_field() ?>
|
|
||||||
|
|
||||||
<div class="row g-3 align-items-end">
|
<div class="row g-3 align-items-end">
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<label for="ancestor_generations" class="form-label">
|
<label for="ancestor_generations" class="form-label">
|
||||||
<?= I18N::translate('Ancestor generations') ?>
|
<?= I18N::translate('Generations of ancestors') ?>
|
||||||
</label>
|
</label>
|
||||||
<input
|
<div class="d-flex align-items-center gap-2">
|
||||||
type="range"
|
|
||||||
class="form-range"
|
|
||||||
id="ancestor_generations"
|
|
||||||
name="ancestor_generations"
|
|
||||||
min="<?= $min_generations ?>"
|
|
||||||
max="<?= $max_generations ?>"
|
|
||||||
value="<?= $ancestor_generations ?>"
|
|
||||||
oninput="document.getElementById('ancestor_gen_label').textContent = this.value"
|
|
||||||
>
|
|
||||||
<span id="ancestor_gen_label" class="badge bg-secondary"><?= $ancestor_generations ?></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-auto">
|
|
||||||
<label for="descendant_generations" class="form-label">
|
|
||||||
<?= I18N::translate('Descendant generations') ?>
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="range"
|
|
||||||
class="form-range"
|
|
||||||
id="descendant_generations"
|
|
||||||
name="descendant_generations"
|
|
||||||
min="<?= $min_generations ?>"
|
|
||||||
max="<?= $max_generations ?>"
|
|
||||||
value="<?= $descendant_generations ?>"
|
|
||||||
oninput="document.getElementById('descendant_gen_label').textContent = this.value"
|
|
||||||
>
|
|
||||||
<span id="descendant_gen_label" class="badge bg-secondary"><?= $descendant_generations ?></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-auto">
|
|
||||||
<div class="form-check">
|
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="range"
|
||||||
class="form-check-input"
|
class="form-range"
|
||||||
id="show_siblings"
|
id="ancestor_generations"
|
||||||
name="show_siblings"
|
min="<?= $min_generations ?>"
|
||||||
value="1"
|
max="<?= $max_generations ?>"
|
||||||
<?= $show_siblings ? 'checked' : '' ?>
|
value="<?= $ancestor_generations ?>"
|
||||||
|
style="width: 120px;"
|
||||||
>
|
>
|
||||||
<label class="form-check-label" for="show_siblings">
|
<span id="ancestor_gen_label" class="badge bg-secondary"><?= $ancestor_generations ?></span>
|
||||||
<?= I18N::translate('Show siblings') ?>
|
|
||||||
</label>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="col-auto">
|
<div class="col-auto">
|
||||||
<button type="submit" class="btn btn-primary">
|
<label for="descendant_generations" class="form-label">
|
||||||
<?= I18N::translate('View') ?>
|
<?= I18N::translate('Generations of descendants') ?>
|
||||||
</button>
|
</label>
|
||||||
|
<div class="d-flex align-items-center gap-2">
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
class="form-range"
|
||||||
|
id="descendant_generations"
|
||||||
|
min="<?= $min_generations ?>"
|
||||||
|
max="<?= $max_generations ?>"
|
||||||
|
value="<?= $descendant_generations ?>"
|
||||||
|
style="width: 120px;"
|
||||||
|
>
|
||||||
|
<span id="descendant_gen_label" class="badge bg-secondary"><?= $descendant_generations ?></span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
|
|
||||||
<?= view($module->name() . '::modules/full-diagram/chart', [
|
<?= view($module->name() . '::modules/full-diagram/chart', [
|
||||||
'module' => $module,
|
'module' => $module,
|
||||||
'individual' => $individual,
|
'individual' => $individual,
|
||||||
'tree' => $tree,
|
'tree' => $tree,
|
||||||
'tree_data' => $tree_data,
|
'tree_data' => $tree_data,
|
||||||
'javascript_url' => $javascript_url,
|
'javascript_url' => $javascript_url,
|
||||||
'stylesheet_url' => $stylesheet_url,
|
'stylesheet_url' => $stylesheet_url,
|
||||||
|
'ancestor_generations' => $ancestor_generations,
|
||||||
|
'descendant_generations' => $descendant_generations,
|
||||||
|
'show_siblings' => $show_siblings,
|
||||||
]) ?>
|
]) ?>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
(function() {
|
||||||
|
var baseChartUrl = <?= json_encode(route($module::ROUTE_NAME, [
|
||||||
|
'tree' => $tree->name(),
|
||||||
|
'xref' => $individual->xref(),
|
||||||
|
]), JSON_THROW_ON_ERROR) ?>;
|
||||||
|
|
||||||
|
var timer = null;
|
||||||
|
|
||||||
|
function navigate() {
|
||||||
|
var ag = document.getElementById('ancestor_generations').value;
|
||||||
|
var dg = document.getElementById('descendant_generations').value;
|
||||||
|
var sep = baseChartUrl.indexOf('?') === -1 ? '?' : '&';
|
||||||
|
window.location.href = baseChartUrl + sep
|
||||||
|
+ 'ancestor_generations=' + ag
|
||||||
|
+ '&descendant_generations=' + dg
|
||||||
|
+ '&show_siblings=1';
|
||||||
|
}
|
||||||
|
|
||||||
|
function scheduleNavigate() {
|
||||||
|
clearTimeout(timer);
|
||||||
|
timer = setTimeout(navigate, 400);
|
||||||
|
}
|
||||||
|
|
||||||
|
var ag = document.getElementById('ancestor_generations');
|
||||||
|
var dg = document.getElementById('descendant_generations');
|
||||||
|
|
||||||
|
ag.addEventListener('input', function() {
|
||||||
|
document.getElementById('ancestor_gen_label').textContent = this.value;
|
||||||
|
});
|
||||||
|
dg.addEventListener('input', function() {
|
||||||
|
document.getElementById('descendant_gen_label').textContent = this.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
ag.addEventListener('change', scheduleNavigate);
|
||||||
|
dg.addEventListener('change', scheduleNavigate);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ namespace FullDiagram;
|
|||||||
class Configuration
|
class Configuration
|
||||||
{
|
{
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly int $ancestorGenerations = 3,
|
private readonly int $ancestorGenerations = 4,
|
||||||
private readonly int $descendantGenerations = 3,
|
private readonly int $descendantGenerations = 4,
|
||||||
private readonly bool $showSiblings = true,
|
private readonly bool $showSiblings = true,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|||||||
179
src/Module.php
179
src/Module.php
@@ -12,32 +12,40 @@ namespace FullDiagram;
|
|||||||
|
|
||||||
use Fig\Http\Message\RequestMethodInterface;
|
use Fig\Http\Message\RequestMethodInterface;
|
||||||
use Fisharebest\Webtrees\Auth;
|
use Fisharebest\Webtrees\Auth;
|
||||||
|
use Fisharebest\Webtrees\Contracts\UserInterface;
|
||||||
use Fisharebest\Webtrees\I18N;
|
use Fisharebest\Webtrees\I18N;
|
||||||
use Fisharebest\Webtrees\Individual;
|
use Fisharebest\Webtrees\Individual;
|
||||||
use Fisharebest\Webtrees\Menu;
|
use Fisharebest\Webtrees\Menu;
|
||||||
use Fisharebest\Webtrees\Module\AbstractModule;
|
use Fisharebest\Webtrees\Module\AbstractModule;
|
||||||
|
use Fisharebest\Webtrees\Module\ModuleBlockInterface;
|
||||||
|
use Fisharebest\Webtrees\Module\ModuleBlockTrait;
|
||||||
use Fisharebest\Webtrees\Module\ModuleChartInterface;
|
use Fisharebest\Webtrees\Module\ModuleChartInterface;
|
||||||
use Fisharebest\Webtrees\Module\ModuleChartTrait;
|
use Fisharebest\Webtrees\Module\ModuleChartTrait;
|
||||||
use Fisharebest\Webtrees\Module\ModuleCustomInterface;
|
use Fisharebest\Webtrees\Module\ModuleCustomInterface;
|
||||||
use Fisharebest\Webtrees\Module\ModuleCustomTrait;
|
use Fisharebest\Webtrees\Module\ModuleCustomTrait;
|
||||||
use Fisharebest\Webtrees\Registry;
|
use Fisharebest\Webtrees\Registry;
|
||||||
|
use Fisharebest\Webtrees\Tree;
|
||||||
use Fisharebest\Webtrees\Validator;
|
use Fisharebest\Webtrees\Validator;
|
||||||
use Fisharebest\Webtrees\View;
|
use Fisharebest\Webtrees\View;
|
||||||
use FullDiagram\Facade\DataFacade;
|
use FullDiagram\Facade\DataFacade;
|
||||||
|
use Illuminate\Support\Str;
|
||||||
use Psr\Http\Message\ResponseInterface;
|
use Psr\Http\Message\ResponseInterface;
|
||||||
use Psr\Http\Message\ServerRequestInterface;
|
use Psr\Http\Message\ServerRequestInterface;
|
||||||
use Psr\Http\Server\RequestHandlerInterface;
|
use Psr\Http\Server\RequestHandlerInterface;
|
||||||
|
|
||||||
class Module extends AbstractModule implements ModuleChartInterface, ModuleCustomInterface, RequestHandlerInterface
|
class Module extends AbstractModule implements ModuleChartInterface, ModuleCustomInterface, ModuleBlockInterface, RequestHandlerInterface
|
||||||
{
|
{
|
||||||
use ModuleChartTrait;
|
use ModuleChartTrait;
|
||||||
use ModuleCustomTrait;
|
use ModuleCustomTrait;
|
||||||
|
use ModuleBlockTrait;
|
||||||
|
|
||||||
public const ROUTE_NAME = 'full-diagram';
|
public const ROUTE_NAME = 'full-diagram';
|
||||||
public const ROUTE_URL = '/tree/{tree}/full-diagram/{xref}';
|
public const ROUTE_URL = '/tree/{tree}/full-diagram/{xref}';
|
||||||
|
|
||||||
private const DEFAULT_ANCESTOR_GENERATIONS = 3;
|
private const DEFAULT_ANCESTOR_GENERATIONS = 4;
|
||||||
private const DEFAULT_DESCENDANT_GENERATIONS = 3;
|
private const DEFAULT_DESCENDANT_GENERATIONS = 4;
|
||||||
|
private const BLOCK_DEFAULT_ANCESTOR_GENS = 4;
|
||||||
|
private const BLOCK_DEFAULT_DESCENDANT_GENS = 4;
|
||||||
private const MINIMUM_GENERATIONS = 1;
|
private const MINIMUM_GENERATIONS = 1;
|
||||||
private const MAXIMUM_GENERATIONS = 10;
|
private const MAXIMUM_GENERATIONS = 10;
|
||||||
|
|
||||||
@@ -80,6 +88,50 @@ class Module extends AbstractModule implements ModuleChartInterface, ModuleCusto
|
|||||||
->allows(RequestMethodInterface::METHOD_POST);
|
->allows(RequestMethodInterface::METHOD_POST);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Translations ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
public function customTranslations(string $language): array
|
||||||
|
{
|
||||||
|
$translations = [
|
||||||
|
'de' => [
|
||||||
|
'Full Diagram' => 'Vollständiges Diagramm',
|
||||||
|
'Full Diagram of %s' => 'Vollständiges Diagramm von %s',
|
||||||
|
'An interactive diagram showing ancestors, descendants, and siblings.' => 'Ein interaktives Diagramm mit Vorfahren, Nachkommen und Geschwistern.',
|
||||||
|
'Show siblings' => 'Geschwister anzeigen',
|
||||||
|
'Born' => 'Geboren',
|
||||||
|
'Baptism' => 'Taufe',
|
||||||
|
'Marriage' => 'Heirat',
|
||||||
|
'Died' => 'Gestorben',
|
||||||
|
'Occupation' => 'Beruf',
|
||||||
|
'Residence' => 'Wohnort',
|
||||||
|
'View profile' => 'Profil anzeigen',
|
||||||
|
'Died at age %s' => 'Gestorben im Alter von %s',
|
||||||
|
'Deceased' => 'Verstorben',
|
||||||
|
'Age ~%s' => 'Alter ~%s',
|
||||||
|
],
|
||||||
|
'nl' => [
|
||||||
|
'Full Diagram' => 'Volledig diagram',
|
||||||
|
'Full Diagram of %s' => 'Volledig diagram van %s',
|
||||||
|
'An interactive diagram showing ancestors, descendants, and siblings.' => 'Een interactief diagram met voorouders, nakomelingen en broers/zussen.',
|
||||||
|
'Show siblings' => 'Broers/zussen tonen',
|
||||||
|
'Born' => 'Geboren',
|
||||||
|
'Baptism' => 'Doop',
|
||||||
|
'Marriage' => 'Huwelijk',
|
||||||
|
'Died' => 'Overleden',
|
||||||
|
'Occupation' => 'Beroep',
|
||||||
|
'Residence' => 'Woonplaats',
|
||||||
|
'View profile' => 'Profiel bekijken',
|
||||||
|
'Died at age %s' => 'Overleden op %s-jarige leeftijd',
|
||||||
|
'Deceased' => 'Overleden',
|
||||||
|
'Age ~%s' => 'Leeftijd ~%s',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
return $translations[$language] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Chart interface ─────────────────────────────────────────────
|
||||||
|
|
||||||
public function chartMenuClass(): string
|
public function chartMenuClass(): string
|
||||||
{
|
{
|
||||||
return 'menu-chart-full-diagram';
|
return 'menu-chart-full-diagram';
|
||||||
@@ -103,6 +155,127 @@ class Module extends AbstractModule implements ModuleChartInterface, ModuleCusto
|
|||||||
return I18N::translate('Full Diagram of %s', $individual->fullName());
|
return I18N::translate('Full Diagram of %s', $individual->fullName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ─── Block interface ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
public function isTreeBlock(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isUserBlock(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function loadAjax(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getBlock(Tree $tree, int $block_id, string $context, array $config = []): string
|
||||||
|
{
|
||||||
|
$PEDIGREE_ROOT_ID = $tree->getPreference('PEDIGREE_ROOT_ID');
|
||||||
|
$gedcomid = $tree->getUserPreference(Auth::user(), UserInterface::PREF_TREE_ACCOUNT_XREF);
|
||||||
|
$default_xref = $gedcomid ?: $PEDIGREE_ROOT_ID;
|
||||||
|
|
||||||
|
$xref = $this->getBlockSetting($block_id, 'pid', $default_xref);
|
||||||
|
$ancestorGenerations = (int) $this->getBlockSetting($block_id, 'ancestor_generations', (string) self::BLOCK_DEFAULT_ANCESTOR_GENS);
|
||||||
|
$descendantGenerations = (int) $this->getBlockSetting($block_id, 'descendant_generations', (string) self::BLOCK_DEFAULT_DESCENDANT_GENS);
|
||||||
|
$showSiblings = $this->getBlockSetting($block_id, 'show_siblings', '1') === '1';
|
||||||
|
|
||||||
|
$individual = Registry::individualFactory()->make($xref, $tree);
|
||||||
|
|
||||||
|
if (!$individual instanceof Individual) {
|
||||||
|
$content = I18N::translate('You must select an individual and a chart type in the block preferences');
|
||||||
|
|
||||||
|
if ($context !== self::CONTEXT_EMBED) {
|
||||||
|
return view('modules/block-template', [
|
||||||
|
'block' => Str::kebab($this->name()),
|
||||||
|
'id' => $block_id,
|
||||||
|
'config_url' => $this->configUrl($tree, $context, $block_id),
|
||||||
|
'title' => $this->title(),
|
||||||
|
'content' => $content,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
$individual = Auth::checkIndividualAccess($individual, false, true);
|
||||||
|
|
||||||
|
$configuration = new Configuration(
|
||||||
|
$ancestorGenerations,
|
||||||
|
$descendantGenerations,
|
||||||
|
$showSiblings,
|
||||||
|
);
|
||||||
|
|
||||||
|
$dataFacade = new DataFacade();
|
||||||
|
$treeData = $dataFacade->buildFullTree($individual, $configuration);
|
||||||
|
|
||||||
|
$title = $this->chartTitle($individual);
|
||||||
|
$content = view($this->name() . '::modules/full-diagram/block', [
|
||||||
|
'module' => $this,
|
||||||
|
'individual' => $individual,
|
||||||
|
'tree' => $tree,
|
||||||
|
'tree_data' => json_encode($treeData, JSON_THROW_ON_ERROR),
|
||||||
|
'javascript_url' => $this->assetUrl('js/full-diagram.min.js'),
|
||||||
|
'stylesheet_url' => $this->assetUrl('css/full-diagram.css'),
|
||||||
|
'block_id' => $block_id,
|
||||||
|
'ancestor_generations' => $ancestorGenerations,
|
||||||
|
'descendant_generations' => $descendantGenerations,
|
||||||
|
'show_siblings' => $showSiblings,
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($context !== self::CONTEXT_EMBED) {
|
||||||
|
return view('modules/block-template', [
|
||||||
|
'block' => Str::kebab($this->name()),
|
||||||
|
'id' => $block_id,
|
||||||
|
'config_url' => $this->configUrl($tree, $context, $block_id),
|
||||||
|
'title' => $title,
|
||||||
|
'content' => $content,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function saveBlockConfiguration(ServerRequestInterface $request, int $block_id): void
|
||||||
|
{
|
||||||
|
$xref = Validator::parsedBody($request)->isXref()->string('xref');
|
||||||
|
$ancestorGenerations = Validator::parsedBody($request)->isBetween(self::MINIMUM_GENERATIONS, self::MAXIMUM_GENERATIONS)->integer('ancestor_generations');
|
||||||
|
$descendantGenerations = Validator::parsedBody($request)->isBetween(self::MINIMUM_GENERATIONS, self::MAXIMUM_GENERATIONS)->integer('descendant_generations');
|
||||||
|
$showSiblings = Validator::parsedBody($request)->string('show_siblings', '0');
|
||||||
|
|
||||||
|
$this->setBlockSetting($block_id, 'pid', $xref);
|
||||||
|
$this->setBlockSetting($block_id, 'ancestor_generations', (string) $ancestorGenerations);
|
||||||
|
$this->setBlockSetting($block_id, 'descendant_generations', (string) $descendantGenerations);
|
||||||
|
$this->setBlockSetting($block_id, 'show_siblings', $showSiblings === '1' ? '1' : '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function editBlockConfiguration(Tree $tree, int $block_id): string
|
||||||
|
{
|
||||||
|
$PEDIGREE_ROOT_ID = $tree->getPreference('PEDIGREE_ROOT_ID');
|
||||||
|
$gedcomid = $tree->getUserPreference(Auth::user(), UserInterface::PREF_TREE_ACCOUNT_XREF);
|
||||||
|
$default_xref = $gedcomid ?: $PEDIGREE_ROOT_ID;
|
||||||
|
|
||||||
|
$xref = $this->getBlockSetting($block_id, 'pid', $default_xref);
|
||||||
|
$ancestorGenerations = (int) $this->getBlockSetting($block_id, 'ancestor_generations', (string) self::BLOCK_DEFAULT_ANCESTOR_GENS);
|
||||||
|
$descendantGenerations = (int) $this->getBlockSetting($block_id, 'descendant_generations', (string) self::BLOCK_DEFAULT_DESCENDANT_GENS);
|
||||||
|
$showSiblings = $this->getBlockSetting($block_id, 'show_siblings', '1') === '1';
|
||||||
|
|
||||||
|
$individual = Registry::individualFactory()->make($xref, $tree);
|
||||||
|
|
||||||
|
return view($this->name() . '::modules/full-diagram/block-config', [
|
||||||
|
'individual' => $individual,
|
||||||
|
'tree' => $tree,
|
||||||
|
'ancestor_generations' => $ancestorGenerations,
|
||||||
|
'descendant_generations' => $descendantGenerations,
|
||||||
|
'show_siblings' => $showSiblings,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Route handler ───────────────────────────────────────────────
|
||||||
|
|
||||||
public function handle(ServerRequestInterface $request): ResponseInterface
|
public function handle(ServerRequestInterface $request): ResponseInterface
|
||||||
{
|
{
|
||||||
$tree = Validator::attributes($request)->tree();
|
$tree = Validator::attributes($request)->tree();
|
||||||
|
|||||||
Reference in New Issue
Block a user