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