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