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