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