ILIAS  trunk Revision v11.0_alpha-3011-gc6b235a2e85
Renderer.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
22
32use ILIAS\UI\Implementation\Render\Template as UITemplateWrapper;
35use ILIAS\UI\Renderer as RendererInterface;
38use LogicException;
39
41{
42 public const BLOCK_MAINBAR_ENTRIES = 'trigger_item';
43 public const BLOCK_MAINBAR_TOOLS = 'tool_trigger_item';
44 public const BLOCK_METABAR_ENTRIES = 'meta_element';
45
46 private array $trigger_signals = [];
47
51 public function render(Component\Component $component, RendererInterface $default_renderer): string
52 {
53 if ($component instanceof MainBar) {
54 return $this->renderMainbar($component, $default_renderer);
55 }
56 if ($component instanceof MetaBar) {
57 return $this->renderMetabar($component, $default_renderer);
58 }
59 if ($component instanceof Footer) {
60 return $this->renderFooter($component, $default_renderer);
61 }
62 if ($component instanceof ModeInfo) {
63 return $this->renderModeInfo($component, $default_renderer);
64 }
65 if ($component instanceof Component\MainControls\SystemInfo) {
66 return $this->renderSystemInfo($component, $default_renderer);
67 }
68 $this->cannotHandleComponent($component);
69 }
70
71 protected function calculateMainBarTreePosition($pos, $slate)
72 {
73 if (!$slate instanceof Slate && !$slate instanceof MainBar) {
74 return $slate;
75 }
76 return $slate
77 ->withMainBarTreePosition($pos)
78 ->withMappedSubNodes(
79 function ($num, $slate, $is_tool = false) use ($pos) {
80 if ($is_tool) {
81 $pos = 'T';
82 }
83 return $this->calculateMainBarTreePosition("$pos:$num", $slate);
84 }
85 );
86 }
87
88 protected function renderToolEntry(
89 string $entry_id,
90 string $mb_id,
91 MainBar $component,
92 UITemplateWrapper $tpl,
93 RendererInterface $default_renderer
94 ): string {
95 $hidden = $component->getInitiallyHiddenToolIds();
96 $close_buttons = $component->getCloseButtons();
97
98 $is_removeable = array_key_exists($entry_id, $close_buttons);
99 $is_hidden = in_array($entry_id, $hidden);
100
101 if ($is_removeable) {
102 $trigger_signal = $component->getTriggerSignal($mb_id, $component::ENTRY_ACTION_REMOVE);
103 $this->trigger_signals[] = $trigger_signal;
104 $btn_removetool = $close_buttons[$entry_id]
105 ->withAdditionalOnloadCode(
106 fn($id) => "il.UI.maincontrols.mainbar.addPartIdAndEntry('$mb_id', 'remover', '$id', true);"
107 )
108 ->withOnClick($trigger_signal);
109
110 $tpl->setCurrentBlock("tool_removal");
111 $tpl->setVariable("REMOVE_TOOL", $default_renderer->render($btn_removetool));
112 $tpl->parseCurrentBlock();
113 }
114
115 $is_removeable = $is_removeable ? 'true' : 'false';
116 $is_hidden = $is_hidden ? 'true' : 'false';
117 return "il.UI.maincontrols.mainbar.addToolEntry('$mb_id', $is_removeable, $is_hidden, '$entry_id');";
118 }
119
120 protected function renderMainbarEntry(
121 array $entries,
122 string $block,
123 MainBar $component,
124 UITemplateWrapper $tpl,
125 RendererInterface $default_renderer
126 ): void {
127 $f = $this->getUIFactory();
128 foreach ($entries as $k => $entry) {
129 $button = $entry;
130 $slate = null;
131 $js = '';
132
133 if ($entry instanceof Slate) {
134 $slate = $entry;
135 $mb_id = $entry->getMainBarTreePosition();
136 $is_tool = $block === static::BLOCK_MAINBAR_TOOLS;
137 if ($is_tool) {
138 $js = $this->renderToolEntry($k, $mb_id, $component, $tpl, $default_renderer);
139 }
140
141 $trigger_signal = $component->getTriggerSignal($mb_id, $component::ENTRY_ACTION_TRIGGER);
142 $this->trigger_signals[] = $trigger_signal;
143 $button = $f->button()->bulky($entry->getSymbol(), $entry->getName(), '#')
144 ->withOnClick($trigger_signal)
145 ->withHelpTopics(...$entry->getHelpTopics());
146 } else {
147 //add Links/Buttons as toplevel entries
148 $pos = array_search($k, array_keys($entries));
149 $mb_id = '0:' . $pos;
150 $is_tool = false;
151 }
152
153 $button = $button->withAdditionalOnLoadCode(
154 function ($id) use ($js, $mb_id, $k, $is_tool): string {
155 $add_as_tool = $is_tool ? 'true' : 'false';
156 $js .= "
157 il.UI.maincontrols.mainbar.addPartIdAndEntry('$mb_id', 'triggerer', '$id', $add_as_tool);
158 il.UI.maincontrols.mainbar.addMapping('$k','$mb_id');
159 ";
160 return $js;
161 }
162 )->withAriaRole(IBulky::MENUITEM);
163
164 $tpl->setCurrentBlock($block);
165 $tpl->setVariable("BUTTON", $default_renderer->render($button));
166 $tpl->parseCurrentBlock();
167
168 if ($slate) {
169 $entry = $entry->withAriaRole(ISlate::MENU);
170
171 $tpl->setCurrentBlock("slate_item");
172 $tpl->setVariable("SLATE", $default_renderer->render($entry));
173 $tpl->parseCurrentBlock();
174 }
175 }
176 }
177
178 protected function renderMainbar(MainBar $component, RendererInterface $default_renderer): string
179 {
180 $f = $this->getUIFactory();
181 $tpl = $this->getTemplate("tpl.mainbar.html", true, true);
182
183 $tpl->setVariable("ARIA_LABEL", $this->txt('mainbar_aria_label'));
184 $more_btn_label = $this->txt('mainbar_more_label');
188 $more_slate = $f->mainControls()->slate()->combined(
189 $more_btn_label,
190 $f->symbol()->glyph()->more()
191 );
192 $more_slate = $more_slate->withAriaRole(ISlate::MENU);
193 $component = $component->withAdditionalEntry(
194 '_mb_more_entry',
195 $more_slate
196 );
197 $component = $this->calculateMainBarTreePosition("0", $component);
198
199 $mb_entries = [
200 static::BLOCK_MAINBAR_ENTRIES => $component->getEntries(),
201 static::BLOCK_MAINBAR_TOOLS => $component->getToolEntries()
202 ];
203
204 foreach ($mb_entries as $block => $entries) {
205 $this->renderMainbarEntry(
206 $entries,
207 $block,
208 $component,
209 $tpl,
210 $default_renderer
211 );
212 }
213
214 //tools-section trigger
215 if (count($component->getToolEntries()) > 0) {
216 $btn_tools = $component->getToolsButton()
217 ->withOnClick($component->getToggleToolsSignal());
218
219 $tpl->setCurrentBlock("tools_trigger");
220 $tpl->setVariable("BUTTON", $default_renderer->render($btn_tools));
221 $tpl->parseCurrentBlock();
222 }
223
224 //disengage all, close slates
225 $btn_disengage = $f->button()->bulky($f->symbol()->glyph()->collapseHorizontal("#"), $this->txt('close'), "#")
226 ->withOnClick($component->getDisengageAllSignal());
227 $tpl->setVariable("CLOSE_SLATES", $default_renderer->render($btn_disengage));
228
229
230 $id = $this->bindMainbarJS($component);
231 $tpl->setVariable('ID', $id);
232
233 return $tpl->get();
234 }
235
236 protected function renderMetabar(MetaBar $component, RendererInterface $default_renderer): string
237 {
238 $f = $this->getUIFactory();
239 $tpl = $this->getTemplate("tpl.metabar.html", true, true);
240 $active = '';
241 $signals = [
242 'entry' => $component->getEntryClickSignal(),
243 'close_slates' => $component->getDisengageAllSignal()
244 ];
245 $entries = $component->getEntries();
246
247 $more_label = $this->txt('show_more');
248 $more_symbol = $f->symbol()->glyph()->disclosure()
249 ->withCounter($f->counter()->novelty(0))
250 ->withCounter($f->counter()->status(0));
254 $more_slate = $f->mainControls()->slate()->combined($more_label, $more_symbol);
255 $more_slate = $more_slate->withAriaRole(ISlate::MENU);
256 $entries[] = $more_slate;
257
258 $this->renderTriggerButtonsAndSlates(
259 $tpl,
260 $default_renderer,
261 $signals['entry'],
262 static::BLOCK_METABAR_ENTRIES,
263 $entries,
264 $active
265 );
266
267 $component = $component->withOnLoadCode(
268 function ($id) use ($signals) {
269 $entry_signal = $signals['entry'];
270 $close_slates_signal = $signals['close_slates'];
271 return "
272 il.UI.maincontrols.metabar.init('$id');
273 il.UI.maincontrols.metabar.get('$id').registerSignals(
274 '$entry_signal',
275 '$close_slates_signal',
276 );
277 il.UI.maincontrols.metabar.get('$id').init();
278 window.addEventListener(
279 'resize',
280 ()=>{il.UI.maincontrols.metabar.get('$id').init()}
281 );
282 ";
283 }
284 );
285 $tpl->setVariable('ARIA_LABEL', $this->txt('metabar_aria_label'));
286
287 $id = $this->bindJavaScript($component);
288 $tpl->setVariable('ID', $id);
289 return $tpl->get();
290 }
291
292 protected function renderModeInfo(ModeInfo $component, RendererInterface $default_renderer): string
293 {
294 $tpl = $this->getTemplate("tpl.mode_info.html", true, true);
295 $tpl->setVariable('MODE_TITLE', $component->getModeTitle());
296 $base_URI = $component->getCloseAction()->getBaseURI();
297 $query = $component->getCloseAction()->getQuery();
298 $action = $base_URI . '?' . $query;
299 $close = $this->getUIFactory()->symbol()->glyph()->close($action);
300 $tpl->setVariable('CLOSE_GLYPH', $default_renderer->render($close));
301
302 return $tpl->get();
303 }
304
305 protected function renderSystemInfo(
306 Component\MainControls\SystemInfo $component,
307 RendererInterface $default_renderer
308 ): string {
309 $tpl = $this->getTemplate("tpl.system_info.html", true, true);
310 $tpl->setVariable('HEADLINE', $component->getHeadLine());
311 $tpl->setVariable('BODY', $component->getInformationText());
312 $tpl->setVariable('DENOTATION', $component->getDenotation());
313 switch ($component->getDenotation()) {
314 case Component\MainControls\SystemInfo::DENOTATION_NEUTRAL:
315 case Component\MainControls\SystemInfo::DENOTATION_IMPORTANT:
316 $tpl->setVariable('LIVE', 'aria-live="polite"');
317 break;
318 case Component\MainControls\SystemInfo::DENOTATION_BREAKING:
319 $tpl->setVariable('ROLE', 'role="alert"');
320 break;
321 }
322 if ($component->isDismissable()) {
323 $close = $this->getUIFactory()->symbol()->glyph()->close("#");
324 $signal = $component->getCloseSignal();
325 $close = $close->withOnClick($signal);
326 $tpl->setVariable('CLOSE_BUTTON', $default_renderer->render($close));
327 $tpl->setVariable('CLOSE_URI', (string) $component->getDismissAction());
328 $component = $component->withAdditionalOnLoadCode(fn($id) => "$(document).on('$signal', function() { il.UI.maincontrols.system_info.close('$id'); });");
329 }
330
331 $more = $this->getUIFactory()->symbol()->glyph()->more("#");
332 $tpl->setVariable('MORE_BUTTON', $default_renderer->render($more));
333
334 $component = $component->withAdditionalOnLoadCode(fn($id) => "il.UI.maincontrols.system_info.init('$id')");
335
336 $id = $this->bindJavaScript($component);
337 $tpl->setVariable('ID', $id);
338 $tpl->setVariable('ID_HEADLINE', $id . "_headline");
339 $tpl->setVariable('ID_DESCRIPTION', $id . "_description");
340
341 return $tpl->get();
342 }
343
344
346 UITemplateWrapper $tpl,
347 RendererInterface $default_renderer,
348 Signal $entry_signal,
349 string $block,
350 array $entries,
351 ?string $active = null
352 ): void {
353 foreach ($entries as $id => $entry) {
354 $use_block = $block;
355 $engaged = (string) $id === $active;
356 $slate = null;
357 if ($entry instanceof Slate) {
358 $f = $this->getUIFactory();
359 $secondary_signal = $entry->getToggleSignal();
360 $clickable = $f->button()->bulky($entry->getSymbol(), $entry->getName(), '#')
361 ->withEngagedState($engaged)
362 ->withOnClick($entry_signal)
363 ->appendOnClick($secondary_signal)
364 ->withAriaRole(IBulky::MENUITEM)
365 ->withHelpTopics(...$entry->getHelpTopics());
366
367 $slate = $entry;
368 } elseif ($entry instanceof IBulky) {
369 $clickable = $entry;
370 $clickable = $clickable->withAriaRole(IBulky::MENUITEM);
371 $slate = null;
372 } else {
373 $clickable = $entry;
374 }
375
376 $clickable_html = $default_renderer->render($clickable);
377
378 if ($slate) {
379 $tpl->setCurrentBlock("slate_item");
380 $tpl->setVariable("SLATE", $default_renderer->render($slate));
381 $tpl->parseCurrentBlock();
382 }
383
384 $tpl->setCurrentBlock($use_block);
385 $tpl->setVariable("BUTTON", $clickable_html);
386 $tpl->parseCurrentBlock();
387 }
388 }
389
390 protected function bindMainbarJS(MainBar $component): ?string
391 {
392 $trigger_signals = $this->trigger_signals;
393
394 $inititally_active = $component->getActive();
395
396 $component = $component->withOnLoadCode(
397 function ($id) use ($component, $trigger_signals, $inititally_active): string {
398 $disengage_all_signal = $component->getDisengageAllSignal();
399 $tools_toggle_signal = $component->getToggleToolsSignal();
400
401 $js = "il.UI.maincontrols.mainbar.addTriggerSignal('$disengage_all_signal');";
402 $js .= "il.UI.maincontrols.mainbar.addTriggerSignal('$tools_toggle_signal');";
403
404 foreach ($trigger_signals as $signal) {
405 $js .= "il.UI.maincontrols.mainbar.addTriggerSignal('$signal');";
406 }
407
408 foreach ($component->getToolEntries() as $k => $tool) {
409 $signal = $component->getEngageToolSignal($k);
410 $js .= "il.UI.maincontrols.mainbar.addTriggerSignal('$signal');";
411 }
412
413 $js .= "
414 window.addEventListener('resize', il.UI.maincontrols.mainbar.adjustToScreenSize);
415 il.UI.maincontrols.mainbar.init('$inititally_active');
416 ";
417 return $js;
418 }
419 );
420
421 return $this->bindJavaScript($component);
422 }
423
424 protected function renderFooter(I\Footer $component, RendererInterface $default_renderer): string
425 {
426 if (!$this->isFooterVisible($component)) {
427 return '';
428 }
429
430 $template = $this->getTemplate("tpl.footer.html", true, true);
431
432 // maybe render section 1 (permanent link):
433 $permanent_url = $component->getPermanentURL();
434 if (null !== $permanent_url) {
435 $template->setCurrentBlock('with_additional_item');
436 $template->setVariable('ITEM_CONTENT', $this->permanentLink((string) $permanent_url, $default_renderer));
437 $template->parseCurrentBlock();
438 $this->parseFooterSection($template, 'permanent-link', $this->txt('footer_permanent_link'));
439 }
440
441 // maybe render section 2 (link groups):
442 $additional_link_groups = $component->getAdditionalLinkGroups();
443 if ([] !== $additional_link_groups) {
444 $link_groups = [];
445 foreach ($additional_link_groups as [$title, $actions]) {
446 $link_groups[] = [$this->getUIFactory()->listing()->unordered($actions), $title];
447 }
448
449 $this->parseAdditionalFooterSectionItems(
450 $template,
451 $default_renderer,
452 'link-groups',
453 $this->txt('footer_link_groups'),
454 $link_groups,
455 );
456 }
457
458 // maybe render section 3 (links):
459 $additional_links = $component->getAdditionalLinks();
460 if ([] !== $additional_links) {
461 $links = array_map(static fn($link) => [$link, null], $additional_links);
462 $this->parseAdditionalFooterSectionItems(
463 $template,
464 $default_renderer,
465 'links',
466 $this->txt('footer_links'),
467 $links,
468 );
469 }
470
471 // maybe render section 4 (icons):
472 $additional_icons = $component->getAdditionalIcons();
473 if ([] !== $additional_icons) {
474 $icons = [];
475 foreach ($additional_icons as [$icon, $action]) {
476 if (null !== $action) {
477 if ($action instanceof URI) {
478 $action = (string) $action;
479 }
480 $icons[] = $this->getUIFactory()->button()->shy('', $action)->withSymbol($icon);
481 } else {
482 $icons[] = $icon;
483 }
484 }
485
486 $this->parseAdditionalFooterSectionIcons(
487 $template,
488 $default_renderer,
489 'icons',
490 $this->txt('footer_icons'),
491 $icons
492 );
493 }
494
495 // maybe render section 5 (texts):
496 $additional_texts = $component->getAdditionalTexts();
497 if ([] !== $additional_texts) {
498 $texts = array_map(static fn($text) => [$text, null], $additional_texts);
499 $this->parseAdditionalFooterSectionItems(
500 $template,
501 $default_renderer,
502 'texts',
503 $this->txt('footer_texts'),
504 $texts,
505 );
506 }
507
508 // modals are appended to the rendered footer HTML for legacy support.
509 // can be removed after Footer::withAdditionalModalAndTrigger() is.
510 return $template->get() . $default_renderer->render($component->getModals());
511 }
512
517 Template $template,
518 RendererInterface $default_renderer,
519 string $section_type,
520 string $section_label,
521 array $section_items = [],
522 ): void {
523 foreach ($section_items as [$content, $title]) {
524 $template->setCurrentBlock('with_additional_item');
525 if (null !== $title) {
526 $template->setVariable('ITEM_TITLE', $this->convertSpecialCharacters($title));
527 }
528 if ($content instanceof Component\Component) {
529 $content = $default_renderer->render($content);
530 } else {
531 $content = $this->convertSpecialCharacters($content);
532 }
533 $template->setVariable('ITEM_CONTENT', $content);
534 $template->parseCurrentBlock();
535 }
536
537 $this->parseFooterSection($template, $section_type, $section_label);
538 }
539
544 Template $template,
545 RendererInterface $default_renderer,
546 string $section_type,
547 string $section_label,
548 array $section_icons = [],
549 ): void {
550 foreach ($section_icons as $icon) {
551 $template->setCurrentBlock('with_additional_icon');
552 $template->setVariable('ICON', $default_renderer->render($icon));
553 $template->parseCurrentBlock();
554 }
555
556 $this->parseFooterSection($template, $section_type, $section_label);
557 }
558
559 protected function parseFooterSection(
560 Template $template,
561 string $section_type,
562 string $section_label,
563 ): void {
564 $template->setCurrentBlock('with_additional_section');
565 $template->setVariable('SECTION_TYPE', $section_type);
566 $template->setVariable('SECTION_LABEL', $section_label);
567 $template->parseCurrentBlock();
568 }
569
570 protected function isFooterVisible(I\Footer $component): bool
571 {
572 return $component->getPermanentURL() !== null
573 || !empty($component->getAdditionalLinkGroups())
574 || !empty($component->getAdditionalLinks())
575 || !empty($component->getAdditionalIcons())
576 || !empty($component->getAdditionalTexts());
577 }
578
582 public function registerResources(ResourceRegistry $registry): void
583 {
584 parent::registerResources($registry);
585 $registry->register('assets/js/mainbar.js');
586 $registry->register('assets/js/maincontrols.min.js');
587 $registry->register('assets/js/GS.js');
588 $registry->register('assets/js/system_info.js');
589 $registry->register('assets/js/footer.min.js');
590 }
591
592 private function permanentLink(string $permanent_url, RendererInterface $renderer): string
593 {
594 $template = $this->getTemplate("tpl.permanent-link.html", true, true);
595
596 $code = function (string $id) use ($permanent_url): string {
597 $id = $this->jsonEncode($id);
598 $perm_url = $this->jsonEncode((string) $permanent_url);
599
600 return "document.getElementById($id).addEventListener('click', e => il.Footer.permalink.copyText($perm_url)
601 .then(() => il.Footer.permalink.showTooltip(e.target.nextElementSibling, 5000)));";
602 };
603 $button = $this->getUIFactory()->button()->standard($this->txt('copy_perma_link'), '')->withAdditionalOnLoadCode($code);
604
605 $template->setVariable('PERMANENT', $renderer->render($button));
606 $template->setVariable('PERMANENT_TOOLTIP', $this->txt('perma_link_copied'));
607
608 return $template->get();
609 }
610
611 private function jsonEncode($value): string
612 {
613 return json_encode($value, JSON_HEX_QUOT | JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_THROW_ON_ERROR);
614 }
615}
$id
plugin.php for ilComponentBuildPluginInfoObjectiveTest::testAddPlugins
Definition: plugin.php:23
$renderer
The scope of this class is split ilias-conform URI's into components.
Definition: URI.php:35
renderSystemInfo(Component\MainControls\SystemInfo $component, RendererInterface $default_renderer)
Definition: Renderer.php:305
renderToolEntry(string $entry_id, string $mb_id, MainBar $component, UITemplateWrapper $tpl, RendererInterface $default_renderer)
Definition: Renderer.php:88
parseAdditionalFooterSectionItems(Template $template, RendererInterface $default_renderer, string $section_type, string $section_label, array $section_items=[],)
Definition: Renderer.php:516
renderModeInfo(ModeInfo $component, RendererInterface $default_renderer)
Definition: Renderer.php:292
renderTriggerButtonsAndSlates(UITemplateWrapper $tpl, RendererInterface $default_renderer, Signal $entry_signal, string $block, array $entries, ?string $active=null)
Definition: Renderer.php:345
renderFooter(I\Footer $component, RendererInterface $default_renderer)
Definition: Renderer.php:424
parseFooterSection(Template $template, string $section_type, string $section_label,)
Definition: Renderer.php:559
renderMainbarEntry(array $entries, string $block, MainBar $component, UITemplateWrapper $tpl, RendererInterface $default_renderer)
Definition: Renderer.php:120
parseAdditionalFooterSectionIcons(Template $template, RendererInterface $default_renderer, string $section_type, string $section_label, array $section_icons=[],)
Definition: Renderer.php:543
registerResources(ResourceRegistry $registry)
Announce resources this renderer requires.
Definition: Renderer.php:582
render(Component\Component $component, RendererInterface $default_renderer)
Definition: Renderer.php:51
permanentLink(string $permanent_url, RendererInterface $renderer)
Definition: Renderer.php:592
cannotHandleComponent(Component $component)
This method MUST be called by derived component renderers, if.
return true
This describes the MainBar.
Definition: MainBar.php:34
getCloseButtons()
Buttons to close tools; maybe configure with callback.
getToggleToolsSignal()
Signal to toggle the tools-section.
getDisengageAllSignal()
This signal disengages all slates when triggered.
This describes the MetaBar.
Definition: MetaBar.php:33
Registry for resources required by rendered output like Javascript or CSS.
register(string $name)
Add a dependency.
Interface to templating as it is used in the UI framework.
Definition: Template.php:29
setVariable(string $name, $value)
Set a variable in the current block.
get(?string $block=null)
Get the rendered template or a specific block.
setCurrentBlock(string $name)
Set the block to work on.
parseCurrentBlock()
Parse the block that is currently worked on.
An entity that renders components to a string output.
Definition: Renderer.php:31