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