1<?php
2
3/* Copyright (c) 2018 Nils Haagen <nils.haagen@concepts.and-training.de> Extended GPL, see docs/LICENSE */
4
5namespace ILIAS\UI\Implementation\Component\MainControls;
6
7use ILIAS\UI\Implementation\Render\AbstractComponentRenderer;
8use ILIAS\UI\Renderer as RendererInterface;
9use ILIAS\UI\Component;
10use ILIAS\UI\Component\Signal;
11use ILIAS\UI\Component\MainControls\MainBar;
12use ILIAS\UI\Component\MainControls\MetaBar;
13use ILIAS\UI\Component\MainControls\Slate\Slate;
14use ILIAS\UI\Component\MainControls\Footer;
15use ILIAS\UI\Implementation\Component\Button\Bulky as IBulky;
16use ILIAS\UI\Implementation\Component\MainControls\Slate\Slate as ISlate;
17use ILIAS\UI\Implementation\Render\Template as UITemplateWrapper;
18use ILIAS\Data\URI;
19
20class Renderer extends AbstractComponentRenderer
21{
22    const BLOCK_MAINBAR_ENTRIES = 'trigger_item';
23    const BLOCK_MAINBAR_TOOLS = 'tool_trigger_item';
24    const BLOCK_METABAR_ENTRIES = 'meta_element';
25
26    private $signals_for_tools = [];
27    private $trigger_signals = [];
28
29    /**
30     * @inheritdoc
31     */
32    public function render(Component\Component $component, RendererInterface $default_renderer)
33    {
34        $this->checkComponent($component);
35
36        if ($component instanceof MainBar) {
37            return $this->renderMainbar($component, $default_renderer);
38        }
39        if ($component instanceof MetaBar) {
40            return $this->renderMetabar($component, $default_renderer);
41        }
42        if ($component instanceof Footer) {
43            return $this->renderFooter($component, $default_renderer);
44        }
45        if ($component instanceof ModeInfo) {
46            return $this->renderModeInfo($component, $default_renderer);
47        }
48    }
49
50    protected function calculateMainBarTreePosition($pos, $slate)
51    {
52        if (!$slate instanceof Slate && !$slate instanceof MainBar) {
53            return $slate;
54        }
55        return $slate
56            ->withMainBarTreePosition($pos)
57            ->withMappedSubNodes(
58                function ($num, $slate, $is_tool = false) use ($pos) {
59                    if ($is_tool) {
60                        $pos = 'T';
61                    }
62                    return $this->calculateMainBarTreePosition("$pos:$num", $slate);
63                }
64            );
65    }
66
67    protected function renderToolEntry(
68        Slate $entry,
69        string $entry_id,
70        string $mb_id,
71        MainBar $component,
72        UITemplateWrapper $tpl,
73        RendererInterface $default_renderer
74    ) : string {
75        $hidden = $component->getInitiallyHiddenToolIds();
76        $close_buttons = $component->getCloseButtons();
77
78        $is_removeable = array_key_exists($entry_id, $close_buttons);
79        $is_hidden = in_array($entry_id, $hidden);
80
81        if ($is_removeable) {
82            $trigger_signal = $component->getTriggerSignal($mb_id, $component::ENTRY_ACTION_REMOVE);
83            $this->trigger_signals[] = $trigger_signal;
84            $btn_removetool = $close_buttons[$entry_id]
85               ->withAdditionalOnloadCode(
86                   function ($id) use ($mb_id) {
87                       return "il.UI.maincontrols.mainbar.addPartIdAndEntry('{$mb_id}', 'remover', '{$id}', true);";
88                   }
89               )
90                ->withOnClick($trigger_signal);
91
92            $tpl->setCurrentBlock("tool_removal");
93            $tpl->setVariable("REMOVE_TOOL", $default_renderer->render($btn_removetool));
94            $tpl->parseCurrentBlock();
95        }
96
97        $is_removeable = $is_removeable ? 'true':'false';
98        $is_hidden = $is_hidden ? 'true':'false';
99        return "il.UI.maincontrols.mainbar.addToolEntry('{$mb_id}', {$is_removeable}, {$is_hidden}, '{$entry_id}');";
100    }
101
102    protected function renderMainbarEntry(
103        array $entries,
104        string $block,
105        MainBar $component,
106        UITemplateWrapper $tpl,
107        RendererInterface $default_renderer
108    ) {
109        $f = $this->getUIFactory();
110        foreach ($entries as $k => $entry) {
111            $button = $entry;
112            $slate = null;
113            $js = '';
114
115            if ($entry instanceof Slate) {
116                $slate = $entry;
117                $mb_id = $entry->getMainBarTreePosition();
118                $is_tool = $block === static::BLOCK_MAINBAR_TOOLS;
119                if ($is_tool) {
120                    $js = $this->renderToolEntry($entry, $k, $mb_id, $component, $tpl, $default_renderer);
121                }
122
123                $trigger_signal = $component->getTriggerSignal($mb_id, $component::ENTRY_ACTION_TRIGGER);
124                $this->trigger_signals[] = $trigger_signal;
125                $button = $f->button()->bulky($entry->getSymbol(), $entry->getName(), '#')
126                    ->withAriaRole(IBulky::MENUITEM)
127                    ->withOnClick($trigger_signal);
128            } else {
129                //add Links/Buttons as toplevel entries
130                $pos = array_search($k, array_keys($entries));
131                $mb_id = '0:' . $pos;
132                $is_tool = false;
133            }
134
135            $button = $button->withAdditionalOnLoadCode(
136                function ($id) use ($js, $mb_id, $k, $is_tool) {
137                    $add_as_tool = $is_tool ? 'true':'false';
138                    $js .= "
139                        il.UI.maincontrols.mainbar.addPartIdAndEntry('{$mb_id}', 'triggerer', '{$id}', {$add_as_tool});
140                        il.UI.maincontrols.mainbar.addMapping('{$k}','{$mb_id}');
141                    ";
142                    return $js;
143                }
144            );
145            $tpl->setCurrentBlock($block);
146            $tpl->setVariable("BUTTON", $default_renderer->render($button));
147            $tpl->parseCurrentBlock();
148
149            if ($slate) {
150                $entry = $entry->withAriaRole(ISlate::MENU);
151
152                $tpl->setCurrentBlock("slate_item");
153                $tpl->setVariable("SLATE", $default_renderer->render($entry));
154                $tpl->parseCurrentBlock();
155            }
156        }
157    }
158
159    protected function renderMainbar(MainBar $component, RendererInterface $default_renderer)
160    {
161        $f = $this->getUIFactory();
162        $tpl = $this->getTemplate("tpl.mainbar.html", true, true);
163
164        $tpl->setVariable("ARIA_LABEL", $this->txt('mainbar_aria_label'));
165
166        //add "more"-slate
167        $more_slate = $f->maincontrols()->slate()->combined(
168            $component->getMoreButton()->getLabel(),
169            $f->symbol()->glyph()->more()
170        )->withAriaRole(ISlate::MENU);
171        $component = $component->withAdditionalEntry(
172            '_mb_more_entry',
173            $more_slate
174        );
175
176        $component = $this->calculateMainBarTreePosition("0", $component);
177
178        $mb_entries = [
179            static::BLOCK_MAINBAR_ENTRIES => $component->getEntries(),
180            static::BLOCK_MAINBAR_TOOLS => $component->getToolEntries()
181        ];
182
183        foreach ($mb_entries as $block => $entries) {
184            $this->renderMainbarEntry(
185                $entries,
186                $block,
187                $component,
188                $tpl,
189                $default_renderer
190            );
191        }
192
193        //tools-section trigger
194        if (count($component->getToolEntries()) > 0) {
195            $btn_tools = $component->getToolsButton()
196                ->withOnClick($component->getToggleToolsSignal())
197                ->withAriaRole(IBulky::MENUITEM);
198
199            $tpl->setCurrentBlock("tools_trigger");
200            $tpl->setVariable("BUTTON", $default_renderer->render($btn_tools));
201            $tpl->parseCurrentBlock();
202        }
203
204        //disengage all, close slates
205        $btn_disengage = $f->button()->bulky($f->symbol()->glyph()->back("#"), "close", "#")
206            ->withOnClick($component->getDisengageAllSignal());
207        $tpl->setVariable("CLOSE_SLATES", $default_renderer->render($btn_disengage));
208
209
210        $id = $this->bindMainbarJS($component);
211        $tpl->setVariable('ID', $id);
212
213        return $tpl->get();
214    }
215
216    protected function renderMetabar(MetaBar $component, RendererInterface $default_renderer)
217    {
218        $f = $this->getUIFactory();
219        $tpl = $this->getTemplate("tpl.metabar.html", true, true);
220        $active = '';
221        $signals = [
222            'entry' => $component->getEntryClickSignal(),
223            'close_slates' => $component->getDisengageAllSignal()
224        ];
225        $entries = $component->getEntries();
226
227        $more_label = 'more';
228        $more_symbol = $f->symbol()->glyph()->disclosure()
229            ->withCounter($f->counter()->novelty(0))
230            ->withCounter($f->counter()->status(0));
231        $more_slate = $f->maincontrols()->slate()->combined($more_label, $more_symbol, $f->legacy(''))
232            ->withAriaRole(ISlate::MENU);
233        $entries[] = $more_slate;
234
235        $this->renderTriggerButtonsAndSlates(
236            $tpl,
237            $default_renderer,
238            $signals['entry'],
239            static::BLOCK_METABAR_ENTRIES,
240            $entries,
241            $active
242        );
243
244        $component = $component->withOnLoadCode(
245            function ($id) use ($signals) {
246                $entry_signal = $signals['entry'];
247                $close_slates_signal = $signals['close_slates'];
248                return "
249					il.UI.maincontrols.metabar.registerSignals(
250						'{$id}',
251						'{$entry_signal}',
252						'{$close_slates_signal}',
253					);
254					il.UI.maincontrols.metabar.init();
255					$(window).resize(il.UI.maincontrols.metabar.init);
256				";
257            }
258        );
259        $tpl->setVariable('ARIA_LABEL', $this->txt('metabar_aria_label'));
260
261        $id = $this->bindJavaScript($component);
262        $tpl->setVariable('ID', $id);
263        return $tpl->get();
264    }
265
266    protected function renderModeInfo(ModeInfo $component, RendererInterface $default_renderer)
267    {
268        $tpl = $this->getTemplate("tpl.mode_info.html", true, true);
269        $tpl->setVariable('MODE_TITLE', $component->getModeTitle());
270        $base_URI = $component->getCloseAction()->getBaseURI();
271        $query = $component->getCloseAction()->getQuery();
272        $action = $base_URI . '?' . $query;
273        $close = $this->getUIFactory()->symbol()->glyph()->close($action);
274        $tpl->setVariable('CLOSE_GLYPH', $default_renderer->render($close));
275
276        return $tpl->get();
277    }
278
279
280    protected function renderTriggerButtonsAndSlates(
281        UITemplateWrapper $tpl,
282        RendererInterface $default_renderer,
283        Signal $entry_signal,
284        string $block,
285        array $entries,
286        string $active = null,
287        array $initially_hidden_ids = [],
288        array $close_buttons = [],
289        Signal $tool_removal_signal = null
290    ) {
291        foreach ($entries as $id => $entry) {
292            $use_block = $block;
293            $engaged = (string) $id === $active;
294
295            if ($entry instanceof Slate) {
296                $f = $this->getUIFactory();
297                $secondary_signal = $entry->getToggleSignal();
298                $button = $f->button()->bulky($entry->getSymbol(), $entry->getName(), '#')
299                    ->withOnClick($entry_signal)
300                    ->appendOnClick($secondary_signal)
301                    ->withEngagedState($engaged)
302                    ->withAriaRole(IBulky::MENUITEM);
303
304                $slate = $entry;
305            } else {
306                $button = $entry;
307                $slate = null;
308            }
309
310            $button_html = $default_renderer->render($button);
311
312            $tpl->setCurrentBlock($use_block);
313            $tpl->setVariable("BUTTON", $button_html);
314            $tpl->parseCurrentBlock();
315
316            if ($slate) {
317                $slate = $slate->withAriaRole(ISlate::MENU);
318
319                $tpl->setCurrentBlock("slate_item");
320                $tpl->setVariable("SLATE", $default_renderer->render($slate));
321                $tpl->parseCurrentBlock();
322            }
323        }
324    }
325
326    protected function bindMainbarJS(MainBar $component) : string
327    {
328        $trigger_signals = $this->trigger_signals;
329
330        $inititally_active = $component->getActive();
331
332        $component = $component->withOnLoadCode(
333            function ($id) use ($component, $trigger_signals, $inititally_active) {
334                $disengage_all_signal = $component->getDisengageAllSignal();
335                $tools_toggle_signal = $component->getToggleToolsSignal();
336
337                $js = "il.UI.maincontrols.mainbar.addTriggerSignal('{$disengage_all_signal}');";
338                $js .= "il.UI.maincontrols.mainbar.addTriggerSignal('{$tools_toggle_signal}');";
339
340                foreach ($trigger_signals as $signal) {
341                    $js .= "il.UI.maincontrols.mainbar.addTriggerSignal('{$signal}');";
342                }
343
344                foreach ($component->getToolEntries() as $k => $tool) {
345                    $signal = $component->getEngageToolSignal($k);
346                    $js .= "il.UI.maincontrols.mainbar.addTriggerSignal('{$signal}');";
347                }
348
349                $js .= "
350                    $(window).resize(il.UI.maincontrols.mainbar.adjustToScreenSize);
351                    il.UI.maincontrols.mainbar.init('{$inititally_active}');
352                ";
353                return $js;
354            }
355        );
356
357        $id = $this->bindJavaScript($component);
358        return $id;
359    }
360
361    protected function renderFooter(Footer $component, RendererInterface $default_renderer)
362    {
363        $tpl = $this->getTemplate("tpl.footer.html", true, true);
364        $links = $component->getLinks();
365        if ($links) {
366            $link_list = $this->getUIFactory()->listing()->unordered($links);
367            $tpl->setVariable('LINKS', $default_renderer->render($link_list));
368        }
369
370        $tpl->setVariable('TEXT', $component->getText());
371
372        $perm_url = $component->getPermanentURL();
373        if ($perm_url instanceof URI) {
374            // Since URI::__toString() is only available in ILIAS >= 7 we have to do that on our own...
375            $uri = $perm_url->getBaseURI();
376            $query = $perm_url->getQuery();
377            if ($query) {
378                $uri .= '?' . $query;
379            }
380            $fragment = $perm_url->getFragment();
381            if ($fragment) {
382                $uri .= '#' . $fragment;
383            }
384
385            $tpl->setVariable('PERMA_LINK_LABEL', $this->txt('perma_link'));
386            $tpl->setVariable('PERMANENT_URL', $uri);
387        }
388        return $tpl->get();
389    }
390
391    /**
392     * @inheritdoc
393     */
394    public function registerResources(\ILIAS\UI\Implementation\Render\ResourceRegistry $registry)
395    {
396        parent::registerResources($registry);
397        $registry->register('./src/UI/templates/js/MainControls/mainbar.js');
398        $registry->register('./src/UI/templates/js/MainControls/metabar.js');
399        $registry->register('./src/GlobalScreen/Client/dist/GS.js');
400        $registry->register('./src/UI/templates/js/MainControls/footer.js');
401    }
402
403    /**
404     * @inheritdoc
405     */
406    protected function getComponentInterfaceName()
407    {
408        return array(
409            MetaBar::class,
410            MainBar::class,
411            Footer::class,
412            ModeInfo::class
413        );
414    }
415}
416