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