ILIAS  trunk Revision v12.0_alpha-377-g3641b37b9db
Renderer.php
Go to the documentation of this file.
1<?php
2
19declare(strict_types=1);
20
22
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 $glyph_reveal = $f->symbol()->glyph()->eyeopen("#")
470 ->withOnClick($sig_reveal);
471 $glyph_mask = $f->symbol()->glyph()->eyeclosed("#")
472 ->withOnClick($sig_mask);
473
474 $tpl->setVariable('PASSWORD_REVEAL', $default_renderer->render($glyph_reveal));
475 $tpl->setVariable('PASSWORD_MASK', $default_renderer->render($glyph_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 //disable first option if required.
492 $tpl->setCurrentBlock("options");
493 if (!$value) {
494 $tpl->setVariable("SELECTED", 'selected="selected"');
495 }
496 if ($component->isRequired() && !$value) {
497 $tpl->setVariable("DISABLED_OPTION", "disabled");
498 $tpl->setVariable("HIDDEN", "hidden");
499 }
500
501 if (!($value && $component->isRequired())) {
502 $tpl->setVariable("VALUE", null);
503 $tpl->setVariable("VALUE_STR", $component->isRequired() ? $this->txt('ui_select_dropdown_label') : '-');
504 $tpl->parseCurrentBlock();
505 }
506
507 foreach ($component->getOptions() as $option_key => $option_value) {
508 $tpl->setCurrentBlock("options");
509 if ($value == $option_key) {
510 $tpl->setVariable("SELECTED", 'selected="selected"');
511 }
512 $tpl->setVariable("VALUE", $option_key);
513 $tpl->setVariable("VALUE_STR", $option_value);
514 $tpl->parseCurrentBlock();
515 }
516
517 $label_id = $this->createId();
518 $tpl->setVariable('ID', $label_id);
519 return $this->wrapInFormContext($component, $component->getLabel(), $tpl->get(), $label_id);
520 }
521
522 protected function renderMarkdownField(F\Markdown $component, RendererInterface $default_renderer): string
523 {
524 [$textarea_tpl, $component] = $this->getPreparedTextareaTemplate($component);
525
527 $component = $component->withAdditionalOnLoadCode(
528 static function ($id) use ($component): string {
529 return "
530 il.UI.Input.markdown.init(
531 document.querySelector('#$id .c-input__field textarea')?.id,
532 '{$component->getMarkdownRenderer()->getAsyncUrl()}',
533 '{$component->getMarkdownRenderer()->getParameterName()}'
534 );
535 ";
536 }
537 );
538
539 $textarea_id = $this->createId();
540 $textarea_tpl->setVariable('ID', $textarea_id);
541
542 $markdown_tpl = $this->getTemplate("tpl.markdown.html", true, true);
543 $markdown_tpl->setVariable('TEXTAREA', $textarea_tpl->get());
544
545 $markdown_tpl->setVariable(
546 'PREVIEW',
547 $component->getMarkdownRenderer()->render(
548 $this->htmlEntities()($component->getValue() ?? '')
549 )
550 );
551
552 $markdown_tpl->setVariable(
553 'VIEW_CONTROLS',
554 $default_renderer->render(
555 $this->getUIFactory()->viewControl()->mode([
556 $this->txt('ui_md_input_edit') => '#',
557 $this->txt('ui_md_input_view') => '#',
558 ], "")
559 )
560 );
561
563 $markdown_actions_glyphs = [
564 'ACTION_HEADING' => $this->getUIFactory()->symbol()->glyph()->header(),
565 'ACTION_LINK' => $this->getUIFactory()->symbol()->glyph()->link(),
566 'ACTION_BOLD' => $this->getUIFactory()->symbol()->glyph()->bold(),
567 'ACTION_ITALIC' => $this->getUIFactory()->symbol()->glyph()->italic(),
568 'ACTION_ORDERED_LIST' => $this->getUIFactory()->symbol()->glyph()->numberedlist(),
569 'ACTION_UNORDERED_LIST' => $this->getUIFactory()->symbol()->glyph()->bulletlist()
570 ];
571
572 foreach ($markdown_actions_glyphs as $tpl_variable => $glyph) {
573 if ($component->isDisabled()) {
574 $glyph = $glyph->withUnavailableAction();
575 }
576
577 $action = $this->getUIFactory()->button()->standard('', '#')->withSymbol($glyph);
578
579 if ($component->isDisabled()) {
580 $action = $action->withUnavailableAction();
581 }
582
583 $markdown_tpl->setVariable($tpl_variable, $default_renderer->render($action));
584 }
585
586 return $this->wrapInFormContext($component, $component->getLabel(), $markdown_tpl->get());
587 }
588
589 protected function renderTextareaField(F\Textarea $component, RendererInterface $default_renderer): string
590 {
591 [$tpl, $component] = $this->getPreparedTextareaTemplate($component);
592
594 $component = $component->withAdditionalOnLoadCode(
595 static fn($id) => "il.UI.Input.textarea.init(document.querySelector('#$id .c-input__field textarea')?.id);",
596 );
597
598 $label_id = $this->createId();
599 $tpl->setVariable('ID', $label_id);
600 return $this->wrapInFormContext($component, $component->getLabel(), $tpl->get(), $label_id);
601 }
602
606 protected function getPreparedTextareaTemplate(F\Textarea $component): array
607 {
608 $tpl = $this->getTemplate("tpl.textarea.html", true, true);
609
610 if (0 < $component->getMaxLimit()) {
611 $tpl->setVariable('REMAINDER_TEXT', $this->txt('ui_chars_remaining'));
612 $tpl->setVariable('REMAINDER', $component->getMaxLimit() - strlen($component->getValue() ?? ''));
613 $tpl->setVariable('MAX_LIMIT', $component->getMaxLimit());
614 }
615
616 if (null !== $component->getMinLimit()) {
617 $tpl->setVariable('MIN_LIMIT', $component->getMinLimit());
618 }
619
620 [$mustache_variable_html, $component] = $this->renderMustacheVariables($component);
621 $tpl->setVariable('MUSTACHE_VARIABLES_HTML', $mustache_variable_html);
622
623 $this->applyName($component, $tpl);
624 $this->applyValue(
625 $component,
626 $tpl,
627 $this->mustacheVariableEntities()
628 );
629 return [$tpl, $component];
630 }
631
635 protected function renderMustacheVariables(F\HasMustacheVariablesInternal $component): array
636 {
637 $mustache_variable_definitions = $component->getMustacheVariables();
639 return ['', $component];
640 }
641
642 $template = $this->getTemplate('tpl.mustache_variables.html', true, true);
643 $template->setVariable('MUSTACHE_VARIABLE_USAGE_INFO', $this->txt('ui_mustache_variables_usage_info'));
644
645 $mustache_variable_context_info = $component->getMustacheVariableContextInfo();
646 if (null !== $mustache_variable_context_info) {
647 $template->setVariable('MUSTACHE_VARIABLE_CONTEXT_INFO', $mustache_variable_context_info);
648 }
649
650 foreach ($mustache_variable_definitions as $variable_name => $description) {
651 $template->setCurrentBlock('with_mustache_variable_definition');
652 $template->setVariable('VARIABLE_NAME', $variable_name);
653 $template->setVariable('VARIABLE_DESCRIPTION', $description);
654 $template->parseCurrentBlock();
655 }
656
657 // @todo: this feature is currently highly coupled to textareas
658 $enriched_component = $component->withAdditionalOnLoadCode(static fn($id) => "
659 il.UI.Input.mustacheVariables.init(
660 il.UI.Input.textarea.get(document.querySelector('#$id .c-input__field textarea')?.id) ??
661 il.UI.Input.markdown.get(document.querySelector('#$id .c-input__field textarea')?.id),
662 document.getElementById('$id'),
663 );
664 ");
665
666 return [$template->get(), $enriched_component];
667 }
668
669 protected function renderRadioField(F\Radio $component, RendererInterface $default_renderer): string
670 {
671 $tpl = $this->getTemplate("tpl.radio.html", true, true);
672 $id = $this->createId();
673
674 foreach ($component->getOptions() as $value => $label) {
675 // @todo: can we get rid of this enganglement?
676 if ($component->hasOptionFilter()) {
677 $tpl->touchBlock('is_filter_option');
678 }
679
680 $opt_id = $id . '_' . $value . '_opt';
681
682 $tpl->setCurrentBlock('optionblock');
683 $tpl->setVariable("NAME", $component->getName());
684 $tpl->setVariable("OPTIONID", $opt_id);
685 $tpl->setVariable("VALUE", $value);
686 $tpl->setVariable("LABEL", $label);
687
688 if ($component->getValue() !== null && $component->getValue() == $value) {
689 $tpl->setVariable("CHECKED", 'checked="checked"');
690 }
691 if ($component->isDisabled()) {
692 $tpl->setVariable("DISABLED", 'disabled="disabled"');
693 }
694
695 $byline = $component->getBylineFor((string) $value);
696 if (!empty($byline)) {
697 $tpl->setVariable("BYLINE", $byline);
698 }
699
700 $tpl->parseCurrentBlock();
701 }
702
703 if ($component->hasOptionFilter()) {
704 // @todo: can we get rid of this enganglement?
705 $tpl->touchBlock("has_option_filter");
706 $field_html = $tpl->get();
707 [$field_html, $component] = $this->renderOptionFilter($field_html, $component, $default_renderer);
708 } else {
709 $field_html = $tpl->get();
710 }
711
712 return $this->wrapInFormContext($component, $component->getLabel(), $field_html);
713 }
714
715 protected function renderMultiSelectField(F\MultiSelect $component, RendererInterface $default_renderer): string
716 {
717 $tpl = $this->getTemplate("tpl.multiselect.html", true, true);
718
719 $options = $component->getOptions();
720 if (count($options) > 0) {
721 $value = $component->getValue();
722 $name = $this->applyName($component, $tpl);
723 foreach ($options as $opt_value => $opt_label) {
724 // @todo: can we get rid of this enganglement?
725 if ($component->hasOptionFilter()) {
726 $tpl->touchBlock('is_filter_option');
727 }
728
729 $tpl->setCurrentBlock("option");
730 $tpl->setVariable("NAME", $name);
731 $tpl->setVariable("VALUE", $opt_value);
732 $tpl->setVariable("LABEL", $opt_label);
733
734 if ($value && in_array($opt_value, $value)) {
735 $tpl->setVariable("CHECKED", 'checked="checked"');
736 }
737
738 $tpl->parseCurrentBlock();
739 }
740 } else {
741 $tpl->touchBlock("no_options");
742 }
743
744 if ($component->hasOptionFilter()) {
745 // @todo: can we get rid of this enganglement?
746 $tpl->touchBlock("has_option_filter");
747 $field_html = $tpl->get();
748 [$field_html, $component] = $this->renderOptionFilter($field_html, $component, $default_renderer);
749 } else {
750 $field_html = $tpl->get();
751 }
752
753 return $this->wrapInFormContext($component, $component->getLabel(), $field_html);
754 }
755
756 protected function renderDateTimeField(F\DateTime $component, RendererInterface $default_renderer): string
757 {
758 list($component, $tpl) = $this->internalRenderDateTimeField($component, $default_renderer);
759 $label_id = $this->createId();
760 $tpl->setVariable('ID', $label_id);
761 return $this->wrapInFormContext($component, $component->getLabel(), $tpl->get(), $label_id);
762 }
763
767 protected function internalRenderDateTimeField(F\DateTime $component, RendererInterface $default_renderer): array
768 {
769 $tpl = $this->getTemplate("tpl.datetime.html", true, true);
770 $this->applyName($component, $tpl);
771
772 if ($component->getTimeOnly() === true) {
773 $format = $component::TIME_FORMAT;
774 $dt_type = self::TYPE_TIME;
775 } else {
776 $dt_type = self::TYPE_DATE;
777 $format = $this->getTransformedDateFormat(
778 $component->getFormat(),
779 self::DATEPICKER_FORMAT_MAPPING
780 );
781
782 if ($component->getUseTime() === true) {
783 $format .= ' ' . $component::TIME_FORMAT;
784 $dt_type = self::TYPE_DATETIME;
785 }
786 }
787
788 $tpl->setVariable("DTTYPE", $dt_type);
789
790 $min_max_format = self::DATE_DATEPICKER_MINMAX_FORMAT;
791 if ($dt_type === self::TYPE_DATETIME) {
792 $min_max_format = self::DATETIME_DATEPICKER_MINMAX_FORMAT;
793 }
794
795 $min_date = $component->getMinValue();
796 if (!is_null($min_date)) {
797 $tpl->setVariable("MIN_DATE", date_format($min_date, $min_max_format));
798 }
799 $max_date = $component->getMaxValue();
800 if (!is_null($max_date)) {
801 $tpl->setVariable("MAX_DATE", date_format($max_date, $min_max_format));
802 }
803
804 $this->applyValue($component, $tpl, function (?string $value) use ($dt_type) {
805 if ($value !== null) {
806 $value = new \DateTimeImmutable($value);
807 return $value->format(match ($dt_type) {
808 self::TYPE_DATETIME => self::HTML5_NATIVE_DATETIME_FORMAT,
809 self::TYPE_DATE => self::HTML5_NATIVE_DATE_FORMAT,
810 self::TYPE_TIME => self::HTML5_NATIVE_TIME_FORMAT,
811 });
812 }
813 return null;
814 });
815 return [$component, $tpl];
816 }
817
818 protected function renderDurationField(F\Duration $component, RendererInterface $default_renderer): string
819 {
820 $inputs = $component->getInputs();
821
822 $input = array_shift($inputs); //from
823 list($input, $tpl) = $this->internalRenderDateTimeField($input, $default_renderer);
824
825 $from_input_id = $this->createId();
826 $tpl->setVariable('ID', $from_input_id);
827 $input_html = $this->wrapInFormContext($input, $input->getLabel(), $tpl->get(), $from_input_id);
828
829 $input = array_shift($inputs) //until
830 ->withAdditionalPickerconfig(['useCurrent' => false]);
831 list($input, $tpl) = $this->internalRenderDateTimeField($input, $default_renderer);
832 $until_input_id = $this->createId();
833 $tpl->setVariable('ID', $until_input_id);
834 $input_html .= $this->wrapInFormContext($input, $input->getLabel(), $tpl->get(), $until_input_id);
835
836 $tpl = $this->getTemplate("tpl.duration.html", true, true);
837 $tpl->setVariable('DURATION', $input_html);
838 return $this->wrapInFormContext($component, $component->getLabel(), $tpl->get());//, $from_input_id);
839 }
840
841 protected function renderSection(F\Section $section, RendererInterface $default_renderer): string
842 {
843 $inputs_html = $default_renderer->render($section->getInputs());
844
845 $headline_tpl = $this->getTemplate("tpl.headlines.html", true, true);
846 $headline_tpl->setVariable("HEADLINE", $section->getLabel());
847 $nesting_level = $section->getNestingLevel() + 2;
848 if ($nesting_level > 6) {
849 $nesting_level = 6;
850 };
851 $headline_tpl->setVariable("LEVEL", $nesting_level);
852
853 $headline_html = $headline_tpl->get();
854
855 return $this->wrapInFormContext($section, $headline_html, $inputs_html);
856 }
857
858 protected function renderUrlField(F\Url $component, RendererInterface $default_renderer): string
859 {
860 $tpl = $this->getTemplate("tpl.url.html", true, true);
861 $this->applyName($component, $tpl);
862 $this->applyValue($component, $tpl, $this->escapeSpecialChars());
863
864 $label_id = $this->createId();
865 $tpl->setVariable('ID', $label_id);
866 return $this->wrapInFormContext($component, $component->getLabel(), $tpl->get(), $label_id);
867 }
868
869 protected function renderImageField(F\Image $input, RendererInterface $default_renderer): string
870 {
871 return $this->renderFileField($input, $default_renderer);
872 }
873
874 protected function renderFileField(F\File $input, RendererInterface $default_renderer): string
875 {
876 $template = $this->getTemplate('tpl.file.html', true, true);
877 foreach ($input->getGeneratedDynamicInputs() as $metadata_input) {
878 $file_info = null;
879 if (null !== ($data = $metadata_input->getValue())) {
880 $file_id = (!$input->hasMetadataInputs()) ? $data : $data[0] ?? null;
881
882 if (null !== $file_id) {
883 $file_info = $input->getUploadHandler()->getInfoResult($file_id);
884 }
885 }
886
887 $template = $this->renderFilePreview(
888 $input,
889 $metadata_input,
890 $default_renderer,
891 $file_info,
892 $template
893 );
894 }
895
896 $file_preview_template = $this->getTemplate('tpl.file.html', true, true);
897 $file_preview_template = $this->renderFilePreview(
898 $input,
899 $input->getTemplateForDynamicInputs(),
900 $default_renderer,
901 null,
902 $file_preview_template
903 );
904
905 $template->setVariable('FILE_PREVIEW_TEMPLATE', $file_preview_template->get('block_file_preview'));
906
907 $this->setHelpBlockForFileField($template, $input);
908
909 $input = $this->initClientsideFileInput($input);
910
911 // display the action button (to choose files).
912 $template->setVariable('ACTION_BUTTON', $default_renderer->render(
913 $this->getUIFactory()->button()->shy(
914 $input->getMaxFiles() <= 1
915 ? $this->txt('select_file_from_computer')
916 : $this->txt('select_files_from_computer'),
917 '#'
918 )
919 ));
920
921 return $this->wrapInFormContext(
922 $input,
923 $input->getLabel(),
924 $template->get(),
925 );
926 }
927
928 protected function renderHiddenField(F\Hidden $input): string
929 {
930 $template = $this->getTemplate('tpl.hidden.html', true, true);
931 $this->applyName($input, $template);
932 $this->applyValue($input, $template, $this->escapeSpecialChars());
933 if ($input->isDisabled()) {
934 $template->setVariable("DISABLED", 'disabled="disabled"');
935 }
936 $this->bindJSandApplyId($input, $template);
937 return $template->get();
938 }
939
943 public function registerResources(ResourceRegistry $registry): void
944 {
945 parent::registerResources($registry);
946 $registry->register('assets/css/tagify.css');
947 $registry->register('assets/js/tagInput.js');
948
949 $registry->register('assets/js/dropzone.min.js');
950 $registry->register('assets/js/dropzone.js');
951 $registry->register('assets/js/input.js');
952 $registry->register('assets/js/core.js');
953 $registry->register('assets/js/file.js');
954 // workaround to manipulate the order of scripts
955 $registry->register('assets/js/drilldown.min.js');
956 $registry->register('assets/js/input.factory.min.js');
957 }
958
963 protected function setSignals(F\FormInput $input)
964 {
965 $signals = null;
966 foreach ($input->getTriggeredSignals() as $s) {
967 $signals[] = [
968 "signal_id" => $s->getSignal()->getId(),
969 "event" => $s->getEvent(),
970 "options" => $s->getSignal()->getOptions()
971 ];
972 }
973 if ($signals !== null) {
974 $signals = json_encode($signals);
975
976 $input = $input->withAdditionalOnLoadCode(function ($id) use ($signals) {
977 $code = "il.UI.input.setSignalsForId('$id', $signals);";
978 return $code;
979 });
980
981 $input = $input->withAdditionalOnLoadCode($input->getUpdateOnLoadCode());
982 }
983 return $input;
984 }
985
992 protected function getTransformedDateFormat(
993 DateFormat\DateFormat $origin,
994 array $mapping
995 ): string {
996 $ret = '';
997 foreach ($origin->toArray() as $element) {
998 if (array_key_exists($element, $mapping)) {
999 $ret .= $mapping[$element];
1000 } else {
1001 $ret .= $element;
1002 }
1003 }
1004 return $ret;
1005 }
1006
1007 protected function renderFilePreview(
1008 FI\File $file_input,
1009 FormInput $metadata_input,
1010 RendererInterface $default_renderer,
1011 ?FileInfoResult $file_info,
1012 Template $template
1013 ): Template {
1014 $template->setCurrentBlock('block_file_preview');
1015 $template->setVariable('REMOVAL_GLYPH', $default_renderer->render(
1016 $this->getUIFactory()->symbol()->glyph()->close()->withAction("#")
1017 ));
1018
1019 if (null !== $file_info) {
1020 $template->setVariable('FILE_NAME', $file_info->getName());
1021 $template->setVariable(
1022 'FILE_SIZE',
1023 (string) (new DataSize($file_info->getSize(), DataSize::Byte))
1024 );
1025 }
1026
1027 // only render expansion toggles if the input
1028 // contains actual (unhidden) inputs.
1029 if ($file_input->hasMetadataInputs()) {
1030 $template->setVariable('EXPAND_GLYPH', $default_renderer->render(
1031 $this->getUIFactory()->symbol()->glyph()->expand()->withAction("#")
1032 ));
1033 $template->setVariable('COLLAPSE_GLYPH', $default_renderer->render(
1034 $this->getUIFactory()->symbol()->glyph()->collapse()->withAction("#")
1035 ));
1036 }
1037
1038 $template->setVariable('METADATA_INPUTS', $default_renderer->render($metadata_input));
1039
1040 $template->parseCurrentBlock();
1041
1042 return $template;
1043 }
1044
1045 protected function initClientsideFileInput(FI\File $input): FI\File
1046 {
1047 return $input->withAdditionalOnLoadCode(
1048 function ($id) use ($input) {
1049 $current_file_count = count($input->getGeneratedDynamicInputs());
1050 $translations = json_encode($input->getTranslations());
1051 $is_disabled = ($input->isDisabled()) ? 'true' : 'false';
1052 $php_upload_limit = $this->getUploadLimitResolver()->getPhpUploadLimitInBytes();
1053 $should_upload_be_chunked = ($input->getMaxFileSize() > $php_upload_limit) ? 'true' : 'false';
1054 $chunk_size = (int) floor($php_upload_limit * self::FILE_UPLOAD_CHUNK_SIZE_FACTOR);
1055 return "
1056 $(document).ready(function () {
1057 il.UI.Input.File.init(
1058 '$id',
1059 '{$input->getUploadHandler()->getUploadURL()}',
1060 '{$input->getUploadHandler()->getFileRemovalURL()}',
1061 '{$input->getUploadHandler()->getFileIdentifierParameterName()}',
1062 $current_file_count,
1063 {$input->getMaxFiles()},
1064 {$input->getMaxFileSize()},
1065 '{$this->prepareDropzoneJsMimeTypes($input->getAcceptedMimeTypes())}',
1066 $is_disabled,
1067 $translations,
1068 $should_upload_be_chunked,
1069 $chunk_size
1070 );
1071 });
1072 ";
1073 }
1074 );
1075 }
1076
1082 protected function prepareDropzoneJsMimeTypes(array $mime_types): string
1083 {
1084 $mime_type_string = '';
1085 foreach ($mime_types as $index => $mime_type) {
1086 $mime_type_string .= (isset($mime_types[$index + 1])) ? "$mime_type," : $mime_type;
1087 }
1088
1089 return $mime_type_string;
1090 }
1091
1092 protected function renderColorSelectField(F\ColorSelect $component, RendererInterface $default_renderer): string
1093 {
1094 $tpl = $this->getTemplate("tpl.color_select.html", true, true);
1095 $this->applyName($component, $tpl);
1096 $tpl->setVariable('VALUE', $component->getValue());
1097
1098 $label_id = $this->createId();
1099 $tpl->setVariable('ID', $label_id);
1100 return $this->wrapInFormContext($component, $component->getLabel(), $tpl->get(), $label_id);
1101 }
1102
1103 protected function renderRatingField(F\Rating $component, RendererInterface $default_renderer): string
1104 {
1105 $tpl = $this->getTemplate("tpl.rating.html", true, true);
1106 $id = $this->createId();
1107 $aria_description_id = $id . '_desc';
1108 $tpl->setVariable('DESCRIPTION_SRC_ID', $aria_description_id);
1109
1110 $option_count = count(FiveStarRatingScale::cases()) - 1;
1111
1112 foreach (range($option_count, 1, -1) as $option) {
1113 $tpl->setCurrentBlock('scaleoption');
1114 $tpl->setVariable('ARIALABEL', $this->txt($option . 'stars'));
1115 $tpl->setVariable('OPT_VALUE', (string) $option);
1116 $tpl->setVariable('OPT_ID', $id . '-' . $option);
1117 $tpl->setVariable('NAME', $component->getName());
1118 $tpl->setVariable('DESCRIPTION_ID', $aria_description_id);
1119
1120 if ($component->getValue() === FiveStarRatingScale::from((int) $option)) {
1121 $tpl->setVariable("SELECTED", ' checked="checked"');
1122 }
1123 if ($component->isDisabled()) {
1124 $tpl->setVariable("DISABLED", 'disabled="disabled"');
1125 }
1126 $tpl->parseCurrentBlock();
1127 }
1128
1129 if (!$component->isRequired()) {
1130 $tpl->setVariable('NEUTRAL_ID', $id . '-0');
1131 $tpl->setVariable('NEUTRAL_NAME', $component->getName());
1132 $tpl->setVariable('NEUTRAL_LABEL', $this->txt('reset_stars'));
1133 $tpl->setVariable('NEUTRAL_DESCRIPTION_ID', $aria_description_id);
1134
1135 if ($component->getValue() === FiveStarRatingScale::NONE || is_null($component->getValue())) {
1136 $tpl->setVariable('NEUTRAL_SELECTED', ' checked="checked"');
1137 }
1138 }
1139
1140 if ($txt = $component->getAdditionalText()) {
1141 $tpl->setVariable('TEXT', $txt);
1142 }
1143
1144 if ($component->isDisabled()) {
1145 $tpl->touchBlock('disabled');
1146 }
1147 if ($average = $component->getCurrentAverage()) {
1148 $average_title = sprintf($this->txt('rating_average'), $average);
1149 $tpl->setVariable('AVERAGE_VALUE', $average_title);
1150 $tpl->setVariable('AVERAGE_VALUE_PERCENT', $average / $option_count * self::CENTUM);
1151 }
1152
1153 return $this->wrapInFormContext($component, $component->getLabel(), $tpl->get());
1154 }
1155
1156 protected function renderTreeMultiSelectField(F\TreeMultiSelect $component, RendererInterface $default_renderer): string
1157 {
1158 $template = $this->prepareTreeSelectTemplate($component, $default_renderer);
1159
1160 if ($component->canSelectChildNodes()) {
1161 $select_child_nodes = 'true';
1162 } else {
1163 $select_child_nodes = 'false';
1164 }
1165
1166 $enriched_component = $component->withAdditionalOnLoadCode(
1167 static fn($id) => "il.UI.Input.treeSelect.initTreeMultiSelect('$id', $select_child_nodes);"
1168 );
1169
1170 $id = $this->bindJSandApplyId($enriched_component, $template);
1171
1172 return $this->wrapInFormContext($component, $component->getLabel(), $template->get(), $id);
1173 }
1174
1175 protected function renderTreeSelectField(F\TreeSelect $component, RendererInterface $default_renderer): string
1176 {
1177 $template = $this->prepareTreeSelectTemplate($component, $default_renderer);
1178
1179 $enriched_component = $component->withAdditionalOnLoadCode(
1180 static fn($id) => "il.UI.Input.treeSelect.initTreeSelect('$id');"
1181 );
1182
1183 $id = $this->bindJSandApplyId($enriched_component, $template);
1184
1185 return $this->wrapInFormContext($component, $component->getLabel(), $template->get(), $id);
1186 }
1187
1188 protected function prepareTreeSelectTemplate(
1189 TreeSelect|TreeMultiSelect $component,
1190 RendererInterface $default_renderer,
1191 ): Template {
1192 $template = $this->getTemplate('tpl.tree_select.html', true, true);
1193
1194 if ($component->isDisabled()) {
1195 $template->setVariable('DISABLED', 'disabled');
1196 }
1197
1198 $template->setVariable('SELECT_LABEL', $this->txt('select'));
1199 $template->setVariable('CLOSE_LABEL', $this->txt('close'));
1200 $template->setVariable('LABEL', $component->getLabel());
1201
1202 $template->setVariable('INPUT_TEMPLATE', $default_renderer->render(
1203 $component->getTemplateForDynamicInputs()
1204 ));
1205 $template->setVariable('BREADCRUMB_TEMPLATE', $default_renderer->render(
1206 $this->getUIFactory()->breadcrumbs([$this->getUIFactory()->link()->standard('label', '#')])
1207 ));
1208 $template->setVariable('BREADCRUMBS', $default_renderer->render(
1209 $this->getUIFactory()->breadcrumbs([])
1210 ));
1211
1213 $dynamic_inputs_generator = (static fn() => yield from $component->getGeneratedDynamicInputs())();
1214
1215 $leaf_generator = $component->getNodeRetrieval()->getNodesAsLeaf(
1216 $this->getUIFactory()->input()->field()->node(),
1217 $this->getUIFactory()->symbol()->icon(),
1218 $component->getValue(),
1219 );
1220
1221 $lockstep_iterator = $this->iterateGeneratorsInLockstep($leaf_generator, $dynamic_inputs_generator);
1222 $sync_node_id_whitelst = [];
1223
1224 foreach ($lockstep_iterator as [$leaf, $dynamic_input]) {
1225 // check against internal interface, will not be delegated to rendering chain.
1227 $this->checkArgInstanceOf('leaf', $leaf, Node\Leaf::class);
1228
1229 $value_template = $this->getTemplate('tpl.tree_select.html', true, true);
1230 $value_template->setCurrentBlock('with_value_template');
1231 $value_template->setVariable('NODE_ID', (string) ($leaf->getId()));
1232 $value_template->setVariable('NODE_NAME', $leaf->getName());
1233 $value_template->setVariable('INPUT_TEMPLATE', $default_renderer->render($dynamic_input));
1234 $value_template->setVariable('UNSELECT_NODE_LABEL', sprintf($this->txt('unselect_node'), $leaf->getName()));
1235 $value_template->parseCurrentBlock();
1236
1237 $template->setCurrentBlock('with_value');
1238 $template->setVariable('VALUE', $value_template->get('with_value_template'));
1239 $template->parseCurrentBlock();
1240
1241 foreach ($leaf->getFullPath() as $node_id) {
1242 // deduplicate overlaping node ids by using them as offset
1243 $sync_node_id_whitelst[$node_id] = $node_id;
1244 }
1245 }
1246
1247 $node_factory = $this->getUIFactory()->input()->field()->node();
1248 $node_generator = $component->getNodeRetrieval()->getNodes(
1249 $node_factory,
1250 $this->getUIFactory()->symbol()->icon(),
1251 array_values($sync_node_id_whitelst),
1252 null,
1253 );
1254
1255 $nodes = [];
1256 foreach ($node_generator as $node) {
1257 // check against public interface, will be delegated to rendering chain.
1258 $this->checkArgInstanceOf('node', $node, Component\Input\Field\Node\Node::class);
1259 $nodes[] = $node;
1260 }
1261
1262 $template->setVariable('DRILLDOWN', $default_renderer->render(
1263 $this->getUIFactory()->menu()->drilldown($component->getLabel(), $nodes)
1264 ));
1265
1266 $this->toJS('unselect_node');
1267 $this->toJS('select_node');
1268
1269 return $template;
1270 }
1271
1279 protected function iterateGeneratorsInLockstep(\Generator $a, \Generator $b): \Generator
1280 {
1281 while ($a->valid() && $b->valid()) {
1282 yield [$a->current(), $b->current()];
1283 $a->next();
1284 $b->next();
1285 }
1286 if ($a->valid() || $b->valid()) {
1287 throw new LogicException('Generators do not have equal lenghts.');
1288 }
1289 }
1290
1291 private function setHelpBlockForFileField(Template $template, FI\File $input): void
1292 {
1293 $template->setCurrentBlock('HELP_BLOCK');
1294
1295 $template->setCurrentBlock('MAX_FILE_SIZE');
1296 $template->setVariable('FILE_SIZE_LABEL', $this->txt('file_notice'));
1297 $template->setVariable('FILE_SIZE_VALUE', new DataSize($input->getMaxFileSize(), DataSize::Byte));
1298 $template->parseCurrentBlock();
1299
1300 $template->setCurrentBlock('MAX_FILES');
1301 $template->setVariable('FILES_LABEL', $this->txt('ui_file_upload_max_nr'));
1302 $template->setVariable('FILES_VALUE', $input->getMaxFiles());
1303 $template->parseCurrentBlock();
1304
1305 $template->parseCurrentBlock();
1306 }
1307
1315 protected function renderOptionFilter(string $input_html, F\HasOptionFilterInternal $component, RendererInterface $default_renderer): array
1316 {
1317 $option_filter_template = $this->getTemplate("tpl.option_filter.html", true, true);
1318 $option_filter_template->setVariable('INPUT', $input_html);
1319
1320 $search_input_id = $this->createId();
1321 $search_input_label_id = $this->createId();
1322 $search_input_description_id = $this->createId();
1323 $list_id = $this->createId();
1324
1325 $option_filter_template->setVariable('SEARCH_INPUT_ID', $search_input_id);
1326 $option_filter_template->setVariable('SEARCH_INPUT_LABEL_ID', $search_input_label_id);
1327 $option_filter_template->setVariable('SEARCH_INPUT_DESCRIPTION_ID', $search_input_description_id);
1328 $option_filter_template->setVariable('LIST_ID', $list_id);
1329
1330 $no_selection_text = $this->txt('ui_field_option_filter_no_selection');
1331 $option_filter_template->setVariable('NOTHING_SELECTED', $no_selection_text);
1332 $option_filter_template->setVariable('ARIA_FILTERED_RESULTS', $this->txt('ui_field_option_filter_filtered_results_aria_label'));
1333
1334 $option_filter_template->setVariable('SEARCH_LABEL', $this->txt("ui_field_option_filter_search_in"));
1335 $option_filter_template->setVariable('SCREEN_READER_HINT', $this->txt('ui_field_option_filter_screen_reader_hint'));
1336 $option_filter_template->setVariable('NO_MATCH', $this->txt('ui_field_option_filter_no_match'));
1337 $option_filter_template->setVariable('OPTIONS_SHOWN', $this->txt('ui_field_option_filter_options_shown'));
1338
1339 $expand_icon = $default_renderer->render($this->getUIFactory()->symbol()->glyph()->expand());
1340 $option_filter_template->setVariable('EXPAND_TEXT', $expand_icon . $this->txt('ui_field_option_filter_show_all_options'));
1341
1342 $collapse_icon = $default_renderer->render($this->getUIFactory()->symbol()->glyph()->collapseHorizontal());
1343 $option_filter_template->setVariable('COLLAPSE_TEXT', $collapse_icon . $this->txt('ui_field_option_filter_show_less'));
1344
1345 $remove_icon = $default_renderer->render($this->getUIFactory()->symbol()->glyph()->remove());
1346 $option_filter_template->setVariable('CLEAR_SEARCH_BTN', $remove_icon . $this->txt('ui_field_option_filter_clear_search'));
1347
1348 $component = $component->withAdditionalOnLoadCode(
1349 static fn($id): string => "il.UI.Input.optionFilter.init(document.getElementById('$id'));",
1350 );
1351
1352 return [$option_filter_template->get(), $component];
1353 }
1354
1355 private function mustacheVariableEntities(): Closure
1356 {
1357 return function ($val) {
1358 $val = htmlentities((string) $val);
1359 return str_replace('{{', '&lcub;&lcub;', str_replace('}}', '&rcub;&rcub;', $val));
1360 };
1361 }
1362}
$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:869
bindJSandApplyId(Component\JavaScriptBindable $component, Template $tpl)
Definition: Renderer.php:244
renderDateTimeField(F\DateTime $component, RendererInterface $default_renderer)
Definition: Renderer.php:756
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:669
renderTagField(F\Tag $component, RendererInterface $default_renderer)
Definition: Renderer.php:398
setHelpBlockForFileField(Template $template, FI\File $input)
Definition: Renderer.php:1291
renderMultiSelectField(F\MultiSelect $component, RendererInterface $default_renderer)
Definition: Renderer.php:715
renderNumericField(F\Numeric $component, RendererInterface $default_renderer)
Definition: Renderer.php:316
renderColorSelectField(F\ColorSelect $component, RendererInterface $default_renderer)
Definition: Renderer.php:1092
renderOptionFilter(string $input_html, F\HasOptionFilterInternal $component, RendererInterface $default_renderer)
Renders a list search around input fields that support it.
Definition: Renderer.php:1315
renderOptionalGroup(F\OptionalGroup $component, RendererInterface $default_renderer)
Definition: Renderer.php:343
renderSection(F\Section $section, RendererInterface $default_renderer)
Definition: Renderer.php:841
applyName(FormInput $component, Template $tpl)
Definition: Renderer.php:237
renderUrlField(F\Url $component, RendererInterface $default_renderer)
Definition: Renderer.php:858
internalRenderDateTimeField(F\DateTime $component, RendererInterface $default_renderer)
Definition: Renderer.php:767
renderFileField(F\File $input, RendererInterface $default_renderer)
Definition: Renderer.php:874
renderDurationField(F\Duration $component, RendererInterface $default_renderer)
Definition: Renderer.php:818
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:992
prepareDropzoneJsMimeTypes(array $mime_types)
Appends all given mime-types to a comma-separated string.
Definition: Renderer.php:1082
renderMustacheVariables(F\HasMustacheVariablesInternal $component)
Definition: Renderer.php:635
renderRatingField(F\Rating $component, RendererInterface $default_renderer)
Definition: Renderer.php:1103
registerResources(ResourceRegistry $registry)
Announce resources this renderer requires.
Definition: Renderer.php:943
renderTreeSelectField(F\TreeSelect $component, RendererInterface $default_renderer)
Definition: Renderer.php:1175
renderFilePreview(FI\File $file_input, FormInput $metadata_input, RendererInterface $default_renderer, ?FileInfoResult $file_info, Template $template)
Definition: Renderer.php:1007
render(Component\Component $component, RendererInterface $default_renderer)
Definition: Renderer.php:88
renderTreeMultiSelectField(F\TreeMultiSelect $component, RendererInterface $default_renderer)
Definition: Renderer.php:1156
iterateGeneratorsInLockstep(\Generator $a, \Generator $b)
Iterates over two Generators in lockstep, yielding their current values as paired arrays which can be...
Definition: Renderer.php:1279
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