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