xref: /webtrees/app/Module/ModuleThemeTrait.php (revision d5d589847982b4ed47de90592c05d01c42f71920)
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): 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(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