ILIAS  trunk Revision v12.0_alpha-1540-g00f839d5fa1
Renderer.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
22
30use ILIAS\UI\Renderer as RendererInterface;
32use LogicException;
33use Closure;
41use ILIAS\UI\Implementation\Component\ComponentHelper;
42
48{
49 use ComponentHelper;
50
51 public const DATETIME_DATEPICKER_MINMAX_FORMAT = 'Y-m-d\Th:m';
52 public const DATE_DATEPICKER_MINMAX_FORMAT = 'Y-m-d';
53 public const TYPE_DATE = 'date';
54 public const TYPE_DATETIME = 'datetime-local';
55 public const TYPE_TIME = 'time';
56 public const HTML5_NATIVE_DATETIME_FORMAT = 'Y-m-d H:i';
57 public const HTML5_NATIVE_DATE_FORMAT = 'Y-m-d';
58 public const HTML5_NATIVE_TIME_FORMAT = 'H:i';
59
61 'd' => 'DD',
62 'jS' => 'Do',
63 'l' => 'dddd',
64 'D' => 'dd',
65 'S' => 'o',
66 'i' => 'mm',
67 'W' => '',
68 'm' => 'MM',
69 'F' => 'MMMM',
70 'M' => 'MMM',
71 'Y' => 'YYYY',
72 'y' => 'YY'
73 ];
74
81 protected const FILE_UPLOAD_CHUNK_SIZE_FACTOR = 0.9;
82
83 private const CENTUM = 100;
84
88 public function render(Component\Component $component, RendererInterface $default_renderer): string
89 {
90 if ($component instanceof Component\Triggerer) {
91 $component = $this->addTriggererOnLoadCode($component);
92 }
93
94 $component = $this->setSignals($component);
95
96 switch (true) {
97 case ($component instanceof F\OptionalGroup):
98 return $this->renderOptionalGroup($component, $default_renderer);
99
100 case ($component instanceof F\SwitchableGroup):
101 return $this->renderSwitchableGroup($component, $default_renderer);
102
103 case ($component instanceof F\Section):
104 return $this->renderSection($component, $default_renderer);
105
106 case ($component instanceof F\Duration):
107 return $this->renderDurationField($component, $default_renderer);
108
109 case ($component instanceof F\Link):
110 return $this->renderLinkField($component, $default_renderer);
111
112 case ($component instanceof F\Group):
113 return $default_renderer->render($component->getInputs());
114
115 case ($component instanceof F\Text):
116 return $this->renderTextField($component, $default_renderer);
117
118 case ($component instanceof F\Numeric):
119 return $this->renderNumericField($component, $default_renderer);
120
121 case ($component instanceof F\Checkbox):
122 return $this->renderCheckboxField($component, $default_renderer);
123
124 case ($component instanceof F\Tag):
125 return $this->renderTagField($component, $default_renderer);
126
127 case ($component instanceof F\Password):
128 return $this->renderPasswordField($component, $default_renderer);
129
130 case ($component instanceof F\Select):
131 return $this->renderSelectField($component, $default_renderer);
132
133 case ($component instanceof F\Markdown):
134 return $this->renderMarkdownField($component, $default_renderer);
135
136 case ($component instanceof F\Textarea):
137 return $this->renderTextareaField($component, $default_renderer);
138
139 case ($component instanceof F\Radio):
140 return $this->renderRadioField($component, $default_renderer);
141
142 case ($component instanceof F\MultiSelect):
143 return $this->renderMultiSelectField($component, $default_renderer);
144
145 case ($component instanceof F\DateTime):
146 return $this->renderDateTimeField($component, $default_renderer);
147
148 case ($component instanceof F\Image):
149 return $this->renderImageField($component, $default_renderer);
150
151 case ($component instanceof F\File):
152 return $this->renderFileField($component, $default_renderer);
153
154 case ($component instanceof F\Url):
155 return $this->renderUrlField($component, $default_renderer);
156
157 case ($component instanceof F\Hidden):
158 return $this->renderHiddenField($component);
159
160 case ($component instanceof F\ColorSelect):
161 return $this->renderColorSelectField($component, $default_renderer);
162
163 case ($component instanceof F\Rating):
164 return $this->renderRatingField($component, $default_renderer);
165
166 case ($component instanceof F\TreeMultiSelect):
167 return $this->renderTreeMultiSelectField($component, $default_renderer);
168
169 case ($component instanceof F\TreeSelect):
170 return $this->renderTreeSelectField($component, $default_renderer);
171
172 default:
173 $this->cannotHandleComponent($component);
174 }
175 }
176
177 protected function wrapInFormContext(
178 FormInput $component,
179 string $label,
180 string $input_html,
181 ?string $id_for_label = null,
182 ?string $dependant_group_html = null
183 ): string {
184 $tpl = $this->getTemplate("tpl.context_form.html", true, true);
185
186 $tpl->setVariable("LABEL", $label);
187 $tpl->setVariable("INPUT", $input_html);
188 $tpl->setVariable("UI_COMPONENT_NAME", $this->getComponentCanonicalNameAttribute($component));
189 $tpl->setVariable("INPUT_NAME", $component->getName());
190
191 if ($component->getOnLoadCode() !== null) {
192 $binding_id = $this->bindJavaScript($component) ?? $this->createId();
193 $tpl->setVariable("BINDING_ID", $binding_id);
194 }
195
196 if ($id_for_label) {
197 $tpl->setCurrentBlock('for');
198 $tpl->setVariable("ID", $id_for_label);
199 $tpl->parseCurrentBlock();
200 } else {
201 $tpl->touchBlock('tabindex');
202 }
203
204 $byline = $component->getByline();
205 if ($byline) {
206 $tpl->setVariable("BYLINE", $byline);
207 }
208
209 $required = $component->isRequired();
210 if ($required) {
211 $tpl->setCurrentBlock('required');
212 $tpl->setVariable("REQUIRED_ARIA", $this->txt('required_field'));
213 $tpl->parseCurrentBlock();
214 }
215
216 if ($component->isDisabled()) {
217 $tpl->touchBlock("disabled");
218 }
219
220 $error = $component->getError();
221 if ($error) {
222 $error_id = $this->createId();
223 $tpl->setVariable("ERROR_LABEL", $this->txt("ui_error"));
224 $tpl->setVariable("ERROR_ID", $error_id);
225 $tpl->setVariable("ERROR", $error);
226 if ($id_for_label) {
227 $tpl->setVariable("ERROR_FOR_ID", $id_for_label);
228 }
229 }
230
231 if ($dependant_group_html) {
232 $tpl->setVariable("DEPENDANT_GROUP", $dependant_group_html);
233 }
234 return $tpl->get();
235 }
236
237 protected function applyName(FormInput $component, Template $tpl): ?string
238 {
239 $name = $component->getName();
240 $tpl->setVariable("NAME", $name);
241 return $name;
242 }
243
244 protected function bindJSandApplyId(Component\JavaScriptBindable $component, Template $tpl): string
245 {
246 $id = $this->bindJavaScript($component) ?? $this->createId();
247 $tpl->setVariable("ID", $id);
248 return $id;
249 }
250
259 protected function applyValue(FormInput $component, Template $tpl, ?callable $escape = null): void
260 {
261 $value = $component->getValue();
262 if (!is_null($escape)) {
263 $value = $escape($value);
264 }
265 if (isset($value) && $value !== '') {
266 $tpl->setVariable("VALUE", $value);
267 }
268 }
269
270 protected function escapeSpecialChars(): Closure
271 {
272 return function ($v) {
273 // with declare(strict_types=1) in place,
274 // htmlspecialchars will not silently convert to string anymore;
275 // therefore, the typecast must be explicit
276 return htmlspecialchars((string) $v, ENT_QUOTES, 'utf-8', false);
277 };
278 }
279
280 protected function htmlEntities(): Closure
281 {
282 return function ($v) {
283 // with declare(strict_types=1) in place,
284 // htmlentities will not silently convert to string anymore;
285 // therefore, the typecast must be explicit
286 return htmlentities((string) $v, ENT_QUOTES, 'utf-8', false);
287 };
288 }
289
290 protected function renderLinkField(F\Link $component, RendererInterface $default_renderer): string
291 {
292 $input_html = $default_renderer->render($component->getInputs());
293 return $this->wrapInFormContext(
294 $component,
295 $component->getLabel(),
296 $input_html,
297 );
298 }
299
300 protected function renderTextField(F\Text $component): string
301 {
302 $tpl = $this->getTemplate("tpl.text.html", true, true);
303 $this->applyName($component, $tpl);
304
305 if ($component->getMaxLength()) {
306 $tpl->setVariable("MAX_LENGTH", $component->getMaxLength());
307 }
308
309 $this->applyValue($component, $tpl, $this->escapeSpecialChars());
310
311 $label_id = $this->createId();
312 $tpl->setVariable('ID', $label_id);
313 return $this->wrapInFormContext($component, $component->getLabel(), $tpl->get(), $label_id);
314 }
315
316 protected function renderNumericField(F\Numeric $component, RendererInterface $default_renderer): string
317 {
318 $tpl = $this->getTemplate("tpl.numeric.html", true, true);
319 $this->applyName($component, $tpl);
320 $this->applyValue($component, $tpl, $this->escapeSpecialChars());
321
322 $tpl->setVariable("STEPSIZE", $component->getStepSize());
323
324 $label_id = $this->createId();
325 $tpl->setVariable('ID', $label_id);
326 return $this->wrapInFormContext($component, $component->getLabel(), $tpl->get(), $label_id);
327 }
328
329 protected function renderCheckboxField(F\Checkbox $component, RendererInterface $default_renderer): string
330 {
331 $tpl = $this->getTemplate("tpl.checkbox.html", true, true);
332 $this->applyName($component, $tpl);
333
334 if ($component->getValue()) {
335 $tpl->touchBlock("value");
336 }
337
338 $label_id = $this->createId();
339 $tpl->setVariable('ID', $label_id);
340 return $this->wrapInFormContext($component, $component->getLabel(), $tpl->get(), $label_id);
341 }
342
343 protected function renderOptionalGroup(F\OptionalGroup $component, RendererInterface $default_renderer): string
344 {
345 $tpl = $this->getTemplate("tpl.optionalgroup_label.html", true, true);
346 $tpl->setVariable('LABEL', $component->getLabel());
347 $tpl->setVariable("NAME", $component->getName());
348 if ($component->getValue()) {
349 $tpl->setVariable("CHECKED", 'checked="checked"');
350 }
351
352 $label_id = $this->createId();
353 $tpl->setVariable('ID', $label_id);
354
355 $label = $tpl->get();
356 $input_html = $default_renderer->render($component->getInputs());
357
358 return $this->wrapInFormContext($component, $label, $input_html, $label_id);
359 }
360
361 protected function renderSwitchableGroup(F\SwitchableGroup $component, RendererInterface $default_renderer): string
362 {
363 $value = null;
364 if ($component->getValue() !== null) {
365 list($value, ) = $component->getValue();
366 }
367
368 $input_html = '';
369 foreach ($component->getInputs() as $key => $group) {
370 $tpl = $this->getTemplate("tpl.switchablegroup_label.html", true, true);
371 $tpl->setVariable('LABEL', $group->getLabel());
372 $tpl->setVariable("NAME", $component->getName());
373 $tpl->setVariable("VALUE", $key);
374
375 $label_id = $this->createId();
376 $tpl->setVariable('ID', $label_id);
377
378 if ($key == $value) {
379 $tpl->setVariable("CHECKED", 'checked="checked"');
380 }
381
382 $input_html .= $this->wrapInFormContext(
383 $group,
384 $tpl->get(),
385 $default_renderer->render($group),
386 $label_id
387 );
388 }
389
390
391 return $this->wrapInFormContext(
392 $component,
393 $component->getLabel(),
394 $input_html
395 );
396 }
397
398 protected function renderTagField(F\Tag $component, RendererInterface $default_renderer): string
399 {
400 $tpl = $this->getTemplate("tpl.tag_input.html", true, true);
401 $this->applyName($component, $tpl);
402
403 $configuration = $component->getConfiguration();
404 $value = $component->getValue();
405
406 if ($value) {
407 $value = array_map(
408 function ($v) {
409 return ['value' => rawurlencode($v), 'display' => $v];
410 },
411 $value
412 );
413 }
414
415 $autocomplete_endpoint = 'undefined';
416 $autocomplete_token = 'undefined';
417 if ($component->getAsyncAutocompleteEndpoint() !== null) {
418 $autocomplete_endpoint = $component->getAsyncAutocompleteEndpoint()
419 ->renderObject([$component->getAsyncAutocompleteToken()]);
420 $autocomplete_token = $component->getAsyncAutocompleteToken()->render();
421 }
422
423 $component = $component->withAdditionalOnLoadCode(
424 function ($id) use ($configuration, $value, $autocomplete_endpoint, $autocomplete_token) {
425 $encoded = json_encode($configuration);
426 $value = json_encode($value);
427 return 'il.UI.Input.tagInput.init(document.querySelector('
428 . "'#{$id} .c-field-tag'), {$encoded}, {$value},"
429 . " {$autocomplete_endpoint}, "
430 . " {$autocomplete_token}"
431 . ");";
432 }
433 );
434
435 if ($component->isDisabled()) {
436 $tpl->setVariable("DISABLED", "disabled");
437 $tpl->setVariable("READONLY", "readonly");
438 }
439
440 $label_id = $this->createId();
441 $tpl->setVariable('ID', $label_id);
442 return $this->wrapInFormContext($component, $component->getLabel(), $tpl->get(), $label_id);
443 }
444
445 protected function renderPasswordField(F\Password $component, RendererInterface $default_renderer): string
446 {
447 $tpl = $this->getTemplate("tpl.password.html", true, true);
448 $this->applyName($component, $tpl);
449
450 if ($component->getRevelation()) {
451 $component = $component->withResetSignals();
452 $sig_reveal = $component->getRevealSignal();
453 $sig_mask = $component->getMaskSignal();
454 $component = $component->withAdditionalOnLoadCode(function ($id) use ($sig_reveal, $sig_mask) {
455 return
456 "$(document).on('$sig_reveal', function() {
457 const fieldContainer = document.querySelector('#$id .c-input__field .c-field-password');
458 fieldContainer.classList.add('revealed');
459 fieldContainer.getElementsByTagName('input').item(0).type='text';
460 });" .
461 "$(document).on('$sig_mask', function() {
462 const fieldContainer = document.querySelector('#$id .c-input__field .c-field-password');
463 fieldContainer.classList.remove('revealed');
464 fieldContainer.getElementsByTagName('input').item(0).type='password';
465 });";
466 });
467
468 $f = $this->getUIFactory();
469 $btn_reveal = $f->button()->shy('', '')->withSymbol($f->symbol()->glyph()->eyeopen())
470 ->withOnClick($sig_reveal);
471 $btn_mask = $f->button()->shy('', '')->withSymbol($f->symbol()->glyph()->eyeclosed())
472 ->withOnClick($sig_mask);
473
474 $tpl->setVariable('PASSWORD_REVEAL', $default_renderer->render($btn_reveal));
475 $tpl->setVariable('PASSWORD_MASK', $default_renderer->render($btn_mask));
476 }
477
478 $this->applyValue($component, $tpl, $this->escapeSpecialChars());
479
480 $label_id = $this->createId();
481 $tpl->setVariable('ID', $label_id);
482 return $this->wrapInFormContext($component, $component->getLabel(), $tpl->get(), $label_id);
483 }
484
485 public function renderSelectField(F\Select $component, RendererInterface $default_renderer): string
486 {
487 $tpl = $this->getTemplate("tpl.select.html", true, true);
488 $this->applyName($component, $tpl);
489
490 $value = $component->getValue();
491 $value_is_empty = $value === null || $value === '';
492 //disable first option if required.
493 $tpl->setCurrentBlock("options");
494 if ($value_is_empty) {
495 $tpl->setVariable("SELECTED", 'selected="selected"');
496 }
497 if ($value_is_empty && $component->isRequired()) {
498 $tpl->setVariable("DISABLED_OPTION", "disabled");
499 $tpl->setVariable("HIDDEN", "hidden");
500 }
501
502 if ($value_is_empty || !$component->isRequired()) {
503 $tpl->setVariable("VALUE", null);
504 $tpl->setVariable("VALUE_STR", $component->isRequired() ? $this->txt('ui_select_dropdown_label') : '-');
505 $tpl->parseCurrentBlock();
506 }
507
508 foreach ($component->getOptions() as $option_key => $option_value) {
509 $tpl->setCurrentBlock("options");
510 if (!$value_is_empty && $value == $option_key) {
511 $tpl->setVariable("SELECTED", 'selected="selected"');
512 }
513 $tpl->setVariable("VALUE", $option_key);
514 $tpl->setVariable("VALUE_STR", $option_value);
515 $tpl->parseCurrentBlock();
516 }
517
518 $label_id = $this->createId();
519 $tpl->setVariable('ID', $label_id);
520 return $this->wrapInFormContext($component, $component->getLabel(), $tpl->get(), $label_id);
521 }
522
523 protected function renderMarkdownField(F\Markdown $component, RendererInterface $default_renderer): string
524 {
525 [$textarea_tpl, $component] = $this->getPreparedTextareaTemplate($component);
526
528 $component = $component->withAdditionalOnLoadCode(
529 static function ($id) use ($component): string {
530 return "
531 il.UI.Input.markdown.init(
532 document.querySelector('#$id .c-input__field textarea')?.id,
533 '{$component->getMarkdownRenderer()->getAsyncUrl()}',
534 '{$component->getMarkdownRenderer()->getParameterName()}'
535 );
536 ";
537 }
538 );
539
540 $textarea_id = $this->createId();
541 $textarea_tpl->setVariable('ID', $textarea_id);
542
543 $markdown_tpl = $this->getTemplate("tpl.markdown.html", true, true);
544 $markdown_tpl->setVariable('TEXTAREA', $textarea_tpl->get());
545
546 $markdown_tpl->setVariable(
547 'PREVIEW',
548 $component->getMarkdownRenderer()->render(
549 $this->htmlEntities()($component->getValue() ?? '')
550 )
551 );
552
553 $markdown_tpl->setVariable(
554 'VIEW_CONTROLS',
555 $default_renderer->render(
556 $this->getUIFactory()->viewControl()->mode([
557 $this->txt('ui_md_input_edit') => '#',
558 $this->txt('ui_md_input_view') => '#',
559 ], "")
560 )
561 );
562
564 $markdown_actions_glyphs = [
565 'ACTION_HEADING' => $this->getUIFactory()->symbol()->glyph()->header(),
566 'ACTION_LINK' => $this->getUIFactory()->symbol()->glyph()->link(),
567 'ACTION_BOLD' => $this->getUIFactory()->symbol()->glyph()->bold(),
568 'ACTION_ITALIC' => $this->getUIFactory()->symbol()->glyph()->italic(),
569 'ACTION_ORDERED_LIST' => $this->getUIFactory()->symbol()->glyph()->numberedlist(),
570 'ACTION_UNORDERED_LIST' => $this->getUIFactory()->symbol()->glyph()->bulletlist()
571 ];
572
573 foreach ($markdown_actions_glyphs as $tpl_variable => $glyph) {
574 $action = $this->getUIFactory()->button()->standard('', '#')->withSymbol($glyph);
575
576 if ($component->isDisabled()) {
577 $action = $action->withUnavailableAction();
578 }
579
580 $markdown_tpl->setVariable($tpl_variable, $default_renderer->render($action));
581 }
582
583 return $this->wrapInFormContext($component, $component->getLabel(), $markdown_tpl->get());
584 }
585
586 protected function renderTextareaField(F\Textarea $component, RendererInterface $default_renderer): string
587 {
588 [$tpl, $component] = $this->getPreparedTextareaTemplate($component);
589
591 $component = $component->withAdditionalOnLoadCode(
592 static fn($id) => "il.UI.Input.textarea.init(document.querySelector('#$id .c-input__field textarea')?.id);",
593 );
594
595 $label_id = $this->createId();
596 $tpl->setVariable('ID', $label_id);
597 return $this->wrapInFormContext($component, $component->getLabel(), $tpl->get(), $label_id);
598 }
599
603 protected function getPreparedTextareaTemplate(F\Textarea $component): array
604 {
605 $tpl = $this->getTemplate("tpl.textarea.html", true, true);
606
607 if (0 < $component->getMaxLimit()) {
608 $tpl->setVariable('REMAINDER_TEXT', $this->txt('ui_chars_remaining'));
609 $tpl->setVariable('REMAINDER', $component->getMaxLimit() - strlen($component->getValue() ?? ''));
610 $tpl->setVariable('MAX_LIMIT', $component->getMaxLimit());
611 }
612
613 if (null !== $component->getMinLimit()) {
614 $tpl->setVariable('MIN_LIMIT', $component->getMinLimit());
615 }
616
617 [$mustache_variable_html, $component] = $this->renderMustacheVariables($component);
618 $tpl->setVariable('MUSTACHE_VARIABLES_HTML', $mustache_variable_html);
619
620 $this->applyName($component, $tpl);
621 $this->applyValue(
622 $component,
623 $tpl,
624 $this->mustacheVariableEntities()
625 );
626 return [$tpl, $component];
627 }
628
632 protected function renderMustacheVariables(F\HasMustacheVariablesInternal $component): array
633 {
634 $mustache_variable_definitions = $component->getMustacheVariables();
636 return ['', $component];
637 }
638
639 $template = $this->getTemplate('tpl.mustache_variables.html', true, true);
640 $template->setVariable('MUSTACHE_VARIABLE_USAGE_INFO', $this->txt('ui_mustache_variables_usage_info'));
641
642 $mustache_variable_context_info = $component->getMustacheVariableContextInfo();
643 if (null !== $mustache_variable_context_info) {
644 $template->setVariable('MUSTACHE_VARIABLE_CONTEXT_INFO', $mustache_variable_context_info);
645 }
646
647 foreach ($mustache_variable_definitions as $variable_name => $description) {
648 $template->setCurrentBlock('with_mustache_variable_definition');
649 $template->setVariable('VARIABLE_NAME', $variable_name);
650 $template->setVariable('VARIABLE_DESCRIPTION', $description);
651 $template->parseCurrentBlock();
652 }
653
654 // @todo: this feature is currently highly coupled to textareas
655 $enriched_component = $component->withAdditionalOnLoadCode(static fn($id) => "
656 il.UI.Input.mustacheVariables.init(
657 il.UI.Input.textarea.get(document.querySelector('#$id .c-input__field textarea')?.id) ??
658 il.UI.Input.markdown.get(document.querySelector('#$id .c-input__field textarea')?.id),
659 document.getElementById('$id'),
660 );
661 ");
662
663 return [$template->get(), $enriched_component];
664 }
665
666 protected function renderRadioField(F\Radio $component, RendererInterface $default_renderer): string
667 {
668 $tpl = $this->getTemplate("tpl.radio.html", true, true);
669 $id = $this->createId();
670
671 foreach ($component->getOptions() as $value => $label) {
672 // @todo: can we get rid of this enganglement?
673 if ($component->hasOptionFilter()) {
674 $tpl->touchBlock('is_filter_option');
675 }
676
677 $opt_id = $id . '_' . $value . '_opt';
678
679 $tpl->setCurrentBlock('optionblock');
680 $tpl->setVariable("NAME", $component->getName());
681 $tpl->setVariable("OPTIONID", $opt_id);
682 $tpl->setVariable("VALUE", $value);
683 $tpl->setVariable("LABEL", $label);
684
685 if ($component->getValue() !== null && $component->getValue() == $value) {
686 $tpl->setVariable("CHECKED", 'checked="checked"');
687 }
688 if ($component->isDisabled()) {
689 $tpl->setVariable("DISABLED", 'disabled="disabled"');
690 }
691
692 $byline = $component->getBylineFor((string) $value);
693 if (!empty($byline)) {
694 $tpl->setVariable("BYLINE", $byline);
695 }
696
697 $tpl->parseCurrentBlock();
698 }
699
700 if ($component->hasOptionFilter()) {
701 // @todo: can we get rid of this enganglement?
702 $tpl->touchBlock("has_option_filter");
703 $field_html = $tpl->get();
704 [$field_html, $component] = $this->renderOptionFilter($field_html, $component, $default_renderer);
705 } else {
706 $field_html = $tpl->get();
707 }
708
709 return $this->wrapInFormContext($component, $component->getLabel(), $field_html);
710 }
711
712 protected function renderMultiSelectField(F\MultiSelect $component, RendererInterface $default_renderer): string
713 {
714 $tpl = $this->getTemplate("tpl.multiselect.html", true, true);
715
716 $options = $component->getOptions();
717 if (count($options) > 0) {
718 $value = $component->getValue();
719 $name = $this->applyName($component, $tpl);
720 foreach ($options as $opt_value => $opt_label) {
721 // @todo: can we get rid of this enganglement?
722 if ($component->hasOptionFilter()) {
723 $tpl->touchBlock('is_filter_option');
724 }
725
726 $tpl->setCurrentBlock("option");
727 $tpl->setVariable("NAME", $name);
728 $tpl->setVariable("VALUE", $opt_value);
729 $tpl->setVariable("LABEL", $opt_label);
730
731 if ($value && in_array($opt_value, $value)) {
732 $tpl->setVariable("CHECKED", 'checked="checked"');
733 }
734
735 $tpl->parseCurrentBlock();
736 }
737 } else {
738 $tpl->touchBlock("no_options");
739 }
740
741 if ($component->hasOptionFilter()) {
742 // @todo: can we get rid of this enganglement?
743 $tpl->touchBlock("has_option_filter");
744 $field_html = $tpl->get();
745 [$field_html, $component] = $this->renderOptionFilter($field_html, $component, $default_renderer);
746 } else {
747 $field_html = $tpl->get();
748 }
749
750 return $this->wrapInFormContext($component, $component->getLabel(), $field_html);
751 }
752
753 protected function renderDateTimeField(F\DateTime $component, RendererInterface $default_renderer): string
754 {
755 list($component, $tpl) = $this->internalRenderDateTimeField($component, $default_renderer);
756 $label_id = $this->createId();
757 $tpl->setVariable('ID', $label_id);
758 return $this->wrapInFormContext($component, $component->getLabel(), $tpl->get(), $label_id);
759 }
760
764 protected function internalRenderDateTimeField(F\DateTime $component, RendererInterface $default_renderer): array
765 {
766 $tpl = $this->getTemplate("tpl.datetime.html", true, true);
767 $this->applyName($component, $tpl);
768
769 if ($component->getTimeOnly() === true) {
770 $format = $component::TIME_FORMAT;
771 $dt_type = self::TYPE_TIME;
772 } else {
773 $dt_type = self::TYPE_DATE;
774 $format = $this->getTransformedDateFormat(
775 $component->getFormat(),
776 self::DATEPICKER_FORMAT_MAPPING
777 );
778
779 if ($component->getUseTime() === true) {
780 $format .= ' ' . $component::TIME_FORMAT;
781 $dt_type = self::TYPE_DATETIME;
782 }
783 }
784
785 $tpl->setVariable("DTTYPE", $dt_type);
786
787 $min_max_format = self::DATE_DATEPICKER_MINMAX_FORMAT;
788 if ($dt_type === self::TYPE_DATETIME) {
789 $min_max_format = self::DATETIME_DATEPICKER_MINMAX_FORMAT;
790 }
791
792 $min_date = $component->getMinValue();
793 if (!is_null($min_date)) {
794 $tpl->setVariable("MIN_DATE", date_format($min_date, $min_max_format));
795 }
796 $max_date = $component->getMaxValue();
797 if (!is_null($max_date)) {
798 $tpl->setVariable("MAX_DATE", date_format($max_date, $min_max_format));
799 }
800
801 $this->applyValue($component, $tpl, function (?string $value) use ($dt_type) {
802 if ($value !== null) {
803 $value = new \DateTimeImmutable($value);
804 return $value->format(match ($dt_type) {
805 self::TYPE_DATETIME => self::HTML5_NATIVE_DATETIME_FORMAT,
806 self::TYPE_DATE => self::HTML5_NATIVE_DATE_FORMAT,
807 self::TYPE_TIME => self::HTML5_NATIVE_TIME_FORMAT,
808 });
809 }
810 return null;
811 });
812 return [$component, $tpl];
813 }
814
815 protected function renderDurationField(F\Duration $component, RendererInterface $default_renderer): string
816 {
817 $inputs = $component->getInputs();
818
819 $input = array_shift($inputs); //from
820 list($input, $tpl) = $this->internalRenderDateTimeField($input, $default_renderer);
821
822 $from_input_id = $this->createId();
823 $tpl->setVariable('ID', $from_input_id);
824 $input_html = $this->wrapInFormContext($input, $input->getLabel(), $tpl->get(), $from_input_id);
825
826 $input = array_shift($inputs) //until
827 ->withAdditionalPickerconfig(['useCurrent' => false]);
828 list($input, $tpl) = $this->internalRenderDateTimeField($input, $default_renderer);
829 $until_input_id = $this->createId();
830 $tpl->setVariable('ID', $until_input_id);
831 $input_html .= $this->wrapInFormContext($input, $input->getLabel(), $tpl->get(), $until_input_id);
832
833 $tpl = $this->getTemplate("tpl.duration.html", true, true);
834 $tpl->setVariable('DURATION', $input_html);
835 return $this->wrapInFormContext($component, $component->getLabel(), $tpl->get());//, $from_input_id);
836 }
837
838 protected function renderSection(F\Section $section, RendererInterface $default_renderer): string
839 {
840 $inputs_html = $default_renderer->render($section->getInputs());
841
842 $headline_tpl = $this->getTemplate("tpl.headlines.html", true, true);
843 $headline_tpl->setVariable("HEADLINE", $section->getLabel());
844 $nesting_level = $section->getNestingLevel() + 2;
845 if ($nesting_level > 6) {
846 $nesting_level = 6;
847 };
848 $headline_tpl->setVariable("LEVEL", $nesting_level);
849
850 $headline_html = $headline_tpl->get();
851
852 return $this->wrapInFormContext($section, $headline_html, $inputs_html);
853 }
854
855 protected function renderUrlField(F\Url $component, RendererInterface $default_renderer): string
856 {
857 $tpl = $this->getTemplate("tpl.url.html", true, true);
858 $this->applyName($component, $tpl);
859 $this->applyValue($component, $tpl, $this->escapeSpecialChars());
860
861 $label_id = $this->createId();
862 $tpl->setVariable('ID', $label_id);
863 return $this->wrapInFormContext($component, $component->getLabel(), $tpl->get(), $label_id);
864 }
865
866 protected function renderImageField(F\Image $input, RendererInterface $default_renderer): string
867 {
868 return $this->renderFileField($input, $default_renderer);
869 }
870
871 protected function renderFileField(F\File $input, RendererInterface $default_renderer): string
872 {
873 $template = $this->getTemplate('tpl.file.html', true, true);
874 foreach ($input->getGeneratedDynamicInputs() as $metadata_input) {
875 $file_info = null;
876 if (null !== ($data = $metadata_input->getValue())) {
877 $file_id = (!$input->hasMetadataInputs()) ? $data : $data[0] ?? null;
878
879 if (null !== $file_id) {
880 $file_info = $input->getUploadHandler()->getInfoResult($file_id);
881 }
882 }
883
884 $template = $this->renderFilePreview(
885 $input,
886 $metadata_input,
887 $default_renderer,
888 $file_info,
889 $template
890 );
891 }
892
893 $file_preview_template = $this->getTemplate('tpl.file.html', true, true);
894 $file_preview_template = $this->renderFilePreview(
895 $input,
896 $input->getTemplateForDynamicInputs(),
897 $default_renderer,
898 null,
899 $file_preview_template
900 );
901
902 $template->setVariable('FILE_PREVIEW_TEMPLATE', $file_preview_template->get('block_file_preview'));
903
904 $this->setHelpBlockForFileField($template, $input);
905
906 $input = $this->initClientsideFileInput($input);
907
908 // display the action button (to choose files).
909 $template->setVariable('ACTION_BUTTON', $default_renderer->render(
910 $this->getUIFactory()->button()->shy(
911 $input->getMaxFiles() <= 1
912 ? $this->txt('select_file_from_computer')
913 : $this->txt('select_files_from_computer'),
914 '#'
915 )
916 ));
917
918 return $this->wrapInFormContext(
919 $input,
920 $input->getLabel(),
921 $template->get(),
922 );
923 }
924
925 protected function renderHiddenField(F\Hidden $input): string
926 {
927 $template = $this->getTemplate('tpl.hidden.html', true, true);
928 $this->applyName($input, $template);
929 $this->applyValue($input, $template, $this->escapeSpecialChars());
930 if ($input->isDisabled()) {
931 $template->setVariable("DISABLED", 'disabled="disabled"');
932 }
933 $this->bindJSandApplyId($input, $template);
934 return $template->get();
935 }
936
940 public function registerResources(ResourceRegistry $registry): void
941 {
942 parent::registerResources($registry);
943 $registry->register('assets/css/tagify.css');
944
945 $registry->register('assets/js/dropzone-min.js');
946 $registry->register('assets/js/dropzone.js');
947 $registry->register('assets/js/input.js');
948 $registry->register('assets/js/core.js');
949 $registry->register('assets/js/file.js');
950 // workaround to manipulate the order of scripts
951 $registry->register('assets/js/drilldown.min.js');
952 $registry->register('assets/js/input.factory.min.js');
953 }
954
959 protected function setSignals(F\FormInput $input)
960 {
961 $signals = null;
962 foreach ($input->getTriggeredSignals() as $s) {
963 $signals[] = [
964 "signal_id" => $s->getSignal()->getId(),
965 "event" => $s->getEvent(),
966 "options" => $s->getSignal()->getOptions()
967 ];
968 }
969 if ($signals !== null) {
970 $signals = json_encode($signals);
971
972 $input = $input->withAdditionalOnLoadCode($input->getUpdateOnLoadCode());
973
974 $input = $input->withAdditionalOnLoadCode(static function ($id) use ($signals) {
975 $code = "il.UI.input.setSignalsForId('$id', $signals);";
976 return $code;
977 });
978 }
979 return $input;
980 }
981
988 protected function getTransformedDateFormat(
989 DateFormat\DateFormat $origin,
990 array $mapping
991 ): string {
992 $ret = '';
993 foreach ($origin->toArray() as $element) {
994 if (array_key_exists($element, $mapping)) {
995 $ret .= $mapping[$element];
996 } else {
997 $ret .= $element;
998 }
999 }
1000 return $ret;
1001 }
1002
1003 protected function renderFilePreview(
1004 FI\File $file_input,
1005 FormInput $metadata_input,
1006 RendererInterface $default_renderer,
1007 ?FileInfoResult $file_info,
1008 Template $template
1009 ): Template {
1010 $f = $this->getUIFactory();
1011 $template->setCurrentBlock('block_file_preview');
1012 $template->setVariable('REMOVAL_GLYPH', $default_renderer->render(
1013 $f->button()->shy('', '')->withSymbol($f->symbol()->glyph()->close())
1014 ));
1015
1016 if (null !== $file_info) {
1017 $template->setVariable('FILE_NAME', $file_info->getName());
1018 $template->setVariable(
1019 'FILE_SIZE',
1020 (string) (new DataSize($file_info->getSize(), DataSize::Byte))
1021 );
1022 }
1023
1024 // only render expansion toggles if the input
1025 // contains actual (unhidden) inputs.
1026 if ($file_input->hasMetadataInputs()) {
1027 $template->setVariable('EXPAND_GLYPH', $default_renderer->render(
1028 $f->button()->shy('', '')->withSymbol($f->symbol()->glyph()->expand())
1029 ));
1030 $template->setVariable('COLLAPSE_GLYPH', $default_renderer->render(
1031 $f->button()->shy('', '')->withSymbol($f->symbol()->glyph()->collapse())
1032 ));
1033 }
1034
1035 $template->setVariable('METADATA_INPUTS', $default_renderer->render($metadata_input));
1036
1037 $template->parseCurrentBlock();
1038
1039 return $template;
1040 }
1041
1042 protected function initClientsideFileInput(FI\File $input): FI\File
1043 {
1044 return $input->withAdditionalOnLoadCode(
1045 function ($id) use ($input) {
1046 $current_file_count = count($input->getGeneratedDynamicInputs());
1047 $translations = json_encode($input->getTranslations());
1048 $is_disabled = ($input->isDisabled()) ? 'true' : 'false';
1049 $php_upload_limit = $this->getUploadLimitResolver()->getPhpUploadLimitInBytes();
1050 $should_upload_be_chunked = ($input->getMaxFileSize() > $php_upload_limit) ? 'true' : 'false';
1051 $chunk_size = (int) floor($php_upload_limit * self::FILE_UPLOAD_CHUNK_SIZE_FACTOR);
1052 return "
1053 $(document).ready(function () {
1054 il.UI.Input.File.init(
1055 '$id',
1056 '{$input->getUploadHandler()->getUploadURL()}',
1057 '{$input->getUploadHandler()->getFileRemovalURL()}',
1058 '{$input->getUploadHandler()->getFileIdentifierParameterName()}',
1059 $current_file_count,
1060 {$input->getMaxFiles()},
1061 {$input->getMaxFileSize()},
1062 '{$this->prepareDropzoneJsMimeTypes($input->getAcceptedMimeTypes())}',
1063 $is_disabled,
1064 $translations,
1065 $should_upload_be_chunked,
1066 $chunk_size
1067 );
1068 });
1069 ";
1070 }
1071 );
1072 }
1073
1079 protected function prepareDropzoneJsMimeTypes(array $mime_types): string
1080 {
1081 $mime_type_string = '';
1082 foreach ($mime_types as $index => $mime_type) {
1083 $mime_type_string .= (isset($mime_types[$index + 1])) ? "$mime_type," : $mime_type;
1084 }
1085
1086 return $mime_type_string;
1087 }
1088
1089 protected function renderColorSelectField(F\ColorSelect $component, RendererInterface $default_renderer): string
1090 {
1091 $tpl = $this->getTemplate("tpl.color_select.html", true, true);
1092 $this->applyName($component, $tpl);
1093 $tpl->setVariable('VALUE', $component->getValue());
1094
1095 $label_id = $this->createId();
1096 $tpl->setVariable('ID', $label_id);
1097 return $this->wrapInFormContext($component, $component->getLabel(), $tpl->get(), $label_id);
1098 }
1099
1100 protected function renderRatingField(F\Rating $component, RendererInterface $default_renderer): string
1101 {
1102 $tpl = $this->getTemplate("tpl.rating.html", true, true);
1103 $id = $this->createId();
1104 $aria_description_id = $id . '_desc';
1105 $tpl->setVariable('DESCRIPTION_SRC_ID', $aria_description_id);
1106
1107 $option_count = count(FiveStarRatingScale::cases()) - 1;
1108
1109 foreach (range($option_count, 1, -1) as $option) {
1110 $tpl->setCurrentBlock('scaleoption');
1111 $tpl->setVariable('ARIALABEL', $this->txt($option . 'stars'));
1112 $tpl->setVariable('OPT_VALUE', (string) $option);
1113 $tpl->setVariable('OPT_ID', $id . '-' . $option);
1114 $tpl->setVariable('NAME', $component->getName());
1115 $tpl->setVariable('DESCRIPTION_ID', $aria_description_id);
1116
1117 if ($component->getValue() === FiveStarRatingScale::from((int) $option)) {
1118 $tpl->setVariable("SELECTED", ' checked="checked"');
1119 }
1120 if ($component->isDisabled()) {
1121 $tpl->setVariable("DISABLED", 'disabled="disabled"');
1122 }
1123 $tpl->parseCurrentBlock();
1124 }
1125
1126 if (!$component->isRequired()) {
1127 $tpl->setVariable('NEUTRAL_ID', $id . '-0');
1128 $tpl->setVariable('NEUTRAL_NAME', $component->getName());
1129 $tpl->setVariable('NEUTRAL_LABEL', $this->txt('reset_stars'));
1130 $tpl->setVariable('NEUTRAL_DESCRIPTION_ID', $aria_description_id);
1131
1132 if ($component->getValue() === FiveStarRatingScale::NONE || is_null($component->getValue())) {
1133 $tpl->setVariable('NEUTRAL_SELECTED', ' checked="checked"');
1134 }
1135 }
1136
1137 if ($txt = $component->getAdditionalText()) {
1138 $tpl->setVariable('TEXT', $txt);
1139 }
1140
1141 if ($component->isDisabled()) {
1142 $tpl->touchBlock('disabled');
1143 }
1144 if ($average = $component->getCurrentAverage()) {
1145 $average_title = sprintf($this->txt('rating_average'), $average);
1146 $tpl->setVariable('AVERAGE_VALUE', $average_title);
1147 $tpl->setVariable('AVERAGE_VALUE_PERCENT', $average / $option_count * self::CENTUM);
1148 }
1149
1150 return $this->wrapInFormContext($component, $component->getLabel(), $tpl->get());
1151 }
1152
1153 protected function renderTreeMultiSelectField(F\TreeMultiSelect $component, RendererInterface $default_renderer): string
1154 {
1155 $template = $this->prepareTreeSelectTemplate($component, $default_renderer);
1156
1157 if ($component->canSelectChildNodes()) {
1158 $select_child_nodes = 'true';
1159 } else {
1160 $select_child_nodes = 'false';
1161 }
1162
1163 $enriched_component = $component->withAdditionalOnLoadCode(
1164 static fn($id) => "il.UI.Input.treeSelect.initTreeMultiSelect('$id', $select_child_nodes);"
1165 );
1166
1167 $id = $this->bindJSandApplyId($enriched_component, $template);
1168
1169 return $this->wrapInFormContext($component, $component->getLabel(), $template->get(), $id);
1170 }
1171
1172 protected function renderTreeSelectField(F\TreeSelect $component, RendererInterface $default_renderer): string
1173 {
1174 $template = $this->prepareTreeSelectTemplate($component, $default_renderer);
1175
1176 $enriched_component = $component->withAdditionalOnLoadCode(
1177 static fn($id) => "il.UI.Input.treeSelect.initTreeSelect('$id');"
1178 );
1179
1180 $id = $this->bindJSandApplyId($enriched_component, $template);
1181
1182 return $this->wrapInFormContext($component, $component->getLabel(), $template->get(), $id);
1183 }
1184
1185 protected function prepareTreeSelectTemplate(
1186 TreeSelect|TreeMultiSelect $component,
1187 RendererInterface $default_renderer,
1188 ): Template {
1189 $template = $this->getTemplate('tpl.tree_select.html', true, true);
1190
1191 if ($component->isDisabled()) {
1192 $template->setVariable('DISABLED', 'disabled');
1193 }
1194
1195 $template->setVariable('SELECT_LABEL', $this->txt('select'));
1196 $template->setVariable('CLOSE_LABEL', $this->txt('close'));
1197 $template->setVariable('LABEL', $component->getLabel());
1198
1199 $template->setVariable('INPUT_TEMPLATE', $default_renderer->render(
1200 $component->getTemplateForDynamicInputs()
1201 ));
1202 $template->setVariable('BREADCRUMB_TEMPLATE', $default_renderer->render(
1203 $this->getUIFactory()->breadcrumbs([$this->getUIFactory()->link()->standard('label', '#')])
1204 ));
1205 $template->setVariable('BREADCRUMBS', $default_renderer->render(
1206 $this->getUIFactory()->breadcrumbs([])
1207 ));
1208
1210 $dynamic_inputs_generator = (static fn() => yield from $component->getGeneratedDynamicInputs())();
1211
1212 $leaf_generator = $component->getNodeRetrieval()->getNodesAsLeaf(
1213 $this->getUIFactory()->input()->field()->node(),
1214 $this->getUIFactory()->symbol()->icon(),
1215 $component->getValue(),
1216 );
1217
1218 $lockstep_iterator = $this->iterateGeneratorsInLockstep($leaf_generator, $dynamic_inputs_generator);
1219 $sync_node_id_whitelst = [];
1220
1221 foreach ($lockstep_iterator as [$leaf, $dynamic_input]) {
1222 // check against internal interface, will not be delegated to rendering chain.
1224 $this->checkArgInstanceOf('leaf', $leaf, Node\Leaf::class);
1225
1226 $value_template = $this->getTemplate('tpl.tree_select.html', true, true);
1227 $value_template->setCurrentBlock('with_value_template');
1228 $value_template->setVariable('NODE_ID', (string) ($leaf->getId()));
1229 $value_template->setVariable('NODE_NAME', $leaf->getName());
1230 $value_template->setVariable('INPUT_TEMPLATE', $default_renderer->render($dynamic_input));
1231 $value_template->setVariable('UNSELECT_NODE_LABEL', sprintf($this->txt('unselect_node'), $leaf->getName()));
1232 $value_template->parseCurrentBlock();
1233
1234 $template->setCurrentBlock('with_value');
1235 $template->setVariable('VALUE', $value_template->get('with_value_template'));
1236 $template->parseCurrentBlock();
1237
1238 foreach ($leaf->getFullPath() as $node_id) {
1239 // deduplicate overlaping node ids by using them as offset
1240 $sync_node_id_whitelst[$node_id] = $node_id;
1241 }
1242 }
1243
1244 $node_factory = $this->getUIFactory()->input()->field()->node();
1245 $node_generator = $component->getNodeRetrieval()->getNodes(
1246 $node_factory,
1247 $this->getUIFactory()->symbol()->icon(),
1248 array_values($sync_node_id_whitelst),
1249 null,
1250 );
1251
1252 $nodes = [];
1253 foreach ($node_generator as $node) {
1254 // check against public interface, will be delegated to rendering chain.
1255 $this->checkArgInstanceOf('node', $node, Component\Input\Field\Node\Node::class);
1256 $nodes[] = $node;
1257 }
1258
1259 $template->setVariable('DRILLDOWN', $default_renderer->render(
1260 $this->getUIFactory()->menu()->drilldown($component->getLabel(), $nodes)
1261 ));
1262
1263 $this->toJS('unselect_node');
1264 $this->toJS('select_node');
1265
1266 return $template;
1267 }
1268
1276 protected function iterateGeneratorsInLockstep(\Generator $a, \Generator $b): \Generator
1277 {
1278 while ($a->valid() && $b->valid()) {
1279 yield [$a->current(), $b->current()];
1280 $a->next();
1281 $b->next();
1282 }
1283 if ($a->valid() || $b->valid()) {
1284 throw new LogicException('Generators do not have equal lenghts.');
1285 }
1286 }
1287
1288 private function setHelpBlockForFileField(Template $template, FI\File $input): void
1289 {
1290 $template->setCurrentBlock('HELP_BLOCK');
1291
1292 $template->setCurrentBlock('MAX_FILE_SIZE');
1293 $template->setVariable('FILE_SIZE_LABEL', $this->txt('file_notice'));
1294 $template->setVariable('FILE_SIZE_VALUE', new DataSize($input->getMaxFileSize(), DataSize::Byte));
1295 $template->parseCurrentBlock();
1296
1297 $template->setCurrentBlock('MAX_FILES');
1298 $template->setVariable('FILES_LABEL', $this->txt('ui_file_upload_max_nr'));
1299 $template->setVariable('FILES_VALUE', $input->getMaxFiles());
1300 $template->parseCurrentBlock();
1301
1302 $template->parseCurrentBlock();
1303 }
1304
1312 protected function renderOptionFilter(string $input_html, F\HasOptionFilterInternal $component, RendererInterface $default_renderer): array
1313 {
1314 $option_filter_template = $this->getTemplate("tpl.option_filter.html", true, true);
1315 $option_filter_template->setVariable('INPUT', $input_html);
1316
1317 $search_input_id = $this->createId();
1318 $search_input_label_id = $this->createId();
1319 $search_input_description_id = $this->createId();
1320 $list_id = $this->createId();
1321
1322 $option_filter_template->setVariable('SEARCH_INPUT_ID', $search_input_id);
1323 $option_filter_template->setVariable('SEARCH_INPUT_LABEL_ID', $search_input_label_id);
1324 $option_filter_template->setVariable('SEARCH_INPUT_DESCRIPTION_ID', $search_input_description_id);
1325 $option_filter_template->setVariable('LIST_ID', $list_id);
1326
1327 $no_selection_text = $this->txt('ui_field_option_filter_no_selection');
1328 $option_filter_template->setVariable('NOTHING_SELECTED', $no_selection_text);
1329 $option_filter_template->setVariable('ARIA_FILTERED_RESULTS', $this->txt('ui_field_option_filter_filtered_results_aria_label'));
1330
1331 $option_filter_template->setVariable('SEARCH_LABEL', $this->txt("ui_field_option_filter_search_in"));
1332 $option_filter_template->setVariable('SCREEN_READER_HINT', $this->txt('ui_field_option_filter_screen_reader_hint'));
1333 $option_filter_template->setVariable('NO_MATCH', $this->txt('ui_field_option_filter_no_match'));
1334 $option_filter_template->setVariable('OPTIONS_SHOWN', $this->txt('ui_field_option_filter_options_shown'));
1335
1336 $expand_icon = $default_renderer->render($this->getUIFactory()->symbol()->glyph()->expand()->withLabel(''));
1337 $option_filter_template->setVariable('EXPAND_TEXT', $expand_icon . $this->txt('ui_field_option_filter_show_all_options'));
1338
1339 $collapse_icon = $default_renderer->render($this->getUIFactory()->symbol()->glyph()->collapseHorizontal()->withLabel(''));
1340 $option_filter_template->setVariable('COLLAPSE_TEXT', $collapse_icon . $this->txt('ui_field_option_filter_show_less'));
1341
1342 $remove_icon = $default_renderer->render($this->getUIFactory()->symbol()->glyph()->remove()->withLabel(''));
1343 $option_filter_template->setVariable('CLEAR_SEARCH_BTN', $remove_icon . $this->txt('ui_field_option_filter_clear_search'));
1344
1345 $component = $component->withAdditionalOnLoadCode(
1346 static fn($id): string => "il.UI.Input.optionFilter.init(document.getElementById('$id'));",
1347 );
1348
1349 return [$option_filter_template->get(), $component];
1350 }
1351
1352 private function mustacheVariableEntities(): Closure
1353 {
1354 return function ($val) {
1355 $val = htmlentities((string) $val);
1356 return str_replace('{{', '&lcub;&lcub;', str_replace('}}', '&rcub;&rcub;', $val));
1357 };
1358 }
1359}
$id
plugin.php for ilComponentBuildPluginInfoObjectiveTest::testAddPlugins
Definition: plugin.php:23
This class provides the data size with additional information to remove the work to calculate the siz...
Definition: DataSize.php:31
A Date Format provides a format definition akin to PHP's date formatting options, but stores the sing...
Definition: DateFormat.php:28
A password is used as part of credentials for authentication.
Definition: Password.php:31
The scope of this class is split ilias-conform URI's into components.
Definition: URI.php:35
FiveStarRatingScale
This is the scale for the Rating Input.
An internal class for the clickable, non-editable Input Fields within Filters.
This implements the checkbox input.
Definition: Checkbox.php:36
This implements the duration input group.
Definition: Duration.php:38
This implements the group input.
Definition: Group.php:41
getValue()
Get the value that is displayed in the input client side.
getTemplateForDynamicInputs()
Returns an Input Field which will be used to generate similar inputs on both server and client.
getGeneratedDynamicInputs()
Returns serverside generated dynamic Inputs, which happens when providing values with.
This implements the multi-select input.
Definition: MultiSelect.php:32
This implements the numeric input.
Definition: Numeric.php:34
This implements the radio input.
Definition: Radio.php:35
renderPasswordField(F\Password $component, RendererInterface $default_renderer)
Definition: Renderer.php:445
renderImageField(F\Image $input, RendererInterface $default_renderer)
Definition: Renderer.php:866
bindJSandApplyId(Component\JavaScriptBindable $component, Template $tpl)
Definition: Renderer.php:244
renderDateTimeField(F\DateTime $component, RendererInterface $default_renderer)
Definition: Renderer.php:753
renderCheckboxField(F\Checkbox $component, RendererInterface $default_renderer)
Definition: Renderer.php:329
renderSelectField(F\Select $component, RendererInterface $default_renderer)
Definition: Renderer.php:485
renderRadioField(F\Radio $component, RendererInterface $default_renderer)
Definition: Renderer.php:666
renderTagField(F\Tag $component, RendererInterface $default_renderer)
Definition: Renderer.php:398
setHelpBlockForFileField(Template $template, FI\File $input)
Definition: Renderer.php:1288
renderMultiSelectField(F\MultiSelect $component, RendererInterface $default_renderer)
Definition: Renderer.php:712
renderNumericField(F\Numeric $component, RendererInterface $default_renderer)
Definition: Renderer.php:316
renderColorSelectField(F\ColorSelect $component, RendererInterface $default_renderer)
Definition: Renderer.php:1089
renderOptionFilter(string $input_html, F\HasOptionFilterInternal $component, RendererInterface $default_renderer)
Renders a list search around input fields that support it.
Definition: Renderer.php:1312
renderOptionalGroup(F\OptionalGroup $component, RendererInterface $default_renderer)
Definition: Renderer.php:343
renderSection(F\Section $section, RendererInterface $default_renderer)
Definition: Renderer.php:838
applyName(FormInput $component, Template $tpl)
Definition: Renderer.php:237
renderUrlField(F\Url $component, RendererInterface $default_renderer)
Definition: Renderer.php:855
internalRenderDateTimeField(F\DateTime $component, RendererInterface $default_renderer)
Definition: Renderer.php:764
renderFileField(F\File $input, RendererInterface $default_renderer)
Definition: Renderer.php:871
renderDurationField(F\Duration $component, RendererInterface $default_renderer)
Definition: Renderer.php:815
wrapInFormContext(FormInput $component, string $label, string $input_html, ?string $id_for_label=null, ?string $dependant_group_html=null)
Definition: Renderer.php:177
renderSwitchableGroup(F\SwitchableGroup $component, RendererInterface $default_renderer)
Definition: Renderer.php:361
getTransformedDateFormat(DateFormat\DateFormat $origin, array $mapping)
Return the datetime format in a form fit for the JS-component of this input.
Definition: Renderer.php:988
prepareDropzoneJsMimeTypes(array $mime_types)
Appends all given mime-types to a comma-separated string.
Definition: Renderer.php:1079
renderMustacheVariables(F\HasMustacheVariablesInternal $component)
Definition: Renderer.php:632
renderRatingField(F\Rating $component, RendererInterface $default_renderer)
Definition: Renderer.php:1100
registerResources(ResourceRegistry $registry)
Announce resources this renderer requires.
Definition: Renderer.php:940
renderTreeSelectField(F\TreeSelect $component, RendererInterface $default_renderer)
Definition: Renderer.php:1172
renderFilePreview(FI\File $file_input, FormInput $metadata_input, RendererInterface $default_renderer, ?FileInfoResult $file_info, Template $template)
Definition: Renderer.php:1003
render(Component\Component $component, RendererInterface $default_renderer)
Definition: Renderer.php:88
renderTreeMultiSelectField(F\TreeMultiSelect $component, RendererInterface $default_renderer)
Definition: Renderer.php:1153
iterateGeneratorsInLockstep(\Generator $a, \Generator $b)
Iterates over two Generators in lockstep, yielding their current values as paired arrays which can be...
Definition: Renderer.php:1276
applyValue(FormInput $component, Template $tpl, ?callable $escape=null)
Escape values for rendering with a Callable "$escape" In order to prevent XSS-attacks,...
Definition: Renderer.php:259
renderLinkField(F\Link $component, RendererInterface $default_renderer)
Definition: Renderer.php:290
This implements the section input.
Definition: Section.php:34
This implements the text input.
Definition: Text.php:33
This implements the textarea input.
Definition: Textarea.php:34
This implements the URL input.
Definition: Url.php:35
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.
ilErrorHandling $error
Definition: class.ilias.php:69
return true
This describes inputs that can be used in forms.
Definition: FormInput.php:33
This describes commonalities between all inputs.
Definition: Input.php:47
getValue()
Get the value that is displayed in the input client side.
Interface to be extended by components that have the possibility to bind to Javascript.
withAdditionalOnLoadCode(Closure $binder)
Add some onload-code to the component instead of replacing the existing one.
getOnLoadCode()
Get the currently bound on load code.
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
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
Definition: Checkbox.php:21
This file is part of ILIAS, a powerful learning management system published by ILIAS open source e-Le...
Definition: Checkbox.php:21
trait JavaScriptBindable
Trait for components implementing JavaScriptBindable providing standard implementation.
$a
thx to https://mlocati.github.io/php-cs-fixer-configurator for the examples