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