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
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 $registry->register('assets/js/tagInput.js');
945
946 $registry->register('assets/js/dropzone.min.js');
947 $registry->register('assets/js/dropzone.js');
948 $registry->register('assets/js/input.js');
949 $registry->register('assets/js/core.js');
950 $registry->register('assets/js/file.js');
951 // workaround to manipulate the order of scripts
952 $registry->register('assets/js/drilldown.min.js');
953 $registry->register('assets/js/input.factory.min.js');
954 }
955
960 protected function setSignals(F\FormInput $input)
961 {
962 $signals = null;
963 foreach ($input->getTriggeredSignals() as $s) {
964 $signals[] = [
965 "signal_id" => $s->getSignal()->getId(),
966 "event" => $s->getEvent(),
967 "options" => $s->getSignal()->getOptions()
968 ];
969 }
970 if ($signals !== null) {
971 $signals = json_encode($signals);
972
973 $input = $input->withAdditionalOnLoadCode($input->getUpdateOnLoadCode());
974
975 $input = $input->withAdditionalOnLoadCode(static function ($id) use ($signals) {
976 $code = "il.UI.input.setSignalsForId('$id', $signals);";
977 return $code;
978 });
979 }
980 return $input;
981 }
982
989 protected function getTransformedDateFormat(
990 DateFormat\DateFormat $origin,
991 array $mapping
992 ): string {
993 $ret = '';
994 foreach ($origin->toArray() as $element) {
995 if (array_key_exists($element, $mapping)) {
996 $ret .= $mapping[$element];
997 } else {
998 $ret .= $element;
999 }
1000 }
1001 return $ret;
1002 }
1003
1004 protected function renderFilePreview(
1005 FI\File $file_input,
1006 FormInput $metadata_input,
1007 RendererInterface $default_renderer,
1008 ?FileInfoResult $file_info,
1009 Template $template
1010 ): Template {
1011 $f = $this->getUIFactory();
1012 $template->setCurrentBlock('block_file_preview');
1013 $template->setVariable('REMOVAL_GLYPH', $default_renderer->render(
1014 $f->button()->shy('', '')->withSymbol($f->symbol()->glyph()->close())
1015 ));
1016
1017 if (null !== $file_info) {
1018 $template->setVariable('FILE_NAME', $file_info->getName());
1019 $template->setVariable(
1020 'FILE_SIZE',
1021 (string) (new DataSize($file_info->getSize(), DataSize::Byte))
1022 );
1023 }
1024
1025 // only render expansion toggles if the input
1026 // contains actual (unhidden) inputs.
1027 if ($file_input->hasMetadataInputs()) {
1028 $template->setVariable('EXPAND_GLYPH', $default_renderer->render(
1029 $f->button()->shy('', '')->withSymbol($f->symbol()->glyph()->expand())
1030 ));
1031 $template->setVariable('COLLAPSE_GLYPH', $default_renderer->render(
1032 $f->button()->shy('', '')->withSymbol($f->symbol()->glyph()->collapse())
1033 ));
1034 }
1035
1036 $template->setVariable('METADATA_INPUTS', $default_renderer->render($metadata_input));
1037
1038 $template->parseCurrentBlock();
1039
1040 return $template;
1041 }
1042
1043 protected function initClientsideFileInput(FI\File $input): FI\File
1044 {
1045 return $input->withAdditionalOnLoadCode(
1046 function ($id) use ($input) {
1047 $current_file_count = count($input->getGeneratedDynamicInputs());
1048 $translations = json_encode($input->getTranslations());
1049 $is_disabled = ($input->isDisabled()) ? 'true' : 'false';
1050 $php_upload_limit = $this->getUploadLimitResolver()->getPhpUploadLimitInBytes();
1051 $should_upload_be_chunked = ($input->getMaxFileSize() > $php_upload_limit) ? 'true' : 'false';
1052 $chunk_size = (int) floor($php_upload_limit * self::FILE_UPLOAD_CHUNK_SIZE_FACTOR);
1053 return "
1054 $(document).ready(function () {
1055 il.UI.Input.File.init(
1056 '$id',
1057 '{$input->getUploadHandler()->getUploadURL()}',
1058 '{$input->getUploadHandler()->getFileRemovalURL()}',
1059 '{$input->getUploadHandler()->getFileIdentifierParameterName()}',
1060 $current_file_count,
1061 {$input->getMaxFiles()},
1062 {$input->getMaxFileSize()},
1063 '{$this->prepareDropzoneJsMimeTypes($input->getAcceptedMimeTypes())}',
1064 $is_disabled,
1065 $translations,
1066 $should_upload_be_chunked,
1067 $chunk_size
1068 );
1069 });
1070 ";
1071 }
1072 );
1073 }
1074
1080 protected function prepareDropzoneJsMimeTypes(array $mime_types): string
1081 {
1082 $mime_type_string = '';
1083 foreach ($mime_types as $index => $mime_type) {
1084 $mime_type_string .= (isset($mime_types[$index + 1])) ? "$mime_type," : $mime_type;
1085 }
1086
1087 return $mime_type_string;
1088 }
1089
1090 protected function renderColorSelectField(F\ColorSelect $component, RendererInterface $default_renderer): string
1091 {
1092 $tpl = $this->getTemplate("tpl.color_select.html", true, true);
1093 $this->applyName($component, $tpl);
1094 $tpl->setVariable('VALUE', $component->getValue());
1095
1096 $label_id = $this->createId();
1097 $tpl->setVariable('ID', $label_id);
1098 return $this->wrapInFormContext($component, $component->getLabel(), $tpl->get(), $label_id);
1099 }
1100
1101 protected function renderRatingField(F\Rating $component, RendererInterface $default_renderer): string
1102 {
1103 $tpl = $this->getTemplate("tpl.rating.html", true, true);
1104 $id = $this->createId();
1105 $aria_description_id = $id . '_desc';
1106 $tpl->setVariable('DESCRIPTION_SRC_ID', $aria_description_id);
1107
1108 $option_count = count(FiveStarRatingScale::cases()) - 1;
1109
1110 foreach (range($option_count, 1, -1) as $option) {
1111 $tpl->setCurrentBlock('scaleoption');
1112 $tpl->setVariable('ARIALABEL', $this->txt($option . 'stars'));
1113 $tpl->setVariable('OPT_VALUE', (string) $option);
1114 $tpl->setVariable('OPT_ID', $id . '-' . $option);
1115 $tpl->setVariable('NAME', $component->getName());
1116 $tpl->setVariable('DESCRIPTION_ID', $aria_description_id);
1117
1118 if ($component->getValue() === FiveStarRatingScale::from((int) $option)) {
1119 $tpl->setVariable("SELECTED", ' checked="checked"');
1120 }
1121 if ($component->isDisabled()) {
1122 $tpl->setVariable("DISABLED", 'disabled="disabled"');
1123 }
1124 $tpl->parseCurrentBlock();
1125 }
1126
1127 if (!$component->isRequired()) {
1128 $tpl->setVariable('NEUTRAL_ID', $id . '-0');
1129 $tpl->setVariable('NEUTRAL_NAME', $component->getName());
1130 $tpl->setVariable('NEUTRAL_LABEL', $this->txt('reset_stars'));
1131 $tpl->setVariable('NEUTRAL_DESCRIPTION_ID', $aria_description_id);
1132
1133 if ($component->getValue() === FiveStarRatingScale::NONE || is_null($component->getValue())) {
1134 $tpl->setVariable('NEUTRAL_SELECTED', ' checked="checked"');
1135 }
1136 }
1137
1138 if ($txt = $component->getAdditionalText()) {
1139 $tpl->setVariable('TEXT', $txt);
1140 }
1141
1142 if ($component->isDisabled()) {
1143 $tpl->touchBlock('disabled');
1144 }
1145 if ($average = $component->getCurrentAverage()) {
1146 $average_title = sprintf($this->txt('rating_average'), $average);
1147 $tpl->setVariable('AVERAGE_VALUE', $average_title);
1148 $tpl->setVariable('AVERAGE_VALUE_PERCENT', $average / $option_count * self::CENTUM);
1149 }
1150
1151 return $this->wrapInFormContext($component, $component->getLabel(), $tpl->get());
1152 }
1153
1154 protected function renderTreeMultiSelectField(F\TreeMultiSelect $component, RendererInterface $default_renderer): string
1155 {
1156 $template = $this->prepareTreeSelectTemplate($component, $default_renderer);
1157
1158 if ($component->canSelectChildNodes()) {
1159 $select_child_nodes = 'true';
1160 } else {
1161 $select_child_nodes = 'false';
1162 }
1163
1164 $enriched_component = $component->withAdditionalOnLoadCode(
1165 static fn($id) => "il.UI.Input.treeSelect.initTreeMultiSelect('$id', $select_child_nodes);"
1166 );
1167
1168 $id = $this->bindJSandApplyId($enriched_component, $template);
1169
1170 return $this->wrapInFormContext($component, $component->getLabel(), $template->get(), $id);
1171 }
1172
1173 protected function renderTreeSelectField(F\TreeSelect $component, RendererInterface $default_renderer): string
1174 {
1175 $template = $this->prepareTreeSelectTemplate($component, $default_renderer);
1176
1177 $enriched_component = $component->withAdditionalOnLoadCode(
1178 static fn($id) => "il.UI.Input.treeSelect.initTreeSelect('$id');"
1179 );
1180
1181 $id = $this->bindJSandApplyId($enriched_component, $template);
1182
1183 return $this->wrapInFormContext($component, $component->getLabel(), $template->get(), $id);
1184 }
1185
1186 protected function prepareTreeSelectTemplate(
1187 TreeSelect|TreeMultiSelect $component,
1188 RendererInterface $default_renderer,
1189 ): Template {
1190 $template = $this->getTemplate('tpl.tree_select.html', true, true);
1191
1192 if ($component->isDisabled()) {
1193 $template->setVariable('DISABLED', 'disabled');
1194 }
1195
1196 $template->setVariable('SELECT_LABEL', $this->txt('select'));
1197 $template->setVariable('CLOSE_LABEL', $this->txt('close'));
1198 $template->setVariable('LABEL', $component->getLabel());
1199
1200 $template->setVariable('INPUT_TEMPLATE', $default_renderer->render(
1201 $component->getTemplateForDynamicInputs()
1202 ));
1203 $template->setVariable('BREADCRUMB_TEMPLATE', $default_renderer->render(
1204 $this->getUIFactory()->breadcrumbs([$this->getUIFactory()->link()->standard('label', '#')])
1205 ));
1206 $template->setVariable('BREADCRUMBS', $default_renderer->render(
1207 $this->getUIFactory()->breadcrumbs([])
1208 ));
1209
1211 $dynamic_inputs_generator = (static fn() => yield from $component->getGeneratedDynamicInputs())();
1212
1213 $leaf_generator = $component->getNodeRetrieval()->getNodesAsLeaf(
1214 $this->getUIFactory()->input()->field()->node(),
1215 $this->getUIFactory()->symbol()->icon(),
1216 $component->getValue(),
1217 );
1218
1219 $lockstep_iterator = $this->iterateGeneratorsInLockstep($leaf_generator, $dynamic_inputs_generator);
1220 $sync_node_id_whitelst = [];
1221
1222 foreach ($lockstep_iterator as [$leaf, $dynamic_input]) {
1223 // check against internal interface, will not be delegated to rendering chain.
1225 $this->checkArgInstanceOf('leaf', $leaf, Node\Leaf::class);
1226
1227 $value_template = $this->getTemplate('tpl.tree_select.html', true, true);
1228 $value_template->setCurrentBlock('with_value_template');
1229 $value_template->setVariable('NODE_ID', (string) ($leaf->getId()));
1230 $value_template->setVariable('NODE_NAME', $leaf->getName());
1231 $value_template->setVariable('INPUT_TEMPLATE', $default_renderer->render($dynamic_input));
1232 $value_template->setVariable('UNSELECT_NODE_LABEL', sprintf($this->txt('unselect_node'), $leaf->getName()));
1233 $value_template->parseCurrentBlock();
1234
1235 $template->setCurrentBlock('with_value');
1236 $template->setVariable('VALUE', $value_template->get('with_value_template'));
1237 $template->parseCurrentBlock();
1238
1239 foreach ($leaf->getFullPath() as $node_id) {
1240 // deduplicate overlaping node ids by using them as offset
1241 $sync_node_id_whitelst[$node_id] = $node_id;
1242 }
1243 }
1244
1245 $node_factory = $this->getUIFactory()->input()->field()->node();
1246 $node_generator = $component->getNodeRetrieval()->getNodes(
1247 $node_factory,
1248 $this->getUIFactory()->symbol()->icon(),
1249 array_values($sync_node_id_whitelst),
1250 null,
1251 );
1252
1253 $nodes = [];
1254 foreach ($node_generator as $node) {
1255 // check against public interface, will be delegated to rendering chain.
1256 $this->checkArgInstanceOf('node', $node, Component\Input\Field\Node\Node::class);
1257 $nodes[] = $node;
1258 }
1259
1260 $template->setVariable('DRILLDOWN', $default_renderer->render(
1261 $this->getUIFactory()->menu()->drilldown($component->getLabel(), $nodes)
1262 ));
1263
1264 $this->toJS('unselect_node');
1265 $this->toJS('select_node');
1266
1267 return $template;
1268 }
1269
1277 protected function iterateGeneratorsInLockstep(\Generator $a, \Generator $b): \Generator
1278 {
1279 while ($a->valid() && $b->valid()) {
1280 yield [$a->current(), $b->current()];
1281 $a->next();
1282 $b->next();
1283 }
1284 if ($a->valid() || $b->valid()) {
1285 throw new LogicException('Generators do not have equal lenghts.');
1286 }
1287 }
1288
1289 private function setHelpBlockForFileField(Template $template, FI\File $input): void
1290 {
1291 $template->setCurrentBlock('HELP_BLOCK');
1292
1293 $template->setCurrentBlock('MAX_FILE_SIZE');
1294 $template->setVariable('FILE_SIZE_LABEL', $this->txt('file_notice'));
1295 $template->setVariable('FILE_SIZE_VALUE', new DataSize($input->getMaxFileSize(), DataSize::Byte));
1296 $template->parseCurrentBlock();
1297
1298 $template->setCurrentBlock('MAX_FILES');
1299 $template->setVariable('FILES_LABEL', $this->txt('ui_file_upload_max_nr'));
1300 $template->setVariable('FILES_VALUE', $input->getMaxFiles());
1301 $template->parseCurrentBlock();
1302
1303 $template->parseCurrentBlock();
1304 }
1305
1313 protected function renderOptionFilter(string $input_html, F\HasOptionFilterInternal $component, RendererInterface $default_renderer): array
1314 {
1315 $option_filter_template = $this->getTemplate("tpl.option_filter.html", true, true);
1316 $option_filter_template->setVariable('INPUT', $input_html);
1317
1318 $search_input_id = $this->createId();
1319 $search_input_label_id = $this->createId();
1320 $search_input_description_id = $this->createId();
1321 $list_id = $this->createId();
1322
1323 $option_filter_template->setVariable('SEARCH_INPUT_ID', $search_input_id);
1324 $option_filter_template->setVariable('SEARCH_INPUT_LABEL_ID', $search_input_label_id);
1325 $option_filter_template->setVariable('SEARCH_INPUT_DESCRIPTION_ID', $search_input_description_id);
1326 $option_filter_template->setVariable('LIST_ID', $list_id);
1327
1328 $no_selection_text = $this->txt('ui_field_option_filter_no_selection');
1329 $option_filter_template->setVariable('NOTHING_SELECTED', $no_selection_text);
1330 $option_filter_template->setVariable('ARIA_FILTERED_RESULTS', $this->txt('ui_field_option_filter_filtered_results_aria_label'));
1331
1332 $option_filter_template->setVariable('SEARCH_LABEL', $this->txt("ui_field_option_filter_search_in"));
1333 $option_filter_template->setVariable('SCREEN_READER_HINT', $this->txt('ui_field_option_filter_screen_reader_hint'));
1334 $option_filter_template->setVariable('NO_MATCH', $this->txt('ui_field_option_filter_no_match'));
1335 $option_filter_template->setVariable('OPTIONS_SHOWN', $this->txt('ui_field_option_filter_options_shown'));
1336
1337 $expand_icon = $default_renderer->render($this->getUIFactory()->symbol()->glyph()->expand()->withLabel(''));
1338 $option_filter_template->setVariable('EXPAND_TEXT', $expand_icon . $this->txt('ui_field_option_filter_show_all_options'));
1339
1340 $collapse_icon = $default_renderer->render($this->getUIFactory()->symbol()->glyph()->collapseHorizontal()->withLabel(''));
1341 $option_filter_template->setVariable('COLLAPSE_TEXT', $collapse_icon . $this->txt('ui_field_option_filter_show_less'));
1342
1343 $remove_icon = $default_renderer->render($this->getUIFactory()->symbol()->glyph()->remove()->withLabel(''));
1344 $option_filter_template->setVariable('CLEAR_SEARCH_BTN', $remove_icon . $this->txt('ui_field_option_filter_clear_search'));
1345
1346 $component = $component->withAdditionalOnLoadCode(
1347 static fn($id): string => "il.UI.Input.optionFilter.init(document.getElementById('$id'));",
1348 );
1349
1350 return [$option_filter_template->get(), $component];
1351 }
1352
1353 private function mustacheVariableEntities(): Closure
1354 {
1355 return function ($val) {
1356 $val = htmlentities((string) $val);
1357 return str_replace('{{', '&lcub;&lcub;', str_replace('}}', '&rcub;&rcub;', $val));
1358 };
1359 }
1360}
$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:27
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:1289
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:1090
renderOptionFilter(string $input_html, F\HasOptionFilterInternal $component, RendererInterface $default_renderer)
Renders a list search around input fields that support it.
Definition: Renderer.php:1313
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:989
prepareDropzoneJsMimeTypes(array $mime_types)
Appends all given mime-types to a comma-separated string.
Definition: Renderer.php:1080
renderMustacheVariables(F\HasMustacheVariablesInternal $component)
Definition: Renderer.php:632
renderRatingField(F\Rating $component, RendererInterface $default_renderer)
Definition: Renderer.php:1101
registerResources(ResourceRegistry $registry)
Announce resources this renderer requires.
Definition: Renderer.php:940
renderTreeSelectField(F\TreeSelect $component, RendererInterface $default_renderer)
Definition: Renderer.php:1173
renderFilePreview(FI\File $file_input, FormInput $metadata_input, RendererInterface $default_renderer, ?FileInfoResult $file_info, Template $template)
Definition: Renderer.php:1004
render(Component\Component $component, RendererInterface $default_renderer)
Definition: Renderer.php:88
renderTreeMultiSelectField(F\TreeMultiSelect $component, RendererInterface $default_renderer)
Definition: Renderer.php:1154
iterateGeneratorsInLockstep(\Generator $a, \Generator $b)
Iterates over two Generators in lockstep, yielding their current values as paired arrays which can be...
Definition: Renderer.php:1277
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