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