1<?php 2/** 3 * webtrees: online genealogy 4 * Copyright (C) 2019 webtrees development team 5 * This program is free software: you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation, either version 3 of the License, or 8 * (at your option) any later version. 9 * This program is distributed in the hope that it will be useful, 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 * GNU General Public License for more details. 13 * You should have received a copy of the GNU General Public License 14 * along with this program. If not, see <http://www.gnu.org/licenses/>. 15 */ 16declare(strict_types=1); 17 18namespace Fisharebest\Webtrees\Module; 19 20use Fisharebest\Webtrees\Auth; 21use Fisharebest\Webtrees\Fact; 22use Fisharebest\Webtrees\Gedcom; 23use Fisharebest\Webtrees\GedcomTag; 24use Fisharebest\Webtrees\I18N; 25use Fisharebest\Webtrees\Individual; 26use Fisharebest\Webtrees\Menu; 27use Fisharebest\Webtrees\Services\ModuleService; 28use Fisharebest\Webtrees\Tree; 29use Symfony\Component\HttpFoundation\Request; 30 31/** 32 * Trait ModuleThemeTrait - default implementation of ModuleThemeInterface 33 */ 34trait ModuleThemeTrait 35{ 36 /** @var Request */ 37 protected $request; 38 39 /** @var Tree|null */ 40 protected $tree; 41 42 /** 43 * @param Request $request 44 * @param Tree|null $tree The current tree (if there is one). 45 */ 46 public function __construct(Request $request, ?Tree $tree) 47 { 48 $this->request = $request; 49 $this->tree = $tree; 50 } 51 52 /** 53 * Add markup to the secondary menu. 54 * 55 * @return string 56 */ 57 public function formatSecondaryMenu(): string 58 { 59 return 60 '<ul class="nav wt-secondary-menu">' . 61 implode('', array_map(function (Menu $menu): string { 62 return $this->formatSecondaryMenuItem($menu); 63 }, $this->secondaryMenu())) . 64 '</ul>'; 65 } 66 67 /** 68 * Add markup to an item in the secondary menu. 69 * 70 * @param Menu $menu 71 * 72 * @return string 73 */ 74 public function formatSecondaryMenuItem(Menu $menu): string 75 { 76 return $menu->bootstrap4(); 77 } 78 79 /** 80 * Display an icon for this fact. 81 * 82 * @TODO use CSS for this 83 * 84 * @param Fact $fact 85 * 86 * @return string 87 */ 88 public function icon(Fact $fact): string 89 { 90 $asset = 'public/css/' . $this->name() . '/images/facts/' . $fact->getTag() . '.png'; 91 if (file_exists(WT_ROOT . 'public' . $asset)) { 92 return '<img src="' . e(asset($asset)) . '" title="' . GedcomTag::getLabel($fact->getTag()) . '">'; 93 } 94 95 // Spacer image - for alignment - until we move to a sprite. 96 $asset = 'public/css/' . $this->name() . '/images/facts/NULL.png'; 97 if (file_exists(WT_ROOT . 'public' . $asset)) { 98 return '<img src="' . e(asset($asset)) . '">'; 99 } 100 101 return ''; 102 } 103 104 /** 105 * Display an individual in a box - for charts, etc. 106 * 107 * @param Individual $individual 108 * 109 * @return string 110 */ 111 public function individualBox(Individual $individual): string 112 { 113 $person_box_class = self::PERSON_BOX_CLASSES[$individual->getSex()]; 114 115 if ($individual->canShow() && $individual->tree()->getPreference('SHOW_HIGHLIGHT_IMAGES')) { 116 $thumbnail = $individual->displayImage(40, 50, 'crop', []); 117 } else { 118 $thumbnail = ''; 119 } 120 121 $content = '<span class="namedef name1">' . $individual->getFullName() . '</span>'; 122 $icons = ''; 123 if ($individual->canShow()) { 124 $content = '<a href="' . e($individual->url()) . '">' . $content . '</a>' . 125 '<div class="namedef name1">' . $individual->getAddName() . '</div>'; 126 $icons = '<div class="icons">' . 127 '<span class="iconz" title="' . I18N::translate('Zoom in/out on this box.') . '">' . 128 '<span class="iconz-zoom-icon">' . view('icons/zoom-in') . '</span>' . 129 '<span class="iconz-zoom-icon d-none">' . view('icons/zoom-out') . '</span>' . 130 '</span>' . 131 '<div class="itr"><i class="icon-pedigree"></i><div class="popup">' . 132 '<ul class="' . $person_box_class . '">' . implode('', array_map(function (Menu $menu): string { 133 return $menu->bootstrap4(); 134 }, $this->individualBoxMenu($individual))) . '</ul>' . 135 '</div>' . 136 '</div>' . 137 '</div>'; 138 } 139 140 return 141 '<div data-xref="' . e($individual->xref()) . '" data-tree="' . e($individual->tree()->name()) . '" class="person_box_template ' . $person_box_class . ' box-style1" style="width: ' . $this->parameter('chart-box-x') . 'px; height: ' . $this->parameter('chart-box-y') . 'px">' . 142 $icons . 143 '<div class="chart_textbox" style="max-height:' . $this->parameter('chart-box-y') . 'px;">' . 144 $thumbnail . 145 $content . 146 '<div class="inout2 details1">' . $this->individualBoxFacts($individual) . '</div>' . 147 '</div>' . 148 '<div class="inout"></div>' . 149 '</div>'; 150 } 151 152 /** 153 * Display an empty box - for a missing individual in a chart. 154 * 155 * @return string 156 */ 157 public function individualBoxEmpty(): string 158 { 159 return '<div class="person_box_template person_boxNN box-style1" style="width: ' . $this->parameter('chart-box-x') . 'px; min-height: ' . $this->parameter('chart-box-y') . 'px"></div>'; 160 } 161 162 /** 163 * Display an individual in a box - for charts, etc. 164 * 165 * @param Individual $individual 166 * 167 * @return string 168 */ 169 public function individualBoxLarge(Individual $individual): string 170 { 171 $person_box_class = self::PERSON_BOX_CLASSES[$individual->getSex()]; 172 173 if ($individual->tree()->getPreference('SHOW_HIGHLIGHT_IMAGES')) { 174 $thumbnail = $individual->displayImage(40, 50, 'crop', []); 175 } else { 176 $thumbnail = ''; 177 } 178 179 $content = '<span class="namedef name1">' . $individual->getFullName() . '</span>'; 180 $icons = ''; 181 if ($individual->canShow()) { 182 $content = '<a href="' . e($individual->url()) . '">' . $content . '</a>' . 183 '<div class="namedef name2">' . $individual->getAddName() . '</div>'; 184 $icons = '<div class="icons">' . 185 '<span class="iconz icon-zoomin" title="' . I18N::translate('Zoom in/out on this box.') . '"></span>' . 186 '<div class="itr"><i class="icon-pedigree"></i><div class="popup">' . 187 '<ul class="' . $person_box_class . '">' . implode('', array_map(function (Menu $menu): string { 188 return $menu->bootstrap4(); 189 }, $this->individualBoxMenu($individual))) . '</ul>' . 190 '</div>' . 191 '</div>' . 192 '</div>'; 193 } 194 195 return 196 '<div data-xref="' . e($individual->xref()) . '" data-tree="' . e($individual->tree()->name()) . '" class="person_box_template ' . $person_box_class . ' box-style2">' . 197 $icons . 198 '<div class="chart_textbox" style="max-height:' . $this->parameter('chart-box-y') . 'px;">' . 199 $thumbnail . 200 $content . 201 '<div class="inout2 details2">' . $this->individualBoxFacts($individual) . '</div>' . 202 '</div>' . 203 '<div class="inout"></div>' . 204 '</div>'; 205 } 206 207 /** 208 * Display an individual in a box - for charts, etc. 209 * 210 * @param Individual $individual 211 * 212 * @return string 213 */ 214 public function individualBoxSmall(Individual $individual): string 215 { 216 $person_box_class = self::PERSON_BOX_CLASSES[$individual->getSex()]; 217 218 if ($individual->tree()->getPreference('SHOW_HIGHLIGHT_IMAGES')) { 219 $thumbnail = $individual->displayImage(40, 50, 'crop', []); 220 } else { 221 $thumbnail = ''; 222 } 223 224 return 225 '<div data-xref="' . $individual->xref() . '" class="person_box_template ' . $person_box_class . ' iconz box-style0" style="width: ' . $this->parameter('compact-chart-box-x') . 'px; min-height: ' . $this->parameter('compact-chart-box-y') . 'px">' . 226 '<div class="compact_view">' . 227 $thumbnail . 228 '<a href="' . e($individual->url()) . '">' . 229 '<span class="namedef name0">' . $individual->getFullName() . '</span>' . 230 '</a>' . 231 '<div class="inout2 details0">' . $individual->getLifeSpan() . '</div>' . 232 '</div>' . 233 '<div class="inout"></div>' . 234 '</div>'; 235 } 236 237 /** 238 * Display an individual in a box - for charts, etc. 239 * 240 * @return string 241 */ 242 public function individualBoxSmallEmpty(): string 243 { 244 return '<div class="person_box_template person_boxNN box-style1" style="width: ' . $this->parameter('compact-chart-box-x') . 'px; min-height: ' . $this->parameter('compact-chart-box-y') . 'px"></div>'; 245 } 246 247 /** 248 * Generate the facts, for display in charts. 249 * 250 * @param Individual $individual 251 * 252 * @return string 253 */ 254 public function individualBoxFacts(Individual $individual): string 255 { 256 $html = ''; 257 258 $opt_tags = preg_split('/\W/', $individual->tree()->getPreference('CHART_BOX_TAGS'), 0, PREG_SPLIT_NO_EMPTY); 259 // Show BIRT or equivalent event 260 foreach (Gedcom::BIRTH_EVENTS as $birttag) { 261 if (!in_array($birttag, $opt_tags)) { 262 $event = $individual->getFirstFact($birttag); 263 if ($event) { 264 $html .= $event->summary(); 265 break; 266 } 267 } 268 } 269 // Show optional events (before death) 270 foreach ($opt_tags as $key => $tag) { 271 if (!in_array($tag, Gedcom::DEATH_EVENTS)) { 272 $event = $individual->getFirstFact($tag); 273 if ($event !== null) { 274 $html .= $event->summary(); 275 unset($opt_tags[$key]); 276 } 277 } 278 } 279 // Show DEAT or equivalent event 280 foreach (Gedcom::DEATH_EVENTS as $deattag) { 281 $event = $individual->getFirstFact($deattag); 282 if ($event) { 283 $html .= $event->summary(); 284 if (in_array($deattag, $opt_tags)) { 285 unset($opt_tags[array_search($deattag, $opt_tags)]); 286 } 287 break; 288 } 289 } 290 // Show remaining optional events (after death) 291 foreach ($opt_tags as $tag) { 292 $event = $individual->getFirstFact($tag); 293 if ($event) { 294 $html .= $event->summary(); 295 } 296 } 297 298 return $html; 299 } 300 301 /** 302 * Links, to show in chart boxes; 303 * 304 * @param Individual $individual 305 * 306 * @return Menu[] 307 */ 308 public function individualBoxMenu(Individual $individual): array 309 { 310 $menus = array_merge( 311 $this->individualBoxMenuCharts($individual), 312 $this->individualBoxMenuFamilyLinks($individual) 313 ); 314 315 return $menus; 316 } 317 318 /** 319 * Chart links, to show in chart boxes; 320 * 321 * @param Individual $individual 322 * 323 * @return Menu[] 324 */ 325 public function individualBoxMenuCharts(Individual $individual): array 326 { 327 $menus = []; 328 foreach (app(ModuleService::class)->findByComponent('chart', $this->tree, Auth::user()) as $chart) { 329 $menu = $chart->chartBoxMenu($individual); 330 if ($menu) { 331 $menus[] = $menu; 332 } 333 } 334 335 usort($menus, function (Menu $x, Menu $y) { 336 return I18N::strcasecmp($x->getLabel(), $y->getLabel()); 337 }); 338 339 return $menus; 340 } 341 342 /** 343 * Family links, to show in chart boxes. 344 * 345 * @param Individual $individual 346 * 347 * @return Menu[] 348 */ 349 public function individualBoxMenuFamilyLinks(Individual $individual): array 350 { 351 $menus = []; 352 353 foreach ($individual->getSpouseFamilies() as $family) { 354 $menus[] = new Menu('<strong>' . I18N::translate('Family with spouse') . '</strong>', $family->url()); 355 $spouse = $family->getSpouse($individual); 356 if ($spouse && $spouse->canShowName()) { 357 $menus[] = new Menu($spouse->getFullName(), $spouse->url()); 358 } 359 foreach ($family->getChildren() as $child) { 360 if ($child->canShowName()) { 361 $menus[] = new Menu($child->getFullName(), $child->url()); 362 } 363 } 364 } 365 366 return $menus; 367 } 368 369 /** 370 * Generate a menu item to change the blocks on the current (index.php) page. 371 * 372 * @return Menu|null 373 */ 374 public function menuChangeBlocks() 375 { 376 if (Auth::check() && $this->request->get('route') === 'user-page') { 377 return new Menu(I18N::translate('Customize this page'), route('user-page-edit', ['ged' => $this->tree->name()]), 'menu-change-blocks'); 378 } 379 380 if (Auth::isManager($this->tree) && $this->request->get('route') === 'tree-page') { 381 return new Menu(I18N::translate('Customize this page'), route('tree-page-edit', ['ged' => $this->tree->name()]), 'menu-change-blocks'); 382 } 383 384 return null; 385 } 386 387 /** 388 * Generate a menu item for the control panel. 389 * 390 * @return Menu|null 391 */ 392 public function menuControlPanel() 393 { 394 if (Auth::isAdmin()) { 395 return new Menu(I18N::translate('Control panel'), route('admin-control-panel'), 'menu-admin'); 396 } 397 398 if (Auth::isManager($this->tree)) { 399 return new Menu(I18N::translate('Control panel'), route('admin-control-panel-manager'), 'menu-admin'); 400 } 401 402 return null; 403 } 404 405 /** 406 * A menu to show a list of available languages. 407 * 408 * @return Menu|null 409 */ 410 public function menuLanguages() 411 { 412 $menu = new Menu(I18N::translate('Language'), '#', 'menu-language'); 413 414 foreach (I18N::activeLocales() as $locale) { 415 $language_tag = $locale->languageTag(); 416 $class = 'menu-language-' . $language_tag . (WT_LOCALE === $language_tag ? ' active' : ''); 417 $menu->addSubmenu(new Menu($locale->endonym(), '#', $class, [ 418 'onclick' => 'return false;', 419 'data-language' => $language_tag, 420 ])); 421 } 422 423 if (count($menu->getSubmenus()) > 1) { 424 return $menu; 425 } 426 427 return null; 428 } 429 430 /** 431 * A login menu option (or null if we are already logged in). 432 * 433 * @return Menu|null 434 */ 435 public function menuLogin() 436 { 437 if (Auth::check()) { 438 return null; 439 } 440 441 // Return to this page after login... 442 $url = $this->request->getRequestUri(); 443 444 // ...but switch from the tree-page to the user-page 445 $url = str_replace('route=tree-page', 'route=user-page', $url); 446 447 return new Menu(I18N::translate('Sign in'), route('login', ['url' => $url]), 'menu-login', ['rel' => 'nofollow']); 448 } 449 450 /** 451 * A logout menu option (or null if we are already logged out). 452 * 453 * @return Menu|null 454 */ 455 public function menuLogout() 456 { 457 if (Auth::check()) { 458 return new Menu(I18N::translate('Sign out'), route('logout'), 'menu-logout'); 459 } 460 461 return null; 462 } 463 464 /** 465 * A link to allow users to edit their account settings. 466 * 467 * @return Menu|null 468 */ 469 public function menuMyAccount() 470 { 471 if (Auth::check()) { 472 return new Menu(I18N::translate('My account'), route('my-account')); 473 } 474 475 return null; 476 } 477 478 /** 479 * A link to the user's individual record (individual.php). 480 * 481 * @return Menu|null 482 */ 483 public function menuMyIndividualRecord() 484 { 485 $record = Individual::getInstance($this->tree->getUserPreference(Auth::user(), 'gedcomid'), $this->tree); 486 487 if ($record) { 488 return new Menu(I18N::translate('My individual record'), $record->url(), 'menu-myrecord'); 489 } 490 491 return null; 492 } 493 494 /** 495 * A link to the user's personal home page. 496 * 497 * @return Menu 498 */ 499 public function menuMyPage(): Menu 500 { 501 return new Menu(I18N::translate('My page'), route('user-page', ['ged' => $this->tree->name()]), 'menu-mypage'); 502 } 503 504 /** 505 * A menu for the user's personal pages. 506 * 507 * @return Menu|null 508 */ 509 public function menuMyPages() 510 { 511 if (Auth::id() && $this->tree !== null) { 512 return new Menu(I18N::translate('My pages'), '#', 'menu-mymenu', [], array_filter([ 513 $this->menuMyPage(), 514 $this->menuMyIndividualRecord(), 515 $this->menuMyPedigree(), 516 $this->menuMyAccount(), 517 $this->menuControlPanel(), 518 $this->menuChangeBlocks(), 519 ])); 520 } 521 522 return null; 523 } 524 525 /** 526 * A link to the user's individual record. 527 * 528 * @return Menu|null 529 */ 530 public function menuMyPedigree() 531 { 532 $gedcomid = $this->tree->getUserPreference(Auth::user(), 'gedcomid'); 533 534 $pedigree_chart = app(ModuleService::class)->findByComponent('chart', $this->tree, Auth::user()) 535 ->filter(function (ModuleInterface $module): bool { 536 return $module instanceof PedigreeChartModule; 537 }); 538 539 if ($gedcomid !== '' && $pedigree_chart instanceof PedigreeChartModule) { 540 return new Menu( 541 I18N::translate('My pedigree'), 542 route('pedigree', [ 543 'xref' => $gedcomid, 544 'ged' => $this->tree->name(), 545 ]), 546 'menu-mypedigree' 547 ); 548 } 549 550 return null; 551 } 552 553 /** 554 * Create a pending changes menu. 555 * 556 * @return Menu|null 557 */ 558 public function menuPendingChanges() 559 { 560 if ($this->pendingChangesExist()) { 561 $url = route('show-pending', [ 562 'ged' => $this->tree ? $this->tree->name() : '', 563 'url' => $this->request->getRequestUri(), 564 ]); 565 566 return new Menu(I18N::translate('Pending changes'), $url, 'menu-pending'); 567 } 568 569 return null; 570 } 571 572 /** 573 * Themes menu. 574 * 575 * @return Menu|null 576 */ 577 public function menuThemes() 578 { 579 $themes = app(ModuleService::class)->findByInterface(ModuleThemeInterface::class); 580 581 $current_theme = app()->make(ModuleThemeInterface::class); 582 583 if ($themes->count() > 1) { 584 $submenus = $themes->map(function (ModuleThemeInterface $theme) use ($current_theme): Menu { 585 $active = $theme->name() === $current_theme->name(); 586 $class = 'menu-theme-' . $theme->name() . ($active ? ' active' : ''); 587 588 return new Menu($theme->title(), '#', $class, [ 589 'onclick' => 'return false;', 590 'data-theme' => $theme->name(), 591 ]); 592 }); 593 594 return new Menu(I18N::translate('Theme'), '#', 'menu-theme', [], $submenus->all()); 595 } 596 597 return null; 598 } 599 600 /** 601 * Misecellaneous dimensions, fonts, styles, etc. 602 * 603 * @param string $parameter_name 604 * 605 * @return string|int|float 606 */ 607 public function parameter($parameter_name) 608 { 609 return ''; 610 } 611 612 /** 613 * Are there any pending changes for us to approve? 614 * 615 * @return bool 616 */ 617 public function pendingChangesExist(): bool 618 { 619 return $this->tree && $this->tree->hasPendingEdit() && Auth::isModerator($this->tree); 620 } 621 622 /** 623 * Generate a list of items for the main menu. 624 * 625 * @return Menu[] 626 */ 627 public function primaryMenu(): array 628 { 629 return app(ModuleService::class)->findByComponent('menu', $this->tree, Auth::user()) 630 ->map(function (ModuleMenuInterface $menu): ?Menu { 631 return $menu->getMenu($this->tree); 632 }) 633 ->filter() 634 ->all(); 635 } 636 637 /** 638 * Create the primary menu. 639 * 640 * @param Menu[] $menus 641 * 642 * @return string 643 */ 644 public function primaryMenuContent(array $menus): string 645 { 646 return implode('', array_map(function (Menu $menu): string { 647 return $menu->bootstrap4(); 648 }, $menus)); 649 } 650 651 /** 652 * Generate a list of items for the user menu. 653 * 654 * @return Menu[] 655 */ 656 public function secondaryMenu(): array 657 { 658 return array_filter([ 659 $this->menuPendingChanges(), 660 $this->menuMyPages(), 661 $this->menuThemes(), 662 $this->menuLanguages(), 663 $this->menuLogin(), 664 $this->menuLogout(), 665 ]); 666 } 667 668 /** 669 * A list of CSS files to include for this page. 670 * 671 * @return string[] 672 */ 673 public function stylesheets(): array 674 { 675 return []; 676 } 677} 678