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