ILIAS  trunk Revision v12.0_alpha-377-g3641b37b9db
Renderer.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
22
25use ILIAS\UI\Renderer as RendererInterface;
27use LogicException;
28
30{
31 public const DEFAULT_DROPDOWN_LABEL = 'label_fieldselection';
32 public const DEFAULT_BUTTON_LABEL = 'label_fieldselection_refresh';
33 public const DEFAULT_SORTATION_DROPDOWN_LABEL = 'label_sortation';
34 public const DEFAULT_DROPDOWN_LABEL_OFFSET = 'label_pagination_offset';
35 public const DEFAULT_DROPDOWN_LABEL_LIMIT = 'label_pagination_limit';
36 public const DEFAULT_MODE_LABEL = 'label_modeviewcontrol';
37
38 public function render(Component\Component $component, RendererInterface $default_renderer): string
39 {
40 if ($component instanceof Component\Triggerer) {
41 $component = $this->addTriggererOnLoadCode($component);
42 }
43
44 switch (true) {
45 case ($component instanceof FieldSelection):
46 return $this->renderFieldSelection($component, $default_renderer);
47 case ($component instanceof Sortation):
48 return $this->renderSortation($component, $default_renderer);
49 case ($component instanceof Pagination):
50 return $this->renderPagination($component, $default_renderer);
51 case ($component instanceof Component\Input\ViewControl\Group):
52 return $default_renderer->render($component->getInputs());
53 case ($component instanceof Component\Input\ViewControl\NullControl):
54 return '';
55 case ($component instanceof Component\Input\ViewControl\Mode):
56 return $this->renderMode($component, $default_renderer);
57
58 default:
59 $this->cannotHandleComponent($component);
60 }
61 }
62
63 protected function renderFieldSelection(FieldSelection $component, RendererInterface $default_renderer): string
64 {
65 $tpl = $this->getTemplate("tpl.viewcontrol_fieldselection.html", true, true);
66 $ui_factory = $this->getUIFactory();
67
68 $set_values = $component->getValue() ?? [];
69 foreach ($component->getOptions() as $opt_value => $opt_label) {
70 $tpl->setCurrentBlock("option");
71 $tpl->setVariable("OPTION_ID", $this->getJavascriptBinding()->createId());
72 $tpl->setVariable("OPTION_VALUE", $opt_value);
73 $tpl->setVariable("OPTION_LABEL", $opt_label);
74 if (in_array($opt_value, $set_values)) {
75 $tpl->setVariable("CHECKED", 'checked');
76 }
77 $tpl->parseCurrentBlock();
78
79 if (in_array($opt_value, $set_values)) {
80 $tpl->setCurrentBlock("value");
81 $tpl->setVariable("NAME", $component->getName());
82 $tpl->setVariable("VALUE", $opt_value);
83 $tpl->parseCurrentBlock();
84 }
85 }
86
87 $internal_signal = $component->getInternalSignal();
88 $param_name = $component->getName();
89 if ($container_submit_signal = $component->getOnChangeSignal()) {
90 $component = $component->withAdditionalOnLoadCode(
91 fn($id) => "il.UI.Input.Viewcontrols.FieldSelection.init(
92 document.getElementById('{$id}'),
93 '{$internal_signal}',
94 '{$container_submit_signal}',
95 '{$param_name}'
96 );"
97 );
98 }
99
100 $component = $component
102 fn($id) =>
103 "$('#{$id} > .dropdown-menu').on('click', (event) => event.stopPropagation());"
104 )->withAdditionalOnLoadCode(
105 fn($id) =>
106 "il.UI.dropdown.init(document.getElementById('{$id}'));"
107 );
108
109 $id = $this->bindJavaScript($component);
110 $container_submit_signal = $component->getOnChangeSignal();
111 $button_label = $component->getButtonLabel() !== '' ?
112 $component->getButtonLabel() : $this->txt(self::DEFAULT_BUTTON_LABEL);
113 $button = $ui_factory->button()->standard($button_label, '#')
114 ->withOnClick($internal_signal);
115
116 $tpl->setVariable('ID', $id);
117 $tpl->setVariable("ID_MENU", $id . '_ctrl');
118 $tpl->setVariable("ARIA_LABEL", $this->txt(self::DEFAULT_DROPDOWN_LABEL));
119 $tpl->setVariable("BUTTON", $default_renderer->render($button));
120
121 return $tpl->get();
122 }
123
124 protected function renderSortation(Sortation $component, RendererInterface $default_renderer): string
125 {
126 $tpl = $this->getTemplate("tpl.viewcontrol_sortation.html", true, true);
127 $ui_factory = $this->getUIFactory();
128
129 foreach ($component->getOptions() as $opt_label => $order) {
130 $opt_value = $order->join(':', fn($ret, $key, $value) => [$key, $value]);
131 $internal_signal = $component->getInternalSignal();
132 $internal_signal->addOption('value', $opt_value);
133 $item = $ui_factory->button()->shy((string) $opt_label, '#')
134 ->withOnClick($internal_signal);
135 $tpl->setCurrentBlock("option");
136 $tpl->setVariable("OPTION", $default_renderer->render($item));
137
138 if ($opt_value === $component->getValue()) {
139 $tpl->touchBlock("selected");
140 $tpl->setCurrentBlock("option");
141 }
142 $tpl->parseCurrentBlock();
143 }
144
145 if ($container_submit_signal = $component->getOnChangeSignal()) {
146 $component = $component->withAdditionalOnLoadCode(
147 fn($id) => "il.UI.Input.Viewcontrols.Sortation.init(
148 document.getElementById('{$id}'),
149 '{$internal_signal}',
150 '{$container_submit_signal}',
151 );"
152 );
153 }
154
155 $component = $component->withAdditionalOnLoadCode(
156 fn($id) => "il.UI.dropdown.init(document.getElementById('{$id}'));"
157 );
158
159 $id = $this->bindJavaScript($component);
160
161 $tpl->setVariable('ID', $id);
162 $tpl->setVariable("ID_MENU", $id . '_ctrl');
163 $tpl->setVariable("ARIA_LABEL", $this->txt(self::DEFAULT_SORTATION_DROPDOWN_LABEL));
164
165 $tpl->setVariable(
166 "VALUES",
167 $default_renderer->render(
168 $component->getInputGroup()
169 )
170 );
171
172 return $tpl->get();
173 }
174
178 protected function buildRanges(
179 int $total_count,
180 int $page_limit
181 ): array {
182 $data_factory = $this->getDataFactory();
183 if ($page_limit >= $total_count) {
184 return [$data_factory->range(0, $page_limit)];
185 }
186 foreach (range(0, $total_count - 1, $page_limit) as $idx => $start) {
187 $ranges[] = $data_factory->range($start, $page_limit);
188 }
189 return $ranges;
190 }
191
195 protected function findCurrentPage(array $ranges, int $offset): int
196 {
197 foreach ($ranges as $idx => $range) {
198 if ($offset >= $range->getStart() && $offset < $range->getEnd()) {
199 return $idx;
200 }
201 }
202 throw new LogicException('offset is not in any range');
203 }
204
209 protected function sliceRangesToVisibleEntries(array $ranges, int $current, int $number_of_visible_entries): array
210 {
211 $first = reset($ranges);
212 $last = end($ranges);
213
214 $start = max(0, $current - floor(($number_of_visible_entries - 1) / 2));
215 if ($start + $number_of_visible_entries >= count($ranges)) {
216 $start = max(0, count($ranges) - $number_of_visible_entries);
217 }
218
219 $entries = array_slice($ranges, (int) $start, $number_of_visible_entries);
220
221 if (! in_array($first, $entries)) {
222 array_shift($entries);
223 array_unshift($entries, $first);
224 }
225 if (! in_array($last, $entries)) {
226 array_pop($entries);
227 array_push($entries, $last);
228 }
229 return $entries;
230 }
231
232 protected function renderPagination(Pagination $component, RendererInterface $default_renderer): string
233 {
234 $tpl = $this->getTemplate("tpl.viewcontrol_pagination.html", true, true);
235 $ui_factory = $this->getUIFactory();
236 $internal_signal = $component->getInternalSignal();
237 $limit_options = $component->getLimitOptions();
238 $total_count = $component->getTotalCount();
239
240 list(Pagination::FNAME_OFFSET => $offset, Pagination::FNAME_LIMIT => $limit) = array_map('intval', $component->getValue());
241 $limit = $limit > 0 ? $limit : reset($limit_options);
242 $offset = $offset >= $total_count ? 0 : $offset;
243
244 if (! $total_count) {
245 $input = $ui_factory->input()->field()->numeric('offset')->withValue($offset);
246 $apply = $ui_factory->button()->standard('apply', '');
247 $tpl->setVariable("INPUT", $default_renderer->render($input));
248 $tpl->setVariable("BUTTON", $default_renderer->render($apply));
249 } else {
250 $ranges = $this->buildRanges($total_count, $limit);
251 $current = $this->findCurrentPage($ranges, $offset);
252
253 if ($limit >= $total_count) {
254 $entries = $ranges;
255 } else {
256 $entries = $this->sliceRangesToVisibleEntries($ranges, $current, $component->getNumberOfVisibleEntries());
257 }
258
259 if (count($entries) > 1) {
260 foreach ($ranges as $idx => $range) {
261 if (in_array($range, $entries)) {
262 $signal = clone $internal_signal;
263 $signal->addOption('offset', $range->getStart());
264 $signal->addOption('limit', $limit);
265 $tpl->setCurrentBlock("entry");
266 $entry = $ui_factory->button()->shy((string) ($idx + 1), '#')->withOnClick($signal);
267 if ($idx === $current) {
268 $entry = $entry->withEngagedState(true);
269 }
270 $tpl->setVariable("ENTRY", $default_renderer->render($entry));
271 $tpl->parseCurrentBlock();
272 } elseif ($idx === 1 || $idx === count($ranges) - 2) {
273 $tpl->setCurrentBlock("entry");
274 $tpl->touchBlock("spacer");
275 $tpl->parseCurrentBlock();
276 }
277 }
278
279 if ($current > 0) {
280 $range = $ranges[$current - 1];
281 $signal = clone $internal_signal;
282 $signal->addOption('offset', $range->getStart());
283 $signal->addOption('limit', $limit);
284 $btn_left = $ui_factory->button()->shy('', $signal ?? '#')
285 ->withSymbol($ui_factory->symbol()->glyph()->back());
286 $tpl->setVariable("LEFT_ROCKER", $default_renderer->render($btn_left));
287 }
288
289 if ($current < count($ranges) - 1) {
290 $range = $ranges[$current + 1];
291 $signal = clone $internal_signal;
292 $signal->addOption('offset', $range->getStart());
293 $signal->addOption('limit', $limit);
294 $btn_right = $ui_factory->button()->shy('', $signal ?? '#')
295 ->withSymbol($ui_factory->symbol()->glyph()->next());
296 $tpl->setVariable("RIGHT_ROCKER", $default_renderer->render($btn_right));
297 }
298 }
299 }
300
301 foreach ($component->getLimitOptions() as $option) {
302 $signal = clone $internal_signal;
303 $signal->addOption('offset', $offset);
304 $signal->addOption('limit', (string) $option);
305 $option_label = $option === \PHP_INT_MAX ? $this->txt('ui_pagination_unlimited') : (string) $option;
306
307 $item = $ui_factory->button()->shy($option_label, '#')
308 ->withOnClick($signal);
309 $tpl->setCurrentBlock("option_limit");
310 $tpl->setVariable("OPTION_LIMIT", $default_renderer->render($item));
311 if ($option === $limit) {
312 $tpl->touchBlock("selected");
313 $tpl->setCurrentBlock("option_limit");
314 }
315 $tpl->parseCurrentBlock();
316 }
317
318 if ($container_submit_signal = $component->getOnChangeSignal()) {
319 $component = $component->withAdditionalOnLoadCode(
320 fn($id) => "il.UI.Input.Viewcontrols.Pagination.init(
321 document.getElementById('{$id}'),
322 '{$internal_signal}',
323 '{$container_submit_signal}',
324 );"
325 );
326 }
327
328 $component = $component->withAdditionalOnLoadCode(
329 fn($id) => "
330 il.UI.dropdown.init(
331 document.getElementById('{$id}').querySelector(
332 '.dropdown.il-viewcontrol-pagination__num-of-items'
333 )
334 );
335 "
336 );
337 $id = $this->bindJavaScript($component);
338
339 $tpl->setVariable('ID', $id);
340 $tpl->setVariable("ID_MENU_OFFSET", $id . '_ctrl_offset');
341 $tpl->setVariable("ARIA_LABEL_OFFSET", $this->txt(self::DEFAULT_DROPDOWN_LABEL_OFFSET));
342 $tpl->setVariable("ID_MENU_LIMIT", $id . '_ctrl_limit');
343 $tpl->setVariable("ARIA_LABEL_LIMIT", $this->txt(self::DEFAULT_DROPDOWN_LABEL_LIMIT));
344
345 $tpl->setVariable(
346 "VALUES",
347 $default_renderer->render(
348 $component->getInputGroup()
349 )
350 );
351
352 return $tpl->get();
353 }
354
355
356 protected function renderMode(Mode $component, RendererInterface $default_renderer): string
357 {
358 $tpl = $this->getTemplate("tpl.viewcontrol_mode.html", true, true);
359 $ui_factory = $this->getUIFactory();
360
361 $container_submit_signal = $component->getOnChangeSignal();
362 $options = $component->getOptions();
363 $set_value = $component->getValue() ?? array_key_first($options);
364
365 $out = [];
366 foreach ($options as $opt_value => $opt_label) {
367 $out[] = $ui_factory->button()->standard($opt_label, '#')
368 ->withEngagedState($opt_value === $set_value)
369 ->withOnLoadCode(
370 static fn($id): string =>
371 "il.UI.Input.Viewcontrols.Mode.init(
372 document.getElementById('{$id}'),
373 '{$opt_value}',
374 '{$container_submit_signal}',
375 );"
376 );
377 }
378 $tpl->setVariable('BUTTONS', $default_renderer->render($out));
379 $tpl->setVariable('VALUE', $set_value);
380 $tpl->setVariable("NAME", $component->getName());
381 $tpl->setVariable("ARIA_LABEL", $this->txt(self::DEFAULT_MODE_LABEL));
382
383 $id = $this->bindJavaScript($component);
384 return $tpl->get();
385 }
386
390 public function registerResources(ResourceRegistry $registry): void
391 {
392 parent::registerResources($registry);
393 $registry->register('assets/js/dropdown.js');
394 $registry->register('assets/js/input.viewcontrols.min.js');
395 }
396}
$id
plugin.php for ilComponentBuildPluginInfoObjectiveTest::testAddPlugins
Definition: plugin.php:23
$out
Definition: buildRTE.php:24
This implements commonalities between inputs.
Definition: Input.php:43
getName()
The name of the input as used in HTML.
Definition: Input.php:192
getValue()
Get the value that is displayed in the input client side.
Definition: Input.php:89
sliceRangesToVisibleEntries(array $ranges, int $current, int $number_of_visible_entries)
Definition: Renderer.php:209
renderMode(Mode $component, RendererInterface $default_renderer)
Definition: Renderer.php:356
registerResources(ResourceRegistry $registry)
Announce resources this renderer requires.
Definition: Renderer.php:390
renderFieldSelection(FieldSelection $component, RendererInterface $default_renderer)
Definition: Renderer.php:63
renderPagination(Pagination $component, RendererInterface $default_renderer)
Definition: Renderer.php:232
render(Component\Component $component, RendererInterface $default_renderer)
Definition: Renderer.php:38
renderSortation(Sortation $component, RendererInterface $default_renderer)
Definition: Renderer.php:124
cannotHandleComponent(Component $component)
This method MUST be called by derived component renderers, if.
bindJavaScript(JavaScriptBindable $component)
Bind the component to JavaScript.
addTriggererOnLoadCode(Triggerer $triggerer)
Add onload-code for triggerer.
getTemplate(string $name, bool $purge_unfilled_vars, bool $purge_unused_blocks)
Get template of component this renderer is made for.
Groups are a special kind of input because they are a monoid operation.
Definition: Group.php:38
withAdditionalOnLoadCode(Closure $binder)
Add some onload-code to the component instead of replacing the existing one.
Registry for resources required by rendered output like Javascript or CSS.
register(string $name)
Add a dependency.
An entity that renders components to a string output.
Definition: Renderer.php:31
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
Definition: Factory.php:21