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