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:
2026-03-14 19:53:12 +01:00
parent 273e398431
commit 04ff9f4711
10 changed files with 485 additions and 73 deletions
+176 -3
View File
@@ -12,32 +12,40 @@ namespace FullDiagram;
use Fig\Http\Message\RequestMethodInterface;
use Fisharebest\Webtrees\Auth;
use Fisharebest\Webtrees\Contracts\UserInterface;
use Fisharebest\Webtrees\I18N;
use Fisharebest\Webtrees\Individual;
use Fisharebest\Webtrees\Menu;
use Fisharebest\Webtrees\Module\AbstractModule;
use Fisharebest\Webtrees\Module\ModuleBlockInterface;
use Fisharebest\Webtrees\Module\ModuleBlockTrait;
use Fisharebest\Webtrees\Module\ModuleChartInterface;
use Fisharebest\Webtrees\Module\ModuleChartTrait;
use Fisharebest\Webtrees\Module\ModuleCustomInterface;
use Fisharebest\Webtrees\Module\ModuleCustomTrait;
use Fisharebest\Webtrees\Registry;
use Fisharebest\Webtrees\Tree;
use Fisharebest\Webtrees\Validator;
use Fisharebest\Webtrees\View;
use FullDiagram\Facade\DataFacade;
use Illuminate\Support\Str;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
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 ModuleCustomTrait;
use ModuleBlockTrait;
public const ROUTE_NAME = 'full-diagram';
public const ROUTE_URL = '/tree/{tree}/full-diagram/{xref}';
private const DEFAULT_ANCESTOR_GENERATIONS = 3;
private const DEFAULT_DESCENDANT_GENERATIONS = 3;
private const DEFAULT_ANCESTOR_GENERATIONS = 4;
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 MAXIMUM_GENERATIONS = 10;
@@ -80,6 +88,50 @@ class Module extends AbstractModule implements ModuleChartInterface, ModuleCusto
->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
{
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());
}
// ─── 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
{
$tree = Validator::attributes($request)->tree();