ILIAS  trunk Revision v11.0_alpha-1713-gd8962da2f67
All Data Structures Namespaces Files Functions Variables Enumerations Enumerator Modules Pages
Renderer.php
Go to the documentation of this file.
1 <?php
2 
19 declare(strict_types=1);
20 
22 
36 use ILIAS\Data\URI;
38 use 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 
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()) {
316  $tpl->setVariable('LIVE', 'aria-live="polite"');
317  break;
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 
345  protected function renderTriggerButtonsAndSlates(
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(), '#')
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 
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);
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 
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);
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()->shy($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 }
Registry for resources required by rendered output like Javascript or CSS.
permanentLink(string $permanent_url, RendererInterface $renderer)
Definition: Renderer.php:592
parseFooterSection(Template $template, string $section_type, string $section_label,)
Definition: Renderer.php:559
$renderer
This describes the MainBar.
Definition: MainBar.php:33
getDisengageAllSignal()
This signal disengages all slates when triggered.
getInitiallyHiddenToolIds()
There are tools that are rendered invisible before first activation.
renderToolEntry(string $entry_id, string $mb_id, MainBar $component, UITemplateWrapper $tpl, RendererInterface $default_renderer)
Definition: Renderer.php:88
txt(string $id)
Get a text from the language file.
setCurrentBlock(string $name)
Set the block to work on.
parseAdditionalFooterSectionIcons(Template $template, RendererInterface $default_renderer, string $section_type, string $section_label, array $section_icons=[],)
Definition: Renderer.php:543
setVariable(string $name, $value)
Set a variable in the current block.
while($session_entry=$r->fetchRow(ilDBConstants::FETCHMODE_ASSOC)) return null
getEntryClickSignal()
The Signal is triggered when any Entry is being clicked.
getDisengageAllSignal()
This signal disengages all slates when triggered.
getTemplate(string $name, bool $purge_unfilled_vars, bool $purge_unused_blocks)
Get template of component this renderer is made for.
renderSystemInfo(Component\MainControls\SystemInfo $component, RendererInterface $default_renderer)
Definition: Renderer.php:305
renderModeInfo(ModeInfo $component, RendererInterface $default_renderer)
Definition: Renderer.php:292
parseAdditionalFooterSectionItems(Template $template, RendererInterface $default_renderer, string $section_type, string $section_label, array $section_items=[],)
Definition: Renderer.php:516
withAdditionalEntry(string $id, $entry)
Append an entry.
cannotHandleComponent(Component $component)
This method MUST be called by derived component renderers, if.
getToolsButton()
Returns the button of the tools-trigger.
register(string $name)
Add a dependency.
parseCurrentBlock()
Parse the block that is currently worked on.
This describes the MetaBar.
Definition: MetaBar.php:32
renderMainbarEntry(array $entries, string $block, MainBar $component, UITemplateWrapper $tpl, RendererInterface $default_renderer)
Definition: Renderer.php:120
$id
plugin.php for ilComponentBuildPluginInfoObjectiveTest::testAddPlugins
Definition: plugin.php:23
renderTriggerButtonsAndSlates(UITemplateWrapper $tpl, RendererInterface $default_renderer, Signal $entry_signal, string $block, array $entries, ?string $active=null)
Definition: Renderer.php:345
render(Component\Component $component, RendererInterface $default_renderer)
Definition: Renderer.php:51
registerResources(ResourceRegistry $registry)
Announce resources this renderer requires.
Definition: Renderer.php:582
getToggleToolsSignal()
Signal to toggle the tools-section.
getEngageToolSignal(string $tool_id)
Signal to engage a tool from outside the MainBar.
renderFooter(I\Footer $component, RendererInterface $default_renderer)
Definition: Renderer.php:424
getCloseButtons()
Buttons to close tools; maybe configure with callback.
bindJavaScript(JavaScriptBindable $component)
Bind the component to JavaScript.