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