Add "more descendants" indicator and fix indicator draw order
Show small child-box indicators at bottom-right of person cards when descendants exist beyond the current view. Fix both ancestor and descendant indicators to draw connecting lines behind boxes.
This commit is contained in:
278
build/full-diagram/resources/css/full-diagram.css
Normal file
278
build/full-diagram/resources/css/full-diagram.css
Normal file
@@ -0,0 +1,278 @@
|
||||
/* Full Diagram Chart Styles */
|
||||
|
||||
/* ── Container ── */
|
||||
.full-diagram-container {
|
||||
position: relative;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 0.5rem;
|
||||
background: #f8f9fa;
|
||||
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 {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.full-diagram-chart svg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* ── Person cards (SVG) ── */
|
||||
.person-card rect {
|
||||
fill: #e8e8e8;
|
||||
stroke: #b0b0b0;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.person-card.sex-m rect {
|
||||
fill: #d4e6f9;
|
||||
stroke: #7bafd4;
|
||||
}
|
||||
|
||||
.person-card.sex-f rect {
|
||||
fill: #f9d4e6;
|
||||
stroke: #d47ba8;
|
||||
}
|
||||
|
||||
.person-card.is-root rect {
|
||||
stroke: #495057;
|
||||
stroke-width: 2.5;
|
||||
filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.2));
|
||||
}
|
||||
|
||||
.person-card:hover rect {
|
||||
filter: drop-shadow(0 3px 8px rgba(0, 0, 0, 0.15));
|
||||
}
|
||||
|
||||
/* Photo placeholder */
|
||||
.person-card .photo-placeholder {
|
||||
fill: #ddd;
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.person-card.sex-m .photo-placeholder {
|
||||
fill: #b8d4ed;
|
||||
}
|
||||
|
||||
.person-card.sex-f .photo-placeholder {
|
||||
fill: #edb8d4;
|
||||
}
|
||||
|
||||
.person-card .silhouette {
|
||||
fill: rgba(255, 255, 255, 0.6);
|
||||
}
|
||||
|
||||
/* Card text */
|
||||
.person-card .person-name {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
fill: #212529;
|
||||
dominant-baseline: auto;
|
||||
}
|
||||
|
||||
.person-card .person-dates {
|
||||
font-size: 10px;
|
||||
fill: #6c757d;
|
||||
dominant-baseline: auto;
|
||||
}
|
||||
|
||||
.person-card .person-subtitle {
|
||||
font-size: 9px;
|
||||
fill: #868e96;
|
||||
font-style: italic;
|
||||
dominant-baseline: auto;
|
||||
}
|
||||
|
||||
/* ── More ancestors/descendants indicators ── */
|
||||
.more-ancestors-indicator rect,
|
||||
.more-descendants-indicator rect {
|
||||
fill: #dee2e6;
|
||||
stroke: #adb5bd;
|
||||
stroke-width: 1;
|
||||
}
|
||||
|
||||
.more-ancestors-indicator line,
|
||||
.more-descendants-indicator line {
|
||||
stroke: #adb5bd;
|
||||
stroke-width: 1.5;
|
||||
}
|
||||
|
||||
.person-card.sex-m .more-ancestors-indicator rect,
|
||||
.person-card.sex-m .more-descendants-indicator rect {
|
||||
fill: #c4d9f0;
|
||||
stroke: #7bafd4;
|
||||
}
|
||||
|
||||
.person-card.sex-f .more-ancestors-indicator rect,
|
||||
.person-card.sex-f .more-descendants-indicator rect {
|
||||
fill: #f0c4d9;
|
||||
stroke: #d47ba8;
|
||||
}
|
||||
|
||||
/* ── Connector lines ── */
|
||||
.link {
|
||||
fill: none;
|
||||
stroke: #adb5bd;
|
||||
stroke-width: 1.5;
|
||||
}
|
||||
|
||||
.couple-link {
|
||||
stroke: #868e96;
|
||||
stroke-width: 2;
|
||||
}
|
||||
|
||||
.ancestor-link {
|
||||
stroke: #adb5bd;
|
||||
}
|
||||
|
||||
.descendant-link {
|
||||
stroke: #adb5bd;
|
||||
}
|
||||
|
||||
/* ── Bio card tooltip ── */
|
||||
.bio-card {
|
||||
position: absolute;
|
||||
z-index: 1000;
|
||||
background: #fff;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
|
||||
padding: 12px;
|
||||
min-width: 220px;
|
||||
max-width: 300px;
|
||||
font-size: 12px;
|
||||
color: #212529;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.bio-header {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
margin-bottom: 8px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.bio-photo {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.bio-header-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.bio-name {
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.bio-age {
|
||||
font-size: 11px;
|
||||
color: #6c757d;
|
||||
margin-top: 2px;
|
||||
}
|
||||
|
||||
.bio-facts {
|
||||
border-top: 1px solid #eee;
|
||||
padding-top: 6px;
|
||||
}
|
||||
|
||||
.bio-fact {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
margin-bottom: 3px;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.bio-fact-label {
|
||||
font-weight: 600;
|
||||
color: #495057;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.bio-fact-label::after {
|
||||
content: ":";
|
||||
}
|
||||
|
||||
.bio-fact-value {
|
||||
color: #6c757d;
|
||||
}
|
||||
|
||||
.bio-link {
|
||||
display: block;
|
||||
margin-top: 8px;
|
||||
padding-top: 6px;
|
||||
border-top: 1px solid #eee;
|
||||
color: #4a90d9;
|
||||
text-decoration: none;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.bio-link:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/* ── Zoom controls ── */
|
||||
.zoom-controls {
|
||||
position: absolute;
|
||||
bottom: 12px;
|
||||
right: 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.zoom-controls button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border: 1px solid #dee2e6;
|
||||
border-radius: 4px;
|
||||
background: rgba(255, 255, 255, 0.9);
|
||||
color: #495057;
|
||||
font-size: 16px;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.zoom-controls button:hover {
|
||||
background: #e9ecef;
|
||||
}
|
||||
|
||||
/* ── Print styles ── */
|
||||
@media print {
|
||||
.full-diagram-container {
|
||||
border: none;
|
||||
height: auto !important;
|
||||
overflow: visible;
|
||||
}
|
||||
.zoom-controls {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
1
build/full-diagram/resources/js/full-diagram.min.js
vendored
Normal file
1
build/full-diagram/resources/js/full-diagram.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@@ -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>
|
||||
@@ -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>
|
||||
@@ -0,0 +1,63 @@
|
||||
<?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 $ancestor_generations
|
||||
* @var int $descendant_generations
|
||||
* @var bool $show_siblings
|
||||
*/
|
||||
?>
|
||||
|
||||
<link rel="stylesheet" href="<?= e($stylesheet_url) ?>">
|
||||
|
||||
<div id="full-diagram-container" class="full-diagram-container wt-chart" data-tree-name="<?= e($tree->name()) ?>">
|
||||
<div class="full-diagram-chart"></div>
|
||||
</div>
|
||||
|
||||
<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.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 src="<?= e($javascript_url) ?>"></script>
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use Fisharebest\Webtrees\Http\RequestHandlers\IndividualPage;
|
||||
use Fisharebest\Webtrees\I18N;
|
||||
use Fisharebest\Webtrees\Individual;
|
||||
use Fisharebest\Webtrees\Module\ModuleChartInterface;
|
||||
use Fisharebest\Webtrees\Tree;
|
||||
use FullDiagram\Configuration;
|
||||
|
||||
/**
|
||||
* @var string $title
|
||||
* @var Individual $individual
|
||||
* @var ModuleChartInterface $module
|
||||
* @var Tree $tree
|
||||
* @var Configuration $configuration
|
||||
* @var string $tree_data
|
||||
* @var string $javascript_url
|
||||
* @var string $stylesheet_url
|
||||
* @var int $ancestor_generations
|
||||
* @var int $descendant_generations
|
||||
* @var bool $show_siblings
|
||||
*/
|
||||
?>
|
||||
|
||||
<?= view('components/breadcrumbs', [
|
||||
'links' => [
|
||||
route(IndividualPage::class, ['tree' => $tree->name(), 'xref' => $individual->xref()]) => $individual->fullName(),
|
||||
$title,
|
||||
],
|
||||
]) ?>
|
||||
|
||||
<h2 class="wt-page-title"><?= $title ?></h2>
|
||||
|
||||
<?= view($module->name() . '::modules/full-diagram/chart', [
|
||||
'module' => $module,
|
||||
'individual' => $individual,
|
||||
'tree' => $tree,
|
||||
'tree_data' => $tree_data,
|
||||
'javascript_url' => $javascript_url,
|
||||
'stylesheet_url' => $stylesheet_url,
|
||||
'ancestor_generations' => $ancestor_generations,
|
||||
'descendant_generations' => $descendant_generations,
|
||||
'show_siblings' => $show_siblings,
|
||||
]) ?>
|
||||
Reference in New Issue
Block a user