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